LGTM

Looks Good To Me

libwireshark のdissector を借りて、バイト列をパケットとしてデコードする

Wireshark という強力なプロトコルアナライザーがあります。多くのプロトコルをサポートし、「どんなパケットが流れているか分からないが、プロトコルスタックの深いところまで解析したい」場合には 非常に頼りになります。

この記事では、いくつかのプログラミング言語上のバイト列を libwireshark を使ってパケット解析してみます。

直面している問題

ネットワーク運用のために、sflow をきちんと解析したいと思っています。netflow では、ネットワークの制約により 欲しい統計が取れないことがあるためです。

  • トラフィック制御のために、Q-in-Q、IPIP、GRE などのトンネルプロトコルの出番が増えた
  • バックボーンを流れるときには、さらにMPLS ラベルやSegment Routing Header がつく

このようにプロトコルスタックが深い場合、高価なASICであったとしても ネットワークデバイスでデコードする前提のnetflow には限界があります。それより、単純に「先頭のNバイトをまるっとコピーしてexport し、コレクター側でなんとかする」というsflow アプローチのほうが、目的に合うこともあるでしょう。N は100~200B 程度なので、そこそこ上位レイヤーまで解析可能です。

sflow をきちんと解析したい別な理由として、マーチャントシリコンを搭載したスイッチ製品が普及し 単純にsflow を扱うケースが増えた、というのもあります。

具体的な問題の例

👇のデータを見てください。fluent-plugin-sflow を使って収集しているレコードのひとつです。

{
  "datagram_source_ip": "192.168.0.2",
  "datagram_size": 220,
  "unix_seconds_utc": 1537117560,
  "datagram_version": 5,
  "agent_sub_id": 0,
  "agent": "169.254.0.2",
  "packet_sequence_no": 766,
  "sys_up_time": 4513318,
  "samples_in_packet": 1,
  "sample_type_tag": "0:1",
  "sample_type": "flow_sample",
  "sample_sequence_no": 1227,
  "source_id": "0:512",
  "mean_skip_count": 1,
  "sample_pool": 1228,
  "drop_events": 0,
  "input_port": 512,
  "output_port": 0,
  "flow_block_tag": "0:1001",
  "header_protocol": 1,
  "sampled_packet_size": 106,
  "stripped_bytes": 4,
  "header_len": 102,
  "header_bytes": "02-05-86-71-74-03-02-05-86-71-64-03-88-47-00-01-01-40-45-00-00-54-4A-95-00-00-40-01-AE-C0-C0-A8-00-02-C0-A8-00-01-08-00-B3-1D-40-0E-00-4D-5B-1D-85-C7-00-08-38-97-08-09-0A-0B-0C-0D-0E-0F-10-11-12-13-14-15-16-17-18-19-1A-1B-1C-1D-1E-1F-20-21-22-23-24-25-26-27-28-29-2A-2B-2C-2D-2E-2F-30-31-32-33-34-35-36-37",
  "dst_mac": "020586717403",
  "src_mac": "020586716403",
  "in_vlan": 0,
  "in_priority": 0,
  "out_vlan": 0,
  "out_priority": 0
}

Ethernet であることはわかりますが、それより上のレイヤーはデコードできていません。これでは何のことかぜんぜんわからない。

Wireshark 👇で見るとわかるように、実際はMPLS ラベルがついたICMP パケットです。

f:id:codeout:20180917021941p:plain

fluent-plugin-sflow は、sflow を策定しているInMon 公式のsFlow Tools を内包し、なるべく普及している方法でデコードを試みていますが、そのプログラムがMPLS に対応していません。

現実問題として あらゆるプロトコルスタックに対応するのは無理なのですが、Wireshark を使えばかなりイイ線いけそうではあります。

やってみる

現在のところ、残念ながらlibwireshark に「バイト列を受け取ってデコード結果を返す」というAPI がありません。Wireshark ファミリーのプログラム向けに、libpcap を用いて

からバイト列を読むAPI があるだけです。

とはいうものの、libpcap の部分をすっ飛ばして「外から受け取ったバイト列をもつフレームをでっち上げる」ことは難しくなさそうです。

