tcpdumpのproto/protochainの違い
Feb 28, 2024
TL;DR
- proto は protocol number でフィルターするが、直下の protocol しか評価しない
ip proto \tcp
はIP/TCP
にマッチする。IP/AH/AH/TCP
はマッチしない。
- protochain は protocol number でフィルターするが、ネストされた protocol にも対応している
ip protochain \tcp
はIP/TCP
、IP/AH/AH/TCP
の両方にマッチする
- protochain は offset を変更するわけではない
IP/IP
の inner header にマッチさせようと、ip protochain ipv4 and src 10.0.0.1
と書くのは誤り- この場合、outer header の src == 10.0.0.1 で評価される。
tcpdump
- パケットキャプチャをする際によく tcpdump が利用されると思うのだが、引数にパケットを評価する filter を書くことができる。
- filter はpcap-filterとも呼ばれ、人間の読みやすい形から最終的に bpf の実行コードにコンパイルされ、評価される。
- tcpdump の-d オプションを使うことで、実際に利用されるアセンブリを見ることが可能。
- pcap-filter の
proto
キーワードを使うことにより、対象パケットのプロトコルによるフィルターをかけることができる。- 例:
tcpdump -nn -i any "proto \tcp"
- 例:
protochain
と呼ばれるキーワードもあり、こちらはプロトコルヘッダチェーン(AH header など)に対応している- 今回、ipip の inner field に対してマッチさせたかったが、思うような挙動をとらなかった。
- iptables bpf module に関しては後ほど記事化する予定。
cbpf/ebpf
- tcpump が最終的に利用するものは bpf(Berkeley Packet Filter)と呼ばれている。
- 最近は bpf を拡張した ebpf と呼ばれるものが流行り、それに対応して従来の bpf は cbpf と呼ばれるようになった。
- 今回話すのは cbpf の話。
- cbpf で利用できる命令コードなどは以下を参照。
- https://www.kernel.org/doc/Documentation/networking/filter.txt
- https://docs.kernel.org/networking/filter.html
ip proto
実際に ip proto でフィルターをかけた際のアセンブリを見ていく。
(コメントは後から付与したもの。出力結果には書いてない。)
; tcpdump -d "ip proto ipv4 and src 10.0.0.1"
(000) ldh [12] ; a = eth.type
(001) jeq #0x800 jt 2 jf 7 ; if (a == ETHERTYPE_IP) goto 2 else goto 7
(002) ldb [23] ; a = ip.proto
(003) jeq #0x4 jt 4 jf 7 ; if (a = IPPROTO_TCP) goto 4 else goto 7
(004) ld [26] ; a = ip.saddr
(005) jeq #0xa000001 jt 6 jf 7 ; if (a == 10.0.0.1) goto 6 else goto 7
(006) ret #262144
(007) ret #0
命令コードは少なくシンプル。順番に評価を行って false な条件があったら jump をするようなコードになっている。
見ているフィールドは 3 つ。
- eth.type == ETHERTYPE_IP
- ip.proto == IPPROTO_TCP
- ip.saddr == 10.0.0.1
ip protochain
; tcpdump -d "ip protochain ipv4 and src 10.0.0.1"
(000) ldh [12] ; a = eth.type
(001) jeq #0x800 jt 2 jf 35 ; if (a == ETHERTYPE_IP) goto 2 else goto 35
(002) ldb [23] ; a = ip.proto
(003) ldxb 4*([14]&0xf) ; x = ip.ihl*4
; parse offset loop start (a: protocol, x: nexthdr length)
(004) jeq #0x4 jt 20 jf 5 ; if (a == IPPROTO_TCP) goto 20 else goto 5
(005) jeq #0x3b jt 20 jf 6 ; if (a == IPPROTO_NONE) goto 20 else goto 6
(006) add #0 ;
(007) jeq #0x33 jt 8 jf 20 ; if (a == IPPROTO_AH) goto 8 else goto 20
(008) txa ; a = x
(009) ldb [x + 14] ; a = ah.nexthdr
(010) st M[0] ; M[0] = a
(011) txa ; a = x
(012) add #1 ; a = a+1
(013) tax ; x = a
(014) ldb [x + 14] ; a = ah.len
(015) add #2 ; a = a+2
(016) mul #4 ; a = a*4
(017) tax ; x = a
(018) ld M[0] ; a = M[0]
(019) ja 4 ; goto 4
; parse offset loop end
(020) add #0
(021) jeq #0x4 jt 22 jf 35 ; if (a == IPPROTO_IP) goto 22 else goto 35
(022) ldh [12] ; a = eth.type
(023) jeq #0x800 jt 24 jf 26 ; if (a == ETHERTYPE_IPV4) goto 24 else goto 26
(024) ld [26] ; a = ip.saddr
(025) jeq #0xa000001 jt 34 jf 26 ; if (a == 10.0.0.1) goto 34 else goto 26
(026) ldh [12] ; a = eth.type
(027) jeq #0x806 jt 28 jf 30 ; if (a == ETHERTYPE_ARP) goto 28 else goto 30
(028) ld [28] ; a = arp.spa
(029) jeq #0xa000001 jt 34 jf 30 ; if (a == 10.0.0.1) goto 34 else goto 30
(030) ldh [12] ; a = eth.type
(031) jeq #0x8035 jt 32 jf 35 ; if (a == ETHERTYPE_RARP) goto 32 else goto 35
(032) ld [28] ; a = rarp.spa
(033) jeq #0xa000001 jt 34 jf 35 ; if (a == 10.0.0.1) goto 34 else goto 35
; return
(034) ret #262144
(035) ret #0
大きく分けて 2 つの部分に分けられる。(and の前と後)
前半
ここは ip protochain ipv4
に当たる内容の部分になっている。
ip.proto == IPPROTO_IP(IPIP のばあい)、ip.proto == IPPROTO_NONE の場合は次のセクション(後半)へ、IPPROTO_AH の場合はループをしながら IPPROTO_IP か IPPROTO_NONE が存在しないかを探している。
(000) ldh [12] ; a = eth.type
(001) jeq #0x800 jt 2 jf 35 ; if (a == ETHERTYPE_IP) goto 2 else goto 35
(002) ldb [23] ; a = ip.proto
(003) ldxb 4*([14]&0xf) ; x = ip.ihl*4
; parse offset loop start (a: protocol, x: nexthdr length)
(004) jeq #0x4 jt 20 jf 5 ; if (a == IPPROTO_IP) goto 20 else goto 5
(005) jeq #0x3b jt 20 jf 6 ; if (a == IPPROTO_NONE) goto 20 else goto 6
(006) add #0 ;
(007) jeq #0x33 jt 8 jf 20 ; if (a == IPPROTO_AH) goto 8 else goto 20
(008) txa ; a = x
(009) ldb [x + 14] ; a = ah.nexthdr
(010) st M[0] ; M[0] = a
(011) txa ; a = x
(012) add #1 ; a = a+1
(013) tax ; x = a
(014) ldb [x + 14] ; a = ah.len
(015) add #2 ; a = a+2
(016) mul #4 ; a = a*4
(017) tax ; x = a
(018) ld M[0] ; a = M[0]
(019) ja 4 ; goto 4
; parse offset loop end
後半
後半は至ってシンプル。 src 10.0.0.1
に当たる評価式になる。
前半部分に関わらず、ip.saddr や、arp.spa、rarp.spa などを評価している。
(020) add #0
(021) jeq #0x4 jt 22 jf 35 ; if (a == IPPROTO_IP) goto 22 else goto 35
(022) ldh [12] ; a = eth.type
(023) jeq #0x800 jt 24 jf 26 ; if (a == ETHERTYPE_IPV4) goto 24 else goto 26
(024) ld [26] ; a = ip.saddr
(025) jeq #0xa000001 jt 34 jf 26 ; if (a == 10.0.0.1) goto 34 else goto 26
(026) ldh [12] ; a = eth.type
(027) jeq #0x806 jt 28 jf 30 ; if (a == ETHERTYPE_ARP) goto 28 else goto 30
(028) ld [28] ; a = arp.spa
(029) jeq #0xa000001 jt 34 jf 30 ; if (a == 10.0.0.1) goto 34 else goto 30
(030) ldh [12] ; a = eth.type
(031) jeq #0x8035 jt 32 jf 35 ; if (a == ETHERTYPE_RARP) goto 32 else goto 35
(032) ld [28] ; a = rarp.spa
(033) jeq #0xa000001 jt 34 jf 35 ; if (a == 10.0.0.1) goto 34 else goto 35
ここで重要なのはパケットの固定位置からロードしている、ということ。
例えば、ip.saddr を読み取るときは ld [26]
のように、パケット頭から 26byte 目を読み取っている。
これは前半部分の protochain の部分は考慮されずに、評価されていることになる。
そのため、ip protochain ipv4 and src 10.0.0.1
では outer src ip が 10.0.0.1 である、ということを表している。
おまけ
- 今回と同じ内容(質問?)が tcpdum の issue に上がっている。
- https://github.com/the-tcpdump-group/libpcap/issues/1133