how tcpdump work with vlan filter

问题场景

tcpdump在网口直接抓包和读取pcap文件两种场景下,同一个filter表达式icmp,对vlan报文有不同的处理结果。

  • 在网口上抓包可以看到vlan和非vlan两种流量
  • 读取pcap只能看到非vlan流量一种流量

【Q1】:在读取pcap文件时候,为什么不能读取到vlan报文呢?

分析结论

内核协议栈:

  1. 过滤条件 icmp:tcpdump 解析出来的 ebpf 指令,是要求报文是eth+IP+ICMP格式。通过ETH/IP 头里的协议类型分别做了限制: IPv4(0x0800)+ICMP(1)。

  2. 在网口上抓包:内核代码里,在tcpdump抓包过滤的钩子函数之前,会把报文的vlan解析并剔除掉。被剥离的vlan头信息,被保存到skb的metadata里了。所以 tcpdump(af_packet)在内核里,通过钩子函数运行过滤条件的 ebpf 指令时,被处理的报文已经不带vlan头了。因此只要满足icmp 头,带和不带 vlan头的报文都会被抓取到。

  3. 读取pcap文件:tcpdump 直接读取文件内容,vlan头并没有被剥离掉,所以vlan报文不满足过滤条件(eth+IP+ICMP),被丢弃了。

国外已经有人发现并分析过这个问题:BPF and tcpdump

复现问题

网络拓扑

1
2
3
4
5
6
3.3.3.3/24                 3.3.3.2/24
vlan3 vlan2 (ns2)
| vlanid 333 |
| |
veth3 --------------------veth2
1.1.1.3/24 1.1.1.2/24

复现步骤

  1. 创建一个新的netns, ns2
  2. 创建veth对, veth2-veth3,其中veth2被放置到ns2里
  3. 分别在veth2和veth3口上建立vlan口,vlan ID 333
  4. 依次为四个网口配置IP地址
    • 3.3.3.3/24
    • 1.1.1.3/24
    • 1.1.1.2/24
    • 3.3.3.2/24
  5. 在vlan2上ping vlan3, 在veth2上ping veth3

网口veth3上抓包,显示有vlan和非vlan两种流量:

1
2
3
4
5
6
7
8
9
10
11
root@martin-HOST:/home/martin# tcpdump  -n -i veth3 icmp -ne -c 4
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on veth3, link-type EN10MB (Ethernet), snapshot length 262144 bytes
18:27:50.825272 82:40:26:88:ee:93 > f6:f4:f8:0c:17:ea, ethertype 802.1Q (0x8100), length 102: vlan 333, p 0, ethertype IPv4 (0x0800), 3.3.3.2 > 3.3.3.3: ICMP echo request, id 56302, seq 33168, length 64
18:27:50.825332 f6:f4:f8:0c:17:ea > 82:40:26:88:ee:93, ethertype 802.1Q (0x8100), length 102: vlan 333, p 0, ethertype IPv4 (0x0800), 3.3.3.3 > 3.3.3.2: ICMP echo reply, id 56302, seq 33168, length 64
18:27:50.856960 82:40:26:88:ee:93 > f6:f4:f8:0c:17:ea, ethertype IPv4 (0x0800), length 98: 1.1.1.2 > 1.1.1.3: ICMP echo request, id 15134, seq 33158, length 64
18:27:50.857020 f6:f4:f8:0c:17:ea > 82:40:26:88:ee:93, ethertype IPv4 (0x0800), length 98: 1.1.1.3 > 1.1.1.2: ICMP echo reply, id 15134, seq 33158, length 64
4 packets captured
4 packets received by filter
0 packets dropped by kernel
root@martin-HOST:/home/martin#

其中:

  • 【VLAN报文】 vlan ID 是333。IP 地址:3.3.3.2 到 3.3.3.3 的 ping(ICMP)流量
  • 【非VLAN报文】 IP 地址:1.1.1.2 > 1.1.1.3 的 ping 流量

使用tcpdump命令将报文保存到pcap文件:

1
2
3
4
5
root@martin-HOST:/home/martin# tcpdump  -n -i veth3 icmp -ne -c 4 -w veth3.pcap
tcpdump: listening on veth3, link-type EN10MB (Ethernet), snapshot length 262144 bytes
4 packets captured
8 packets received by filter
0 packets dropped by kernel

