tcpdump命令与网口`PROMISC`状态标志位

问题现象

tcpdump/iproute2 是很多网络问题下的调试工具,大家也都很熟他的一些特性。比如,

  • 执行tcpdump命令后,如果没有使用-p参数, 网口会进入PROMISC状态。
  • 通过ip link命令可以查看网口状态,其中状态位里有个PROMISC标志。
    但是,后台执行一个在 eth0 口上抓包的tcpdump命令,就会发现一个问题:
  1. 当tcpdump命令在后台运行时,通过查看ip link结果,对应网口的状态标志位没有PROMISC标志位,也没有其他变化。
    tcpdump 执行时, 网口标志位里没有PROMiSC
    为了避免干扰,关闭tcpdump 后,我们又做了另外一个验证操作。
  • 执行ip link set dev eth0 promisc on命令,设置网口混杂模式。这时,网口的PROMISC标志位能正常显示出来。
    ip link设置网口标志位PROMiSC
    这样看来,PROMISC标志位只受 ip link命令控制,与在网口上是否运行tcpdump无关。

看到这里,不免就有疑问了

  • Q1: tcpdump执行时候,对应网口到底进没进入PROMISC混杂模式呢?
    A:进入PROMISC状态了。 因为对应时间段里,dmesg内核日志有明确的记录。
