Mac OSX で vmnet が BIOCSETIF できなくてハマった話し
1年くらいブログ記事書くのをサボっていたので、ちょっとマニアックなネタを投下します。ISUCON3 は予選で敗退してしまった @pandax381 です。
vmnet が BIOCSETIF できない!
Mac OSX(marvericks)で VMware Fusion 6 を使っているのですが、ホスト側の仮想インタフェース(vmnetX)のパケットがキャプチャできないという問題に遭遇しました。具体的には、パケットをキャプチャするためにBPFデバイスをオープンして、vmnetX を ioctl(BIOCSETIF) でアタッチするの処理で失敗してしまうのです。一見普通のEthernetデバイスに見えるのに、どうしてBPFでパケットをキャプチャできないのか調べてみました。
tcpdump で試してみる
僕の作っていたプログラムがイケてないのかもしれないので、まずは tcpdump を使ってキャプチャできるか試してみます。そして、tcpdump先生でもダメならあきらめましょう。
tcpdump に -i オプションで vmnet8 を指定して実行してみます。
$ sudo tcpdump -i vmnet8 tcpdump: vmnet8: No such device exists (BIOCSETIF failed: Device not configured)
あれ、うまくいかない。。そういえば、OSX の tcpdump はインタフェース指定しなければ全てのインタフェースからキャプチャできたはずなので、-i オプションなしで実行してみます。
$ sudo tcpdump tcpdump: data link type PKTAP tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on pktap, link-type PKTAP (Packet Tap), capture size 65535 bytes 18:18:51.715943 IP 192.168.2.128 > 192.168.2.1: ICMP echo request, id 54865, seq 1, length 64 18:18:51.715969 IP 192.168.2.1 > 192.168.2.128: ICMP echo reply, id 54865, seq 1, length 64 18:18:52.715822 IP 192.168.2.128 > 192.168.2.1: ICMP echo request, id 54865, seq 2, length 64 18:18:52.715862 IP 192.168.2.1 > 192.168.2.128: ICMP echo reply, id 54865, seq 2, length 64 18:18:53.716669 IP 192.168.2.128 > 192.168.2.1: ICMP echo request, id 54865, seq 3, length 64 18:18:53.716705 IP 192.168.2.1 > 192.168.2.128: ICMP echo reply, id 54865, seq 3, length 64
ちゃんと vmnet8 に接続されている仮想マシンとのパケットが拾えています。tcpdump は何かしらの方法で vmnet8 を覗けているようです。
なにかオプションを指定する必要があるかもしれないので、マニュアルを見てみると・・・
On Darwin systems version 13 or later, when the interface is unspecified, tcpdump will use a pseudo interface to capture packets on a set of interfaces determined by the kernel (excludes by default loopback and tunnel interfaces). Alternatively, to capture on more than one interface at a time, one may use "pktap" as the interface parameter followed by an optional list of comma separated interface names to include. For example, to capture on the loopback and en0 interface: tcpdump -i pktap,lo0,en0
-i オプションの項目に、ずばり答えが書いてありました...^^;
・OSX(Darwin)の場合には、-i オプションを指定しないと「擬似インタフェース」を使用して全インタフェースからキャプチャする
・任意の複数のインタフェースでキャプチャする場合には -i pktap,lo0,en0 と指定する
どうやら pktap というのが「擬似インタフェース」で、これを使用すれば vmnet8 のパケットがキャプチャできそうなので、早速試してみます。
$ sudo tcpdump -i pktap,vmnet8 tcpdump: data link type PKTAP tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on pktap,vmnet8, link-type PKTAP (Packet Tap), capture size 65535 bytes 18:48:35.799610 IP 192.168.2.128 > 192.168.2.1: ICMP echo request, id 56686, seq 13, length 64 18:48:35.799679 IP 192.168.2.1 > 192.168.2.128: ICMP echo reply, id 56686, seq 13, length 64 18:48:36.802213 IP 192.168.2.128 > 192.168.2.1: ICMP echo request, id 56686, seq 14, length 64 18:48:36.802252 IP 192.168.2.1 > 192.168.2.128: ICMP echo reply, id 56686, seq 14, length 64 18:48:37.803839 IP 192.168.2.128 > 192.168.2.1: ICMP echo request, id 56686, seq 15, length 64 18:48:37.803876 IP 192.168.2.1 > 192.168.2.128: ICMP echo reply, id 56686, seq 15, length 64
ばっちりキャプチャできました!
tcpdump はどんな裏技を使っているのか
tcpdump がどんな処理をしているのかソースを解析してみます。
とりあえず、カレントバージョンの tcpdump と libpcap のソースを grep みるも「pktap」や「PKTAP」というキーワードが見当たらない。。。
仕方ないので、tcpdumpのバージョンを確認してみると
$ tcpdump -h tcpdump version 4.3.0 -- Apple version 56 libpcap version 1.3.0 - Apple version 41 Usage: tcpdump [-aAbdDefhHgIJkKlLnNOpPqQ:RStuUvxX] [ -B size ] [ -c count ] [ -C file_size ] [ -E algo:secret ] [ -F file ] [ -G seconds ] [ -i interface ] [ -j tstamptype ] [ -M secret ] [ -Q metadata-filter-expression ] [ -r file ] [ -s snaplen ] [ -T type ] [ -w file ] [ -W filecount ] [ -y datalinktype ] [ -z command ] [ -Z user ] [ expression ]
ほほう、Apple version ですとな。カスタムバージョンのようなので、http://www.opensource.apple.com/source/ から tcpdump と libpcap を探します。
- http://www.opensource.apple.com/source/tcpdump/tcpdump-56/tcpdump/
- http://www.opensource.apple.com/source/libpcap/libpcap-42/libpcap/
この中をひたすら漁っていると、libpcap の中に「pcap-darwin.c」という、いかにもそれっぽいソースファイルが見つかりました。
pcap_setup_pktap_interface() や pcap_cleanup_pktap_interface() などの関数があるので、これで当たりのようです。
ざっとソースを眺めて、以下のことが分かりました。
- ioctl(SIOCIFCREATE) :PKTAPインタフェースを作成
- ioctl(SIOCSDRVSPEC) :PKTAPインタフェースのフィルタを設定(キャプチャするデバイスと除外するデバイスを設定できる)
- ioctl(SIOCIFDESTROY):PKTAPインタフェースを削除
そして、一番の知りたかった BPF で BIOCSETIF する際の処理では、実デバイスではなくPKTAPインタフェースを指定していました。
なるほど、謎は全て解けた!
自力でやってみる
なにをすれば良いか分かったので、早速自分でコードを書いてみました。
https://github.com/pandax381/pktap_demo/
- PKTAPインタフェースを作成
- 引数で指定されたデバイスをキャプチャ対象としてフィルタを設定(複数可、指定がなければ全てのデバイスが対象)
- BPFデバイスをオープンしてPKTAPインタフェース(pktapX)を指定
- 明示的にプロミスキャスモードにはしない(BIOCPROMISC が失敗する)
- BPFからパケットをキャプチャしてデバッグ出力
PKTAPインタフェースから取得したパケットには、struct pktap_header型のリンクヘッダが付いているので、詳細出力します。
※ /usr/include/ 配下に net/pktap.h が見当たらなかったので、XNU のソースツリーから拝借するようにしています
実行結果はこんな感じ
$ sudo ./pktap_demo vmnet8 ### pktap_debug_print ### pth_length: 108 pth_type_next: 1 pth_dlt: 1 pth_ifname: vmnet8 pth_flags: 1 pth_protocol_family: 2 pth_frame_pre_length: 14 pth_frame_post_length: 0 pth_pid: -1 pth_comm: pth_svc: 0 pth_iftype: 6 pth_ifunit: 8 pth_epid: -1 pth_ecomm: +------+-------------------------------------------------+------------------+ | 0000 | 00 50 56 c0 00 08 00 0c 29 6b b6 e9 08 00 45 00 | .PV.....)k....E. | | 0010 | 00 54 00 00 40 00 40 01 b4 d7 c0 a8 02 80 c0 a8 | .T..@.@......... | | 0020 | 02 01 08 00 ac fe ec 31 00 01 1e 14 9b 52 00 00 | .......1.....R.. | | 0030 | 00 00 e5 94 01 00 00 00 00 00 10 11 12 13 14 15 | ................ | | 0040 | 16 17 18 19 1a 1b 1c 1d 1e 1f 20 21 22 23 24 25 | .......... !"#$% | | 0050 | 26 27 28 29 2a 2b 2c 2d 2e 2f 30 31 32 33 34 35 | &'()*+,-./012345 | | 0060 | 36 37 | 67 | +------+-------------------------------------------------+------------------+ ### pktap_debug_print ### pth_length: 108 pth_type_next: 1 pth_dlt: 1 pth_ifname: vmnet8 pth_flags: 2 pth_protocol_family: 2 pth_frame_pre_length: 14 pth_frame_post_length: 0 pth_pid: -1 pth_comm: pth_svc: 0 pth_iftype: 6 pth_ifunit: 8 pth_epid: -1 pth_ecomm: +------+-------------------------------------------------+------------------+ | 0000 | 00 0c 29 6b b6 e9 00 50 56 c0 00 08 08 00 45 00 | ..)k...PV.....E. | | 0010 | 00 54 b5 a8 40 00 40 01 ff 2e c0 a8 02 01 c0 a8 | .T..@.@......... | | 0020 | 02 80 00 00 b4 fe ec 31 00 01 1e 14 9b 52 00 00 | .......1.....R.. | | 0030 | 00 00 e5 94 01 00 00 00 00 00 10 11 12 13 14 15 | ................ | | 0040 | 16 17 18 19 1a 1b 1c 1d 1e 1f 20 21 22 23 24 25 | .......... !"#$% | | 0050 | 26 27 28 29 2a 2b 2c 2d 2e 2f 30 31 32 33 34 35 | &'()*+,-./012345 | | 0060 | 36 37 | 67 | +------+-------------------------------------------------+------------------+
余談
Google先生に聞いても、唯一ヒットする情報は Apple先生のソースコードだけだったので、意地になって動くコードを書く所までやってみました。社内でドヤってみたけど、レイヤが低すぎたのか誰も反応してくれないのでブログを書いてみたという落ちです。後悔はしていない。
なお、PKTAP は ifconfig でも作成できます。pktap_demo のコード書き終わってから ifconfig のコードを読んでいて発見しました...orz
$ sudo ifconfig pktap create pktap0
$ sudo ifconfig pktap destroy
コマンドで PKTAP にフィルタを設定するには、pktapctl を自分でコンパイルすればできます。
http://opensource.apple.com/source/network_cmds/network_cmds-433/pktapctl/pktapctl.c
あと、そもそもの目的は vmnet に対して出力もしたかったのですが、PKTAPだとキャプチャ専用で出力はできないっぽいです。
@pandax381