重新读取pcap文件中的报文。vlan 和非 vlan 报文各两个,都是 icmp 报文:

1
2
3
4
5
6
7
root@martin-HOST:/home/martin# tcpdump  -n -r veth3.pcap -e
reading from file veth3.pcap, link-type EN10MB (Ethernet), snapshot length 262144
18:31:01.801424 82:40:26:88:ee:93 > f6:f4:f8:0c:17:ea, ethertype IPv4 (0x0800), length 98: 1.1.1.2 > 1.1.1.3: ICMP echo request, id 1341, seq 43049, length 64
18:31:01.801482 f6:f4:f8:0c:17:ea > 82:40:26:88:ee:93, ethertype IPv4 (0x0800), length 98: 1.1.1.3 > 1.1.1.2: ICMP echo reply, id 1341, seq 43049, length 64
18:31:01.865407 82:40:26:88:ee:93 > f6:f4:f8:0c:17:ea, ethertype 802.1Q (0x8100), length 102: vlan 333, p 0, ethertype IPv4 (0x0800), 3.3.3.2 > 3.3.3.3: ICMP echo request, id 43947, seq 43034, length 64
18:31:01.865468 f6:f4:f8:0c:17:ea > 82:40:26:88:ee:93, ethertype 802.1Q (0x8100), length 102: vlan 333, p 0, ethertype IPv4 (0x0800), 3.3.3.3 > 3.3.3.2: ICMP echo reply, id 43947, seq 43034, length 64
root@martin-HOST:/home/martin#

再次读取 pcap 文件,并增加过滤条件icmp,结果里只有非vlan的报文,vlan 报文被过滤掉了:

1
2
3
4
5
root@martin-HOST:/home/martin# tcpdump  -n -r veth3.pcap -e icmp
reading from file veth3.pcap, link-type EN10MB (Ethernet), snapshot length 262144
18:31:01.801424 82:40:26:88:ee:93 > f6:f4:f8:0c:17:ea, ethertype IPv4 (0x0800), length 98: 1.1.1.2 > 1.1.1.3: ICMP echo request, id 1341, seq 43049, length 64
18:31:01.801482 f6:f4:f8:0c:17:ea > 82:40:26:88:ee:93, ethertype IPv4 (0x0800), length 98: 1.1.1.3 > 1.1.1.2: ICMP echo reply, id 1341, seq 43049, length 64
root@martin-HOST:/home/martin#

vlan 封装的icmp报文只能通过vlan and icmp这个过滤条件才能读取出来:

1
2
3
4
5
root@martin-HOST:/home/martin# tcpdump  -n -r veth3.pcap -e vlan and icmp
reading from file veth3.pcap, link-type EN10MB (Ethernet), snapshot length 262144
18:31:01.865407 82:40:26:88:ee:93 > f6:f4:f8:0c:17:ea, ethertype 802.1Q (0x8100), length 102: vlan 333, p 0, ethertype IPv4 (0x0800), 3.3.3.2 > 3.3.3.3: ICMP echo request, id 43947, seq 43034, length 64
18:31:01.865468 f6:f4:f8:0c:17:ea > 82:40:26:88:ee:93, ethertype 802.1Q (0x8100), length 102: vlan 333, p 0, ethertype IPv4 (0x0800), 3.3.3.3 > 3.3.3.2: ICMP echo reply, id 43947, seq 43034, length 64
root@martin-HOST:/home/martin#

详细分析

ebpf指令: filter icmp

通过dump bpf指令,我们可以看到严格按照ETH+IP+ICMP的格式过滤的。因此读取 pcap 文件时候,没有 vlan 报文,这也是符合ebpf 指令的预期结果。

1
2
3
4
5
6
7
8
root@martin-HOST:/home/martin# tcpdump  -r veth3.pcap -n -e -d  icmp
reading from file veth3.pcap, link-type EN10MB (Ethernet), snapshot length 262144
(000) ldh [12] <=== eth头的type类型
(001) jeq #0x800 jt 2 jf 5 <=== 如果是IP协议(0x0800)
(002) ldb [23] <=== 加载IP头里的protocol字段
(003) jeq #0x1 jt 4 jf 5 <=== 如果是ICMP协议(0x01), 返回true(004)
(004) ret #262144 <=== True
(005) ret #0

