问题现象
tcpdump/iproute2 是很多网络问题下的调试工具,大家也都很熟他的一些特性。比如,
- 执行tcpdump命令后,如果没有使用
-p
参数, 网口会进入PROMISC状态。 - 通过ip link命令可以查看网口状态,其中状态位里有个
PROMISC
标志。
但是,后台执行一个在 eth0 口上抓包的tcpdump命令,就会发现一个问题:
- 当tcpdump命令在后台运行时,通过查看
ip link
结果,对应网口的状态标志位没有PROMISC
标志位,也没有其他变化。
为了避免干扰,关闭tcpdump 后,我们又做了另外一个验证操作。
- 执行
ip link set dev eth0 promisc on
命令,设置网口混杂模式。这时,网口的PROMISC
标志位能正常显示出来。
这样看来,PROMISC
标志位只受ip link
命令控制,与在网口上是否运行tcpdump无关。
看到这里,不免就有疑问了
- Q1: tcpdump执行时候,对应网口到底进没进入
PROMISC
混杂模式呢?
A:进入PROMISC
状态了。 因为对应时间段里,dmesg
内核日志有明确的记录。
1 | [5828194.373058] virtio_net virtio0 eth0: entered promiscuous mode |
注意,ip link 命令设置 on 的时候,网口也会进入PROMISC
混杂模式。
- Q2: 既然tcpdump和 iplink都能使网口进入混杂模式,这两个命令又可以独立执行。运行tcpdump命令抓包时,被ip link命令设置了
promisc off
, 网卡会退出混杂模式,会不会对tcdpump抓包造成影响?
A:命令可以并发执行,不会相互干扰。这个场景下,ip link不会让网口退出混杂模式,也不会影响后续的 tcpdump 抓包。promisc off
命令只会让 ip link 显示的网口的状态标志位里的PROMISC
被清除掉。 - Q3: 多个tcpdum并行抓包,退出有先后,会不会相互影响?
A: 并行tcpdump执行,不会影响网口PROMISC
。内核有引用计数机制,保证最后一个 tcpdump 退出时候,网口也跟着关闭混杂模式。
原理
在内核里,针对每个网口上,会维护3 个标志位,来相应上层命令,并控制并记录网口的PROMISC
状态。
- gflags
- promiscuity
- flags
net_device:gflags
+ 其中有一个bit 被用作PROMISC
标志位,是一个promisc开关(标志位)。
+ 跟ip link命令里设置promisc on/off
对应,跟网口状态没有直接关联。每当ip link命令设置promisc on
时候,就设置标志位。同理,off时候就清除标志位。
+ ip link命令的结果中,在网口状态中的PROMISC
标记是这个bit的状态。
+ 连续重复的on命令,等同一次on命令。同理,off也是。+ 只有on/off状态切换时候,才会操作下面的promiscuity
。
net_device: promiscuity
- 这是个引用计数值,默认值0,对应网口处在非
PROMISC
模式。- 每次有上层命令(tcpdump,gflags切换)才会增加和减少。
+ 如果promiscuity 从正整数变为0,则网口退出混杂模式。相反则进入混杂模式。负数就麻烦了,吃出BUG了 :(
- 引用计数+1的操作场景:
gflags从 off 变为 on
- tcpdump 开始执行时
引用计数+1的操作场景:
gflags从 on 变为 off
- tcpdump 退出时
net_device: flags
- flags:这里也有一个bit 被用作
PROMISC
标志位。并且位置是跟前面的gflags位置相同。这个PROMISC
状态标志位,与物理网口的PROMISC
状态是保持一致的,是真正的网口混杂模式的标志。驱动代码里根据这个标志位,来决定设置/清除硬件寄存器,进而打开或者关闭网口的混杂模式。
ip link代码分析
命令ip link set promisc on
的函数调用栈
1 | int main(int argc, char **argv) |
这里有几点需要关注:
- netlink 命令类型是
RTM_SETLINK
, 这决定了内核处理请求时候,对应的消息处理函数。 req->i.ifi_flags
:保存了需要设置或清除的标志位的预期值。比如IFF_PROMISC
标志位。- req->i.ifi_change:需要设置或清除的标志位的掩码。
1 | 693 } else if (strcmp(*argv, "promisc") == 0) { |
ip link的内核实现
数据结构
struct net_device
里有3个跟 PROMISC
相关的变量
1 | 2084 struct net_device { |
如前原理所述,ip link打印的PROMISC
标记是来自于dev->gflags
。promiscuity
是个计数器,决定了flags
里的PROMISC
标记,flags
跟物理网口混杂模式相对应。
1
93 IFF_PROMISC = 1<<8, /* sysfs */
函数调用栈
1 | int rtnl_newlink(struct sk_buff *skb, struct nlmsghdr *nlh, struct netlink_ext_ack *extack) |
备注1:这里真正改变网口标志位的函数是__dev_change_flags
,
还有一个函数__dev_notify_flags
是用来通知其他模块, 网口标志位变化的。如UP
/DOWN
标志位对路由和IP地址模块会有影响。在PROMISC
场景下,关联模块影响不大,不展开讨论。
备注 2:***rtnl_setlink
***
对于RTM_SETLINK
类型的命令请求,内核对应的处理函数是rtnl_newlink
内核的 netlink详细处理机制, 先不展开。
1 | 7053 {.msgtype = RTM_SETLINK, .doit = rtnl_setlink, |
函数 rtnl_dev_combine_flags
1 | 1092 static unsigned int rtnl_dev_combine_flags(const struct net_device *dev, |
这里 ifm->ifi_flags
是ip link
根据命令行解析,并传递给内核的。同样传递给内核的ifm->ifi_change
,对应希望变化的那些标志位的掩码。
rtnl_dev_combine_flags根据掩码ifm->ifi_change
,把这些标记位从将当前 netdev 的标志位上清除(与操作置零),然后设置为上层应用期望设置的值(预期值放在ifm->ifi_flags)。
函数 rtnl_dev_get_flags
netdev设备对应的标志位不是存放在一个变量里,而是函数dev_get_flags
拟合成的。
1 | 1086 static unsigned int rtnl_dev_get_flags(const struct net_device *dev) |
函数 __dev_change_flags
PROMISC
不是更新到dev->flags
,而是更新到dev->gflags
.
参数flags
存放着期望的标志位。
1 | 9420 int __dev_change_flags(struct net_device *dev, unsigned int flags, |
注意__dev_set_promiscuity函数主要更新promiscuity
, 只有 0 和非零切换时候,才会调整dev->flags
里的IFF_PROMISC
。
9468 这行的 dev->flags != old_flags
, 针对的就是dev->flags
更新了IFF_PROMISC
标志位的场景。
函数 __dev_set_promiscuity
1 | 9244 static int __dev_set_promiscuity(struct net_device *dev, int inc, bool notify) |
最后会调用 dev_change_rx_flags(dev, IFF_PROMISC);
, 但除了个别虚拟口(如 vlan)有动作,大部分网口,其实是在__dev_change_flags
里,当__dev_set_promiscuity
的成功返回后,调用dev_set_rx_mode
去更新网口的PROMISC
状态。
注意:
这个函数是个基础函数, tcpdump时,最终也会调用到这个函数。
函数dev_set_rx_mode
对于PROMISC
标志,会去调用驱动的对应函数设置。
1
2
3
4
5
69382 void dev_set_rx_mode(struct net_device *dev)
9383 {
9384 netif_addr_lock_bh(dev);
9385 __dev_set_rx_mode(dev);
9386 netif_addr_unlock_bh(dev);
9387 }
1 | 9354 void __dev_set_rx_mode(struct net_device *dev) |
网口驱动的 ndo_set_rx_mode
以e1000网卡驱动为例,如果网口flags
里有PROMISC
标志,设置对应网口的硬件寄存器标志位。
具体逻辑见 2254~2256。
这里涉及到网口支持多个单播的 MAC 地址,就不展开讨论代码实现。
1 | 2235 static void e1000_set_rx_mode(struct net_device *netdev) |
1 | 1810 #define E1000_RCTL_UPE 0x00000008 /* unicast promiscuous enable */ |
函数dev_get_flags
ip link 读取网口状态时,PROMISC
标志位是通过读写dev->gflags
的,而不是dev->flags
。
这个跟前面设置PROMISC
相对应。
1 | 9395 unsigned int dev_get_flags(const struct net_device *dev) |
tcpdump/libpcap 代码分析
函数调用栈
1 | main |
tcpdump抓包
命令行:tcpdump -i eth0
, 这里没有-p
参数。
1 | 1493 main(int argc, char **argv) |
1 | 1259 static pcap_t * |
默认 tcpdump 命令不会使用-p
参数,因此打开网口抓包时候,需要把网口设置成混杂模式。
对应代码逻辑是,命令不使用-p
参数,pflag保持初始值0,对应pcap_open_live的 ``参数 1,启用混杂模式.
1 | pcap_open_live(const char *device, int snaplen, int promisc, int to_ms, char *errbuf) |
libpcap代码分析
1 |
|
1 | 2600 int |
函数setup_socket
如果指定了网口(is_any_device为0)抓包,并且有 PROMISC 要求,则通过 socketopt 设置网口进入 promisc 状态。
1 | 2324 static int |
tcpdump的内核实现
函数调用栈
tcpdump 的内核实现比较复杂,这里只摘录跟设置PROMISC
状态相关的代码。
1 | packet_setsockopt |
函数packet_setsockopt
tcpdump 对应的 socket 类型是af_packet
, 对应的 socket opt 的处理函数为packet_setsockopt
。
对应setsockopt
调用时候,传递的两个参数SOL_PACKET
和PACKET_ADD_MEMBERSHIP
。
1 | 3833 static int |
函数packet_mc_add
这里会把 tcpdump 传递下来的参数,网口index值mr_ifindex
和类型PACKET_MR_PROMISC
,
为后续打开网口混杂模式做准备。
这里最后一个参数是1
1 | 3730 static int packet_mc_add(struct sock *sk, struct packet_mreq_max *mreq) |
函数packet_dev_mc
这个函数里会调用协议栈的基础dev_set_promiscuity
函数设置网口进入混杂模式。
其他部分跟PROMISC
无关,处理的是多个组播或者单播地址的场景。
1 | 3685 static int packet_dev_mc(struct net_device *dev, struct packet_mclist *i, |
函数dev_set_promiscuity
1 | 282 int dev_set_promiscuity(struct net_device *dev, int inc) |
函数netif_set_promiscuity
注意__dev_set_promiscuity函数主要更新promiscuity
, 只有 0 和非零切换时候,才会调整dev->flags
里的IFF_PROMISC
。如果更新了IFF_PROMISC
标志位, 就会满足 if 判断条件, 调用dev_set_rx_mode(dev)。 由网口根据dev->flags里IFF_PROMISC
标志位,状态去更新网口,进入或者退出混杂模式。
1 | 9300 int netif_set_promiscuity(struct net_device *dev, int inc) |