how tcpdump work with vlan filter

  • 问题描述:
    tcpdump在网口抓包和读取pcap文件,相同的filter表达式”icmp“,对vlan报文有不同的处理结果。
    在网口上抓包可以看到vlan和非vlan两种流量, 而读取pcap只能看到非vlan流量一种流量。

  • 原因:
    kernel里在tcpdump抓包之前会把报文的vlan提前解析掉,并把vlan信息放到skb的metadata里了。
    所以tcpdump(内核的af_packet对应的ptype_all)处理的报文都是不带vlan的,因此这两类报文都会被过滤出来。
    而读取pcap时候,因为vlan报文并没有被剥离掉,所以vlan报文不满足过滤条件,被丢弃了。

国外已经有人发现并分析过这个问题
https://andreaskaris.github.io/blog/networking/bpf-and-tcpdump/

  • 复现方法:

  • 网络拓扑
    Vlan3— veth3 — veth2 — vlan2
    创建一个新的netns, ns2
    创建veth对, veth2-veth3,其中veth2被放置到ns2里。
    分别在veth2和veth3口上建立vlan口,vlan ID333
    依次为四个网口配置IP地址 3.3.3.3/24 —— 1.1.1.3/24 —1.1.1.2/24 —— 3.3.3.2/24
    在vlan2上ping vlan3, 在veth2上ping veth3

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

1
2
3
4
5
6
7
8
9
10
11
root@martin-Standard-PC-Q35-ICH9-2009:/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-Standard-PC-Q35-ICH9-2009:/home/martin#

其中vlan流量是, vlan ID 是333, 3.3.3.2 到 3.3.3.3的ping流量
非vlan流量是1.1.1.2 > 1.1.1.3的ping流量

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

1
2
3
4
5
root@martin-Standard-PC-Q35-ICH9-2009:/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
1
2
3
4
5
6
7
root@martin-Standard-PC-Q35-ICH9-2009:/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-Standard-PC-Q35-ICH9-2009:/home/martin#

再重新用相同的tcpdump过滤条件”icmp“读取pcap报文,发现只能读取非vlan的报文。

1
2
3
4
5
root@martin-Standard-PC-Q35-ICH9-2009:/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-Standard-PC-Q35-ICH9-2009:/home/martin#

vlan报文只能通过”vlan and icmp“这个过滤条件才能读取出来。

1
2
3
4
5
root@martin-Standard-PC-Q35-ICH9-2009:/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-Standard-PC-Q35-ICH9-2009:/home/martin#

在读取pcap文件时候,为什么只能读取到非vlan报文呢?
通过dump bpf指令,我们可以看到严格按照ETH+IP+ICMP得格式过滤的。
这跟我们预期也是符合的

1
2
3
4
5
6
7
8
root@martin-Standard-PC-Q35-ICH9-2009:/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协议(0x8000)
(002) ldb [23] <=== 加载IP头里的protocol字段
(003) jeq #0x1 jt 4 jf 5 <=== 如果是IP协议(0x8000), 返回true(004)
(004) ret #262144 <=== True
(005) ret #0

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

1
2
3
4
5
6
7
8
root@martin-Standard-PC-Q35-ICH9-2009:/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-Standard-PC-Q35-ICH9-2009:/home/martin#

为什么相同的指令在内核和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
     5282         if (eth_type_vlan(skb->protocol)) {
    5283 skb = skb_vlan_untag(skb);
    5284 if (unlikely(!skb))
    5285 goto out;
    5286 }
    ...
    5294 list_for_each_entry_rcu(ptype, &ptype_all, list) {
    5295 if (pt_prev)
    5296 ret = deliver_skb(skb, pt_prev, orig_dev);
    5297 pt_prev = ptype;
    5298 }
    5299
    5300 list_for_each_entry_rcu(ptype, &skb->dev->ptype_all, list) {
    5301 if (pt_prev)
    5302 ret = deliver_skb(skb, pt_prev, orig_dev);
    5303 pt_prev = ptype;
    5304 }

那自然就有一个疑问了, 当内核需要vlan这个过滤条件时,bpf指令怎么处理呢?
首先我们dump下tcpdump产生的bpf指令。注意dump结果里的(000)ldb [vlanp]指令

1
2
3
4
5
6
7
8
9
root@martin-Standard-PC-Q35-ICH9-2009:/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
1
2
3
4
5
root@martin-Standard-PC-Q35-ICH9-2009:/home/martin# tcpdump  --version
tcpdump version 4.99.1
libpcap version 1.10.1 (with TPACKET_V3)
OpenSSL 3.0.2 15 Mar 2022
root@martin-Standard-PC-Q35-ICH9-2009:/home/Martin#

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

1
2
3
4
5
6
7
8
9
root@martin-Standard-PC-Q35-ICH9-2009:/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-Standard-PC-Q35-ICH9-2009:/home/martin#

接下来我们看下这个条件对应的内核代码v6.5:

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

1
2
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?h=v6.5&id=f3335031b
https://linux.kernel.narkive.com/mfgIPZOV/patch-net-next-1-2-bpf-allow-extended-bpf-programs-access-skb-fields