而在网口上抓包时候,传递给内核的bpf指令也是严格按照ETH+IP+ICMP过滤的。所以两者的bpf指令是完全相同的。

1
2
3
4
5
6
7
8
root@martin-HOST:/home/martin# tcpdump  -i veth3 -n -e -d  icmp
(000) ldh [12]
(001) jeq #0x800 jt 2 jf 5
(002) ldb [23]
(003) jeq #0x1 jt 4 jf 5
(004) ret #262144
(005) ret #0
root@martin-HOST:/home/martin#

相同的 ebpf 指令,为什么内核处理报文时候,就能够把 vlan 报文也匹配进来呢?下面我们通过内核协议栈的源码分析下原因。

内核协议栈:vlan报文头

为什么相同的指令在内核和libpcap读取文件时候执行,产生了不同的效果呢。这里有两个原因:

  1. 部分网卡可以直接在硬件层面就卸载vlan报文头,这样协议栈收到的报文就没有vlan了,而vlan信息是通过驱动读取到后,转化为skb的meta信息传递给协议栈的。
  2. 协议栈入口时候,会主动解析并剥离vlan头,等运行到ptype_all并进行af_packet抓包时候,报文已经没有vlan头了。

这两种情况,虽然处理位置不一样,但是效果是一样的,都会导致内核运行ebpf指令时候,看到的是一个eth+ip+icmp报文。报文真正的vlan头已经转化为skb的metadata信息了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
5749 static int __netif_receive_skb_core(struct sk_buff **pskb, bool pfmemalloc,
5750 struct packet_type **ppt_prev)
5751 {
...
5798 if (eth_type_vlan(skb->protocol)) {
5799 skb = skb_vlan_untag(skb); // <=== 此处已经剥离了 vlan tag
5800 if (unlikely(!skb))
5801 goto out;
5802 }
...
5810 list_for_each_entry_rcu(ptype, &dev_net_rcu(skb->dev)->ptype_all,
5811 list) { // <=== 这里开始tcpdump
5812 if (pt_prev)
5813 ret = deliver_skb(skb, pt_prev, orig_dev);
5814 pt_prev = ptype;
5815 }
5816
5817 list_for_each_entry_rcu(ptype, &skb->dev->ptype_all, list) {
5818 if (pt_prev) // <=== 这里tcpdump
5819 ret = deliver_skb(skb, pt_prev, orig_dev);
5820 pt_prev = ptype;
5821 }

ebpf指令: filter vlan

那自然就有一个疑问了,当内核需要vlan这个过滤条件时,bpf指令怎么处理呢?

首先我们看下读取 pcap 文件场景下,过滤条件vlan展开的 ebpf 指令:

1
2
3
4
5
6
7
8
9
root@martin-HOST:/home/martin# tcpdump  -n -r veth3.pcap -e vlan -d
reading from file veth3.pcap, link-type EN10MB (Ethernet), snapshot length 262144
(000) ldh [12]
(001) jeq #0x8100 jt 4 jf 2
(002) jeq #0x88a8 jt 4 jf 3
(003) jeq #0x9100 jt 4 jf 5
(004) ret #262144
(005) ret #0
root@martin-HOST:/home/martin#

在网口直接抓包场景下,过滤条件vlan展开的 ebpf 指令:

1
2
3
4
5
6
7
8
9
root@martin-HOST:/home/martin# tcpdump  -n -i veth3 vlan -d
(000) ldb [vlanp]
(001) jeq #0x1 jt 6 jf 2
(002) ldh [12]
(003) jeq #0x8100 jt 6 jf 4
(004) jeq #0x88a8 jt 6 jf 5
(005) jeq #0x9100 jt 6 jf 7
(006) ret #262144
(007) ret #0

对比读libpcap的指令,我们发现不同之处就在于内核独立了一个判断条件:

1
2
(000) ldb      [vlanp]
(001) jeq #0x1 jt 6 jf 2

dump结果里的(000) ldb [vlanp]指令,接下来我们看下这个条件对应的内核代码。

内核代码

这里的演进历史比较长,从最早的bpf到最新的ebpf扩展,这里只讨论一下最新代码,更老的渊源,看一下: