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 パケットです。
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; }
完全なサンプルは 👇にあります。
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()
のようにできます。
こちらも完全なサンプルは 👇。
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 にする必要があります。
まとめ
Ruby や Python 向けに (もちろんその2つに限らずですが)、libwireshark をリンクした native extension を作っておくと便利です。