1
2
[5828194.373058] virtio_net virtio0 eth0: entered promiscuous mode
[5828198.130489] virtio_net virtio0 eth0: left 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
    状态标志位PROMISC的实现原理

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
2
3
4
5
6
7
8
9
10
11
int main(int argc, char **argv)
|-> do_cmd(basename+2, argc, argv, false);
|-> c->func(argc-1, argv+1)); //相当于 do_iplink
do_iplink
|-> iplink_modify(RTM_NEWLINK, 0, argc-1, argv+1);
|-> iplink_parse(argc, argv, &req, &type);
|-> if (strcmp(*argv, "promisc") == 0) {
|-> req->i.ifi_change |= IFF_PROMISC;
|-> req->i.ifi_flags |= IFF_PROMISC;
|-> .n.nlmsg_type = cmd; //RTM_SETLINK
|-> rtnl_talk(&rth, &req.n, NULL);

这里有几点需要关注:

  • netlink 命令类型是RTM_SETLINK , 这决定了内核处理请求时候,对应的消息处理函数。
  • req->i.ifi_flags:保存了需要设置或清除的标志位的预期值。比如IFF_PROMISC标志位。
  • req->i.ifi_change:需要设置或清除的标志位的掩码。
1
2
3
4
5
6
7
8
693                 } else if (strcmp(*argv, "promisc") == 0) {
694 NEXT_ARG();
695 req->i.ifi_change |= IFF_PROMISC; //设置掩码
696
697 if (strcmp(*argv, "on") == 0)
698 req->i.ifi_flags |= IFF_PROMISC; //如果是 on,则设置标志位
699 else if (strcmp(*argv, "off") == 0)
700 req->i.ifi_flags &= ~IFF_PROMISC; //如果是 off,则清除标志位

ip link的内核实现

数据结构

struct net_device里有3个跟 PROMISC 相关的变量

1
2
3
4
5
6
7
2084 struct net_device {
...
2133 unsigned int flags;
...
2192 unsigned short gflags;
...
2268 unsigned int promiscuity;

如前原理所述,ip link打印的PROMISC标记是来自于dev->gflags
promiscuity是个计数器,决定了flags里的PROMISC标记,flags跟物理网口混杂模式相对应。

1
93         IFF_PROMISC                     = 1<<8,  /* sysfs */

函数调用栈

1
2
3
4
5
6
7
8
9
10
int rtnl_newlink(struct sk_buff *skb, struct nlmsghdr *nlh, struct netlink_ext_ack *extack)
|-> __rtnl_newlink(skb, nlh, ops, tgt_net, link_net, peer_net, tbs, data, extack);
|-> rtnl_changelink(skb, nlh, ops, dev, tgt_net, tbs, data, extack);
|-> do_setlink(skb, dev, tgt_net, nlmsg_data(nlh), extack, tb, status);
|-> if (ifm->ifi_flags || ifm->ifi_change) {
|-> netif_change_flags(dev, rtnl_dev_combine_flags(dev, ifm), extack);
|-> __dev_change_flags(dev, flags, extack);
|-> inc = (flags & IFF_PROMISC) ? 1 : -1;
|-> __dev_set_promiscuity(dev, inc, false)
|-> __dev_notify_flags(dev, old_flags, changes, 0, NULL);

备注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
2
3
4
5
6
7
8
9
10
11
12
1092 static unsigned int rtnl_dev_combine_flags(const struct net_device *dev,
1093 const struct ifinfomsg *ifm)
1094 {
1095 unsigned int flags = ifm->ifi_flags;
1096
1097 /* bugwards compatibility: ifi_change == 0 is treated as ~0 */
1098 if (ifm->ifi_change)
1099 flags = (flags & ifm->ifi_change) |
1100 (rtnl_dev_get_flags(dev) & ~ifm->ifi_change);
1101
1102 return flags;
1103 }

这里 ifm->ifi_flagsip link根据命令行解析,并传递给内核的。同样传递给内核的ifm->ifi_change,对应希望变化的那些标志位的掩码。
rtnl_dev_combine_flags根据掩码ifm->ifi_change,把这些标记位从将当前 netdev 的标志位上清除(与操作置零),然后设置为上层应用期望设置的值(预期值放在ifm->ifi_flags)。

函数 rtnl_dev_get_flags

netdev设备对应的标志位不是存放在一个变量里,而是函数dev_get_flags拟合成的。

1
2
3
4
5
1086 static unsigned int rtnl_dev_get_flags(const struct net_device *dev)
1087 {
1088 return (dev->flags & ~(IFF_PROMISC | IFF_ALLMULTI)) |
1089 (dev->gflags & (IFF_PROMISC | IFF_ALLMULTI));
1090 }

函数 __dev_change_flags

PROMISC不是更新到dev->flags,而是更新到dev->gflags.

参数flags存放着期望的标志位。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 9420 int __dev_change_flags(struct net_device *dev, unsigned int flags,
9421 struct netlink_ext_ack *extack)
9422 {
...
9461 if ((flags ^ dev->gflags) & IFF_PROMISC) { // 判读PROMISC 位是否需要更新, 重复的 ON/OFF 也在这里被滤掉
9462 int inc = (flags & IFF_PROMISC) ? 1 : -1;
9463 old_flags = dev->flags;
9464
9465 dev->gflags ^= IFF_PROMISC; //更新IFF_PROMISC标志位,跟 flags 里的一致。
9466
9467 if (__dev_set_promiscuity(dev, inc, false) >= 0) //更新promiscuity计数,和dev->flags的PROMISC
9468 if (dev->flags != old_flags) //PROMISC位别修改了
9469 dev_set_rx_mode(dev);
9470 }
...

注意__dev_set_promiscuity函数主要更新promiscuity, 只有 0 和非零切换时候,才会调整dev->flags里的IFF_PROMISC
9468 这行的 dev->flags != old_flags, 针对的就是dev->flags更新了IFF_PROMISC标志位的场景。

函数 __dev_set_promiscuity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 9244 static int __dev_set_promiscuity(struct net_device *dev, int inc, bool notify)
9245 {
...
9253 promiscuity = dev->promiscuity + inc; //根据调用要求更新引用计数,并更新IFF_PROMISC的标志位。
9254 if (promiscuity == 0) {
...
9263 flags = old_flags & ~IFF_PROMISC; //如果计数为0, 则是清除标志位。如果非0,设置标志位。最新的标志位会被更新到***`dev->flags`***。
9264 } else {
9265 flags = old_flags | IFF_PROMISC;
9266 }
9267 WRITE_ONCE(dev->promiscuity, promiscuity);
9268 if (flags != old_flags) {
9269 WRITE_ONCE(dev->flags, flags); //回写`IFF_PROMISC`位到`dev->flags`
...
9285 dev_change_rx_flags(dev, IFF_PROMISC);
9286 }

最后会调用 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
6
9382 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
2
3
4
5
6
 9354 void __dev_set_rx_mode(struct net_device *dev)
9355 {
...
9378 if (ops->ndo_set_rx_mode)
9379 ops->ndo_set_rx_mode(dev);
9380 }

网口驱动的 ndo_set_rx_mode

以e1000网卡驱动为例,如果网口flags里有PROMISC标志,设置对应网口的硬件寄存器标志位。
具体逻辑见 2254~2256。
这里涉及到网口支持多个单播的 MAC 地址,就不展开讨论代码实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2235 static void e1000_set_rx_mode(struct net_device *netdev)
2236 {
...
2252 rctl = er32(RCTL);
2253
2254 if (netdev->flags & IFF_PROMISC) {
2255 rctl |= (E1000_RCTL_UPE | E1000_RCTL_MPE);
2256 rctl &= ~E1000_RCTL_VFE;
2257 } else {
2258 if (netdev->flags & IFF_ALLMULTI)
2259 rctl |= E1000_RCTL_MPE;
2260 else
2261 rctl &= ~E1000_RCTL_MPE;
2262 /* Enable VLAN filter if there is a VLAN */
2263 if (e1000_vlan_used(adapter))
2264 rctl |= E1000_RCTL_VFE;
2265 }
2266
2267 if (netdev_uc_count(netdev) > rar_entries - 1) {
2268 rctl |= E1000_RCTL_UPE;
2269 } else if (!(netdev->flags & IFF_PROMISC)) {
2270 rctl &= ~E1000_RCTL_UPE;
2271 use_uc = true;
2272 }
2273
2274 ew32(RCTL, rctl);
1
2
3
1810 #define E1000_RCTL_UPE            0x00000008    /* unicast promiscuous enable */
1811 #define E1000_RCTL_MPE 0x00000010 /* multicast promiscuous enab */
1838 #define E1000_RCTL_VFE 0x00040000 /* vlan filter enable */

函数dev_get_flags

ip link 读取网口状态时,PROMISC标志位是通过读写dev->gflags的,而不是dev->flags
这个跟前面设置PROMISC相对应。

1
2
3
4
5
6
7
8
9
10
11
12
9395 unsigned int dev_get_flags(const struct net_device *dev)
9396 {
9397 unsigned int flags;
9398
9399 flags = (READ_ONCE(dev->flags) & ~(IFF_PROMISC |
9400 IFF_ALLMULTI |
9401 IFF_RUNNING |
9402 IFF_LOWER_UP |
9403 IFF_DORMANT)) |
9404 (READ_ONCE(dev->gflags) & (IFF_PROMISC |
9405 IFF_ALLMULTI));
9406

tcpdump/libpcap 代码分析

函数调用栈

1
2
3
4
5
6
7
8
9
main
|-> pd = open_interface(device, ndo, ebuf);
|-> status = pcap_activate(pc);
|-> status = p->activate_op(p); // handle->activate_op = pcap_activate_linux;
相当于 pcap_activate_linux
|-> ret = setup_socket(handle, is_any_device);
|-> mr.mr_ifindex = handlep->ifindex;
|-> mr.mr_type = PACKET_MR_PROMISC;
|-> if (setsockopt(sock_fd, SOL_PACKET, PACKET_ADD_MEMBERSHIP, &mr, sizeof(mr)) == -1)

tcpdump抓包

命令行:tcpdump -i eth0, 这里没有-p参数。

1
2
3
4
5
6
7
8
1493 main(int argc, char **argv)
1494 {
...
1820 case 'p':
1821 ++pflag;
1822 break;
...
2229 pd = open_interface(device, ndo, ebuf);
1
2
3
4
5
1259 static pcap_t *
1260 open_interface(const char *device, netdissect_options *ndo, char *ebuf)
1261 {
...
1473 pc = pcap_open_live(device, ndo->ndo_snaplen, !pflag, timeout, ebuf);

默认 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
2
3

2874 status = pcap_set_promisc(p, promisc);

1
2
3
4
5
6
7
2600 int
2601 pcap_set_promisc(pcap_t *p, int promisc)
2602 {
...
2605 p->opt.promisc = promisc;
2606 return (0);
2607 }

函数setup_socket

如果指定了网口(is_any_device为0)抓包,并且有 PROMISC 要求,则通过 socketopt 设置网口进入 promisc 状态。

1
2
3
4
5
6
7
8
9
10
11
2324 static int
2325 setup_socket(pcap_t *handle, int is_any_device)
2326 {
...
2595 if (!is_any_device && handle->opt.promisc) {
2596 memset(&mr, 0, sizeof(mr));
2597 mr.mr_ifindex = handlep->ifindex;
2598 mr.mr_type = PACKET_MR_PROMISC;
2599 if (setsockopt(sock_fd, SOL_PACKET, PACKET_ADD_MEMBERSHIP,
2600 &mr, sizeof(mr)) == -1) {
...

tcpdump的内核实现

函数调用栈

tcpdump 的内核实现比较复杂,这里只摘录跟设置PROMISC状态相关的代码。

1
2
3
4
5
packet_setsockopt
|-> packet_mc_add(sk, &mreq);
|-> packet_dev_mc(dev, i, 1);
|-> dev_set_promiscuity(dev, what); //what是上一行调用时的最后参数,`1`
|-> netif_set_promiscuity(dev, inc);

函数packet_setsockopt

tcpdump 对应的 socket 类型是af_packet, 对应的 socket opt 的处理函数为packet_setsockopt
对应setsockopt调用时候,传递的两个参数SOL_PACKETPACKET_ADD_MEMBERSHIP

1
2
3
4
5
6
7
8
3833 static int
3834 packet_setsockopt(struct socket *sock, int level, int optname, sockptr_t optval,
3835 unsigned int optlen)
3836 {
...
3859 if (optname == PACKET_ADD_MEMBERSHIP)
3860 ret = packet_mc_add(sk, &mreq);
...

函数packet_mc_add

这里会把 tcpdump 传递下来的参数,网口index值mr_ifindex和类型PACKET_MR_PROMISC
为后续打开网口混杂模式做准备。
这里最后一个参数是1

1
2
3
4
5
6
7
8
3730 static int packet_mc_add(struct sock *sk, struct packet_mreq_max *mreq)
3731 {
...
3766 i->type = mreq->mr_type;
3767 i->ifindex = mreq->mr_ifindex;
...
3775 err = packet_dev_mc(dev, i, 1);

函数packet_dev_mc

这个函数里会调用协议栈的基础dev_set_promiscuity函数设置网口进入混杂模式。
其他部分跟PROMISC无关,处理的是多个组播或者单播地址的场景。

1
2
3
4
5
6
7
8
9
3685 static int packet_dev_mc(struct net_device *dev, struct packet_mclist *i,
3686 int what)
3687 {
3688 switch (i->type) {
...
3697 case PACKET_MR_PROMISC:
3698 return dev_set_promiscuity(dev, what);
...
3713 }

函数dev_set_promiscuity

1
2
3
4
5
6
7
8
9
10
11
282 int dev_set_promiscuity(struct net_device *dev, int inc)
283 {
284 int ret;
285
286 netdev_lock_ops(dev);
287 ret = netif_set_promiscuity(dev, inc);
288 netdev_unlock_ops(dev);
289
290 return ret;
291 }
292 EXPORT_SYMBOL(dev_set_promiscuity);

函数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
2
3
4
5
6
7
8
9
10
9300 int netif_set_promiscuity(struct net_device *dev, int inc)
9301 {

9305 err = __dev_set_promiscuity(dev, inc, true);
9306 if (err < 0)
9307 return err;
9308 if (dev->flags != old_flags)
9309 dev_set_rx_mode(dev);
9310 return err;
9311 }