近期业务需要通过tc
将某些网卡流量通过隧道镜像到其他IP
进行分析。实现思路参考这篇文章:
理想情况上,采集网卡和隧道承载网卡应该是不同网卡。然而,一些服务器不具备多块网卡。当它们使用同一网卡时很容易形成环路,如下图(来自上述文章):
上述文章也提到Linux
内核存在BUG,当直接从egress
队列镜像到隧道接口时,可能会导致死锁。比如RedHat
这个kb
链接:
解决方案是可以先将流量镜像到loopback
接口, 再从loopback
接口镜像到隧道接口。这种方式下,我们可以在lo
接口上添加bpf
规则避免形成环路,也可以按需求添加bpf
规则过滤不需要的流量。
但实际上这样依然存在风险。根据tc
官方说明,这么做还是不推荐的:
1 | What NOT to do if you don't want your machine to crash: |
我们场景的转发路径就是:eth0->lo->vxlan->eth0
。根据我们较长时间的实践,确认这样的确会触发BUG。经过研究,我们可以实现额外的loopback
接口设备来规避BUG。后续其他文章再来说明该BUG及解决方案,本文来分析镜像流量是否会影响netfilter
。
因为我们的服务器上还运行着自开发的netfilter
内核模块,那么流量镜像到lo
接口后,流量会经过NF_HOOK
吗?
为了确定这个问题,开始进行实验调研。
实验机器网卡eth1
的IP
为:192.168.33.12
,在eth1
上配置镜像egress
方向的TCP
端口为9091
的流量到lo
网卡:
1 | tc qdisc add dev eth1 root handle 1: htb |
命令中的bpf
字节码可以通过iptables
的工具nfbpf_compile
来生成:
1 | [root@default bin]# ./nfbpf_compile EN10MB 'tcp port 9091' |
然后编写实验用的netfilter
模块, 它在收到9091
端口的数据包时打印设备名称:
1 |
|
Makefile
内容为:
1 | .PHONY: all |
编译并加载:
1 | make |
接着,使用tcpdump
在lo
接口上抓包,并从该机器上向外访问9091
端口来发送TCP syn
包:
1 | [root@default ~]# curl http://192.168.33.1:9091 -v |
这时lo
接口上tcpdump
的输出:
1 | [root@default nfqlo]# tcpdump -ilo -nn tcp port 9091 |
运行dmesg
查看内核模块的输出:
1 | [ 273.621144] [nfqlo]: DEV: (no dev) |
可以看到,tcpdump
在lo
上抓到了数据包,而从dmesg
输出看到,数据包并未进入netfilter
处理。
那么数据包在哪里被丢弃的呢?我们继续分析。
IP
协议栈入口函数为ip_rcv
, 在执行完NF_HOOK
之后会调用ip_rcv_finish
,ip_rcv
源码(CentOS
的linux-3.10.0-1127.el7.x86_64
)如下:
1 | /* |
我们使用systemtap
工具来查看两个函数的执行情况。编写systemtap
脚本hook
两个函数:
1 | probe kernel.function("ip_rcv") { |
运行systemtap
并再次发送请求后输出如下:
1 | [root@default systemtap]# stap -g -v 1.stap |
可以看到ip_rcv
被调用了,而ip_rcv_finish
没有被调用,可以确认数据包是在ip_rcv
函数中被丢弃了。
从源码分析,ip_rcv
函数开头部分的pkt_type
检查逻辑是最可疑的地方:
1 | /* When the interface is in promisc. mode, drop all the crap |
修改systemtap
脚本,添加pkt_type
输出:
1 | printf("ip_rcv_finish: skb: %p, %s:%d->%s:%d, dev: %s, pkt_type: %d\n", $skb, saddr, sport, daddr, dport, kernel_string($skb->dev->name), $skb->pkt_type) |
再次运行并发送请求后,确认skb->pkt_type
的确为PACKET_OTHERHOST
, 数据包被丢弃:
1 | ip_rcv: skb: 0xffff8d6410ced200, 192.168.33.12:52986->192.168.33.1:9091, dev: lo, pkt_type: 3 |
1 |
那么skb->pkt_type
是哪里赋值为PACKET_OTHERHOST
的呢?
梳理tc
镜像流量的网络路径,有如下这些观察点:
tc
镜像流量时会调用tcf_mirred
函数- 流量为
egress
方向,tcf_mirred
会调用dev_queue_xmit
dev_queue_xmit
最终会调用网卡驱动的.ndo_start_xmit
函数,lo
网卡的ndo_start_xmit
为loopback_xmit
loopback_xmit
会调用netif_rx
函数,最终会调用__netif_receive_skb
函数__netif_receive_skb
会调用ip_rcv
函数
如图所示:
再次修改systemtap
脚本:
1 | %{ |
运行脚本并再次请求后输出:
1 | dev_queue_xmit: skb: 0xffff8d643ce40af8, 192.168.33.12:52996->192.168.33.1:9091, dev: eth1, pkt_type: 0 |
可以看到skb_clone
后的sk_buff
地址为0xffff8d6423433800
, 它在进入loopback_xmit
时,pkt_type
为0
,在进入netif_rx
时,pkt_type
变为了3
。
再来看loopback_xmit
的源码:
1 | /* |
可以看到,loopback_xmit
在调用netif_rx
前调用了eth_type_trans
函数。eth_type_trans
函数源码如下:
1 | /** |
可以看到,eth_type_trans
函数会比较数据包的目的地址和设备的物理地址,当二者不一致时就会将skb->pkt_type
设置为PACKET_OTHERHOST
。
1 | if (unlikely(!ether_addr_equal_64bits(eth->h_dest, |
至此,我们可以确定整个过程: 当流量被tc
镜像到lo
接口后,继续走lo
设备的发送流程,过程中由于镜像过来的数据包的目的MAC
地址和lo
接口的MAC
地址不一致,从而将skb->pkt_type
设置为PACKET_OTHERHOST
, 表示数据包不是发送给该接口的。loopback_xmit
继续调用netif_rx
函数接收数据包,执行到ip_rcv
时,由于pkt_type
为PACKET_OTHERHOST
而被丢弃。
参考: