tcpdump是在哪儿抓到的包?

2021-09-18 16:48:39 浏览数 (1)

导语

最近使用tcpdump的时候突然想到这个问题。因为我之前只存在一些一知半解的认识:比如直接镜像了网卡的包、在数据包进入内核前就获取了。但这些认识真的正确么?针对这个问题,我进行了一番学习探究。

结论先行

先说结论:通过PF_PACKET这个特殊的套接字协议,直接接收来自链路层的帧。数据包并非没有进入内核,而是在进入内核后直接跳过了内核中三层/四层的协议栈,直达套接字接口,被应用层的tcpdump所使用。实际上,在网卡驱动程序通知内核接受到数据帧的时候,数据包就已经进入了内核处理流程。具体的区别,可以见下图。

内核网络协议栈示意图内核网络协议栈示意图

普通套接字的收包流程

先来看看,普通的套接字的收包路径在内核中是怎么样。

以最常见的以太网网卡,当网卡接口接收到了一个帧,那么接受者知道它一定包含了一个Ethernet报头。封包在协议栈向上传递过程中,一定会在报头中包含一个字段,指出下一阶段的处理应该使用哪一个协议。 以太网卡拥有特定的MAC地址,在监听数据帧的时候,当看到帧的目的MAC地址与自己的地址或者链路层广播地址(FF:FF:FF:FF:FF:FF)相匹配,就会通过DMA把该帧读取到内存中的ring buffer。

当一个数据帧被写入到内存后,将产生一个硬件中断请求,以通知CPU收到了数据包。操作系统为了减少硬中断产生的次数,会采用一个软中断(softirq)唤醒NAPI子系统。这样会产生一个单独的线程,调用网卡驱动注册的poll方法收包,同时禁止网卡产生新的硬中断,这样的效果便是一次中断可以接收多个包。一旦软终端代码判断有softirq处于pending状态,便会调用软终端处理函数net_rx_action。

中断处理函数会在处理循环中调用NAPI poll来接收数据包。poll方法会分配一个sk_buff数据结构(include/linux/skbuff.h),表示该数据包的内核视图。然后将数据从缓冲区提取到新建的sk_buff中,并对其中的protocol字段做初始化,该字段用以识别特定的协议。之后这个字段会被netif_receive_skb内核函数查询,用来确定该执行哪个函数来处理三层的封包。字段涉及协议的值都列在了include/uapi/linux/if_ether.h中,名字形如ETH_P_XXX,比如ip协议为ETH_P_IP。而有一种特殊情况,单一封包可以传递给多个处理函数,这就是tcpdump等网络嗅探应用会用到的ETH_P_ALL。

软终端处理循环的最后是通过netif_receive_skb函数将将数据交给TCP/IP协议栈的。它会从数据包包头中取出协议信息,然后遍历注册在这个协议上的回调函数列表。这里的列表值得一提,分别是ptype_all和ptype_base。他们是hash table数据结构,分别对应通用数据包(ETH_P_ALL类型)和特定协议的数据包(ETH_P_XXX类型),其中存放着指向对应协议处理函数的指针,当收到该类型的数据包时便调用对应的处理函数。

因此,以IP数据包为例,当ETH_P_IP类型数据包出队后,软中断处理程序net_rx_action最终会在ptype_base列表中找到IP协议的处理函数ip_rcv()并调用它,完成数据包向上提交到协议栈。这里略过IP协议栈的处理过程,简而言之,在经过IP数据包完整性校验、Netfilter子系统(iptables的底层实现)、路由子系统等等一些列流程之后,开始准备送往高层协议。这里的处理和net_rx_action很相似,从IP数据包头部提取出协议类型后,通过名为inet_protos的哈希来寻找高层协议的处理函数,每个高层协议都对应一个处理函数,型如tcp_v4_rcv(), udp_rcv()等。

四层协议以较为简单的UDP为例,udp_rcv会对udp包进行合法性校验,然后查找是否有愿意接收此数据包的套接字,如果找到,__udp_queue_rcv_skb会将包放到socket的接收队列。最后,所有在这个socket上等待数据的进程都会收到通过sk_data_ready函数处理的通知。

以上是一个数据包穿越协议栈到达socket的简要过程,实际的内核处理过程会复杂的多,这里只是做简要的描述。以引入本文的主角:PF_PACKET协议数据包在内核中的处理路径。

PF_PACKET套接字的收包流程

当创建PF_PACKET套接字时,与协议相关的数据包类型将被同时注册进ptype_all和ptype_base,接受函数为packet_rcb()。此时,net_rx_action函数会拦截所有进入机器的包,并同样通过netif_receive_skb函数遍历ptype_all后,传递给PF_PACKET接受函数。值得一提的是,tcpdump依赖的libpcap库并非使用原始套接字 recvfrom的方式收包,而是在内核空间分配一块内核缓冲区,然后用户空间调用mmap系统调用映射到用户空间。

参考资料

Monitoring and Tuning the Linux Networking Stack: Receiving Data

Inside the Linux Packet Filter

图解linux tcpdump

《深入理解Linux网络技术内幕》

0 人点赞