0.1. 问题现象 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 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 退出时候,网口也跟着关闭混杂模式。
0.2. 原理 在内核里,针对每个网口上,会维护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模式。
net_device: flags
flags:这里也有一个bit 被用作PROMISC标志位。并且位置是跟前面的gflags位置相同。这个PROMISC状态标志位,与物理网口的PROMISC状态是保持一致的,是真正的网口混杂模式的标志。驱动代码里根据这个标志位,来决定设置/清除硬件寄存器,进而打开或者关闭网口的混杂模式。
0.3. 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 ,则清除标志位
0.4. ip link的内核实现 0.4.1. 数据结构 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 */
0.4.2. 函数调用栈 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,
0.4.3. 函数 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_flags是ip link根据命令行解析,并传递给内核的。同样传递给内核的ifm->ifi_change,对应希望变化的那些标志位的掩码。 rtnl_dev_combine_flags根据掩码ifm->ifi_change,把这些标记位从将当前 netdev 的标志位上清除(与操作置零),然后设置为上层应用期望设置的值(预期值放在ifm->ifi_flags)。
0.4.4. 函数 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 }
0.4.5. 函数 __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 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标志位的场景。
0.4.6. 函数 __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时,最终也会调用到这个函数。
0.4.7. 函数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 }
0.4.8. 网口驱动的 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 0 x00000008 /* unicast promiscuous enable */1811 #define E1000_RCTL_MPE 0 x00000010 /* multicast promiscuous enab */1838 #define E1000_RCTL_VFE 0 x00040000 /* vlan filter enable */
0.4.9. 函数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
0.5. tcpdump/libpcap 代码分析 0.5.1. 函数调用栈 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) 相当于 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 )
0.5.2. 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)
0.5.3. 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 }
0.5.4. 函数setup_socket 如果指定了网口(is_any_device为0)抓包,并且有 PROMISC 要求,则通过 socketopt 设置网口进入 promisc 状态。
1 2 3 4 5 6 7 8 9 10 11 2324 static int2325 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->ifindex2598 mr.mr_type = PACKET_MR_PROMISC2599 if ( setsockopt( sock_fd, SOL_PACKET, PACKET_ADD_MEMBERSHIP,2600 &mr, sizeof( mr) ) == -1 ) { ...
0.6. tcpdump的内核实现 0.6.1. 函数调用栈 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) |-> netif_set_promiscuity( dev, inc)
0.6.2. 函数packet_setsockopt tcpdump 对应的 socket 类型是af_packet, 对应的 socket opt 的处理函数为packet_setsockopt。 对应setsockopt调用时候,传递的两个参数SOL_PACKET 和PACKET_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); ...
0.6.3. 函数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 );
0.6.4. 函数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 }
0.6.5. 函数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);
0.6.6. 函数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 }