void rawshark_process_packet(uint8_t *data, int len, FILE *file) {
    guint32 cum_bytes = 0;
    gint64 data_offset = 0;
    frame_data fdata;
    epan_dissect_t *edt;

    // パケット解析器を初期化する
    cfile.epan = epan_new();
    cfile.epan->get_frame_ts = raw_get_frame_ts;
    cfile.epan->get_interface_name = NULL;
    cfile.epan->get_interface_description = NULL;

    edt = epan_dissect_new(cfile.epan, TRUE, TRUE);

    // NIC やpcap ファイルからフレームを読み出す代わり、
    // 外から受け取ったバイト列を持つフレームをでっち上げる
    struct wtap_pkthdr *whdr = g_malloc(sizeof(struct wtap_pkthdr));

    whdr->rec_type = REC_TYPE_PACKET;
    whdr->pkt_encap = wtap_pcap_encap_to_wtap_encap(1); /* ETHERNET */
    whdr->caplen = len;
    whdr->len = whdr->caplen;
    whdr->opt_comment = NULL;

    cfile.count++;

    frame_data_init(&fdata, cfile.count, whdr, data_offset, cum_bytes);
    frame_data_set_before_dissect(&fdata, &cfile.elapsed_time, &ref, prev_dis);

    if (ref == &fdata) {
        ref_frame = fdata;
        ref = &ref_frame;
    }

    // 解析する
    epan_dissect_run(edt, cfile.cd_t, whdr, frame_tvbuff_new(&fdata, data), &fdata, &cfile.cinfo);

    // フレームと解析器を解放し、結果を JSON で返す
    frame_data_destroy(&fdata);
    g_free(whdr);

    frame_data_set_after_dissect(&fdata, &cum_bytes);
    prev_dis_frame = fdata;
    prev_dis = &prev_dis_frame;

    prev_cap_frame = fdata;
    prev_cap = &prev_cap_frame;

    write_json_proto_tree(NULL, print_dissections_expanded,
                          TRUE, NULL, PF_NONE,
                          edt, file);

    epan_dissect_free(edt);
    edt = NULL;

    epan_free(cfile.epan);
}

これを native extension 化すれば、たとえばfluentd 向けに「Ruby の世界のバイト列を C の世界にもっていってlibwireshark で解析し、Ruby の世界に戻す」ことが可能です。

Ruby + libwireshark

Ruby と C の橋渡し部分。

VALUE rb_rawshark_process_packet(VALUE self, VALUE data) {
    uint8_t *raw = (uint8_t *) StringValuePtr(data);
    int rawlen = (int) RSTRING_LEN(data);
    VALUE json;

    char *buf = NULL;
    size_t buflen = 0;
    FILE *out = open_memstream(&buf, &buflen);

    rawshark_process_packet(raw, rawlen, out);
    fclose(out);

    if (buflen > 4) {
        // NOTE: Truncate leading "  ,\n"
        json = rb_str_new2(buf + 4 * sizeof(char));
    }

    free(buf);
    return json;
}

完全なサンプルは 👇にあります。

github.com

Ruby での利用例。

require 'wireshark'

Wireshark.load

raw = File.read(File.expand_path('../raw_frame', __FILE__))
puts Wireshark.dissect(raw)

Wireshark.unload

Python + libwireshark

C の (uint8_t *) に持って来られればもとの言語はなんでもよく、たとえば Python であればCython をつかって

def dissect(data):
    cdef char *buf = NULL;
    cdef size_t buflen = 0;
    cdef FILE *out = open_memstream(&buf, &buflen)

    rawshark_process_packet(data, len(data), out)
    fclose(out)

    if buflen > 4:
        json = buf[4:buflen]

    free(buf)
    return json.decode()

のようにできます。

こちらも完全なサンプルは 👇。

github.com

Python での利用例

import os
import wireshark

base_dir = os.path.dirname(os.path.abspath(__file__))

with open(os.path.join(base_dir, 'raw_frame'), 'rb') as raw:
    wireshark.load()
    print(wireshark.dissect(raw.read()))
    wireshark.unload()

デコード結果

長くなるので掲載しませんが、なんとWireshark で見る相当のデータがJSON で手に入ります 🎉🎉🎉

もし興味があれば👇のgist をご覧ください。Ethernet Frame の奥のMPLS ラベルの奥のIP パケットの奥のICMP ヘッダが取れています。

Decoded sFlow data with libwireshark · GitHub

最後に、注意点をいくつか

libwireshark は遅い

libwireshark は非常に多くのプロトコルをサポートします。subdissector によって可能な限り再帰的に、上位レイヤーに向かってプロトコル解析するという動作のため、パフォーマンスは望めません。


( 図: 先のMPLS ラベルつきICMP パケットの解析パフォーマンス )

ruby version:      2.5.1
python version:    3.6.5
compiler:          GCC 7.3.0
platform:          Linux-4.15.0-34-generic-x86_64-with-Ubuntu-18.04-bionic
cpu model:         Intel(R) Core(TM) i7-4980HQ CPU @ 2.80GHz  # 2793.532 MHz

gprof 結果。libwireshark に実装されている、フレーム初期化部分が重そうなことがわかります。

  %   cumulative   self              self     total
 time   seconds   seconds    calls  Ts/call  Ts/call  name
  0.00      0.00     0.00    10000     0.00     0.00  frame_tvbuff_new
  0.00      0.00     0.00    10000     0.00     0.00  rawshark_process_packet
  0.00      0.00     0.00        1     0.00     0.00  cap_file_init
  0.00      0.00     0.00        1     0.00     0.00  rawshark_clean
  0.00      0.00     0.00        1     0.00     0.00  rawshark_init

もし「プロトコルスタックが固定で、高速にデコードしたい」「固定ではないが、最大でも3レイヤーデコードすればOK」のような場合は適さないかもしれません。

( libwireshark でも「3レイヤーまで」などの指定は可能かもしれませんが、未調査です )

libwireshark のライセンス

Wireshark は、GPL v2 です。これを動的リンクするプログラムもGPL v2 にする必要があります。

まとめ

RubyPython 向けに (もちろんその2つに限らずですが)、libwireshark をリンクした native extension を作っておくと便利です。