当前位置: 首页 > 科技观察

用户态tcpdump是如何抓取内核网络数据包的?

时间:2023-03-17 19:44:07 科技观察

今天我们就来说说工作中经常用到的tcpdump。在发送和接收网络数据包的过程中,大部分工作都是在内核态完成的。那么问题来了,我们常用的运行在用户态的程序tcpdump是如何抓取内核态数据包的呢?有同学知道tcpdump是基于libpcap的,那么libpcap的工作原理是什么呢?如果让你裸写一个抓包程序,你有什么想法吗?按照小编的风格,不搞清楚底层的原理,我们是不会罢手的。于是对相关源码进行了深入分析。通过本文,您将彻底了解以下问题。tcpdump是如何工作的?tcpdump能抓到netfilter过滤的数据包吗?如果让你自己写一个抓包程序怎么开始?带着这几个问题,让我们开始今天的探索之旅吧!1、网络包接收流程在上一篇文章中,我们详细介绍了网络包是如何从网卡到达用户进程的。我们可以简单的用下图来表示这个过程。找到tcpdump的抓包点我们在网络设备层的代码中找到了tcpdump的抓包入口。在__netif_receive_skb_core函数中,会遍历ptype_all上的协议。记得上面我们提到tcpdump在ptype_all上注册虚拟协议。这个时候就可以执行了。看函数://file:net/core/dev.cstaticint__netif_receive_skb_core(structsk_buff*skb,boolpfmemalloc){...//遍历ptype_all(tcpdump把虚拟协议挂在这里)list_for_each_entry_rcu(ptype,&ptype_all,list){if(!ptype->dev||ptype->dev==skb->dev){if(pt_prev)ret=deliver_skb(skb,pt_prev,orig_dev);pt_prev=ptype;}}}在上述函数中遍历ptype_all,并使用deliver_skb调用协议中的回调函数。//file:net/core/dev.cstaticinlineintdeliver_skb(...){returnpt_prev->func(skb,skb->dev,pt_prev,orig_dev);}对于tcpdump来说,会进入packet_rcv(我们后面会说成What正在进入这个功能)。该函数在net/packet/af_packet.c文件中。//file:net/packet/af_packet.cstaticintpacket_rcv(structsk_buff*skb,...){__skb_queue_tail(&sk->sk_receive_queue,skb);...}可以看出packet_rcv将接收到的skb放入当前packetsocket在接收队列中。这样后面调用recvfrom的时候,就可以拿到抓包了!!再次找到netfilter过滤点。为了解释我们一开始提到的问题,这里我们将稍微多看一下协议层。在ip_rcv中我们发现了netfilter相关的执行逻辑。//file:net/ipv4/ip_input.cintip_rcv(...){...returnNF_HOOK(NFPROTO_IPV4,NF_INET_PRE_ROUTING,skb,dev,NULL,ip_rcv_finish);}如果使用NF_HOOK作为关键词搜索,也可以找到许多netfilter过滤点。但是,所有的过滤点都位于IP协议层。在接收数据包的过程中,数据包首先经过网络设备层,然后到达协议层。然后我们的一个开场问题就有了答案。如果我们设置netfilter规则,在接收数据包的过程中,工作在网络设备层的tcpdump会先开始工作。在netfilter过滤之前,tcpdump抓到了数据包!所以在收包的过程中netfilter过滤不会影响tcpdump的抓包!二、网络包发送过程我们再来看看网络包发送过程。发送过程可以用一张简单的图来概括。找到netfilter过滤点。在发送的过程中,还进入了IP层各种netfilter规则的过滤。//file:net/ipv4/ip_output.cintip_local_out(structsk_buff*skb){//执行netfilter过滤err=__ip_local_out(skb);}int__ip_local_out(structsk_buff*skb){...returnnf_hook(NFPROTO_IPV4,NF_INET_LOCAL_OUT,skb,NULL,skb_dst(skb)->dev,dst_output);}在这个文件中,还可以看到几个netfilter的过滤逻辑。找到tcpdump抓包点。发送过程中协议层处理到达网络设备层时,还有一个tcpdump抓包点。//文件:net/core/dev.cintdev_hard_start_xmit(structsk_buff*skb,structnet_device*dev,structnetdev_queue*txq){...if(!list_empty(&ptype_all))dev_queue_xmit_nit(skb,dev);}staticvoiddev_queue_xmit_nit(structsk_buff*skb,structnet_device*dev){list_for_each_entry_rcu(ptype,&ptype_all,list){if((ptype->dev==dev||!ptype->dev)&&(!skb_loop_sk(ptype,skb))){if(pt_prev){deliver_skb(skb2,pt_prev,skb->dev);pt_prev=ptype;continue;}......}}}在上面的代码中,我们可以看到在dev_queue_xmit_nit中遍历了ptype_all中的协议,调用了deliver_skb反过来。这将执行到tcpdump所依赖的虚拟协议。在网络数据包的发送过程中,正好与接收过程相反,先处理协议层,后处理网络设备层。如果netfilter设置了过滤规则,会直接在协议层过滤掉。在底层网络设备层工作的tcpdump将无法再捕获该网络数据包。3.TCPDUMP启动前面两节我们提到了内核收发包是通过遍历ptype_all来执行抓包的。那么现在让我们看看用户态tcpdump是如何将协议挂载到内部ptype_all的。我们使用strace命令捕获tcpdump命令的系统调用,结果中有一行socket系统调用。Tcpdump的秘密来源就在于这个对socket函数的调用。#stracetcpdump-ieth0socket(AF_PACKET,SOCK_RAW,768)......socket系统调用的第一个参数表示创建的socket所属的地址族或协议族,取值以AF或PF开头。在Linux中,支持许多协议族,所有定义都可以在include/linux/socket.h中找到。这里创建了一个packet类型的socket。协议族和地址族:每个协议族都有对应的地址族。例如IPV4的协议族定义为PF_INET,其地址族定义为AF_INET。它们是一一对应的,取值完全一样,所以经常互换使用。//文件:include/linux/socket.h#defineAF_UNSPEC0#defineAF_UNIX1/*Unixdomainsockets*/#defineAF_LOCAL1/*AF_UNIX的POSIXname*/#defineAF_INET2/*InternetIPProtocol*/#defineAF_INET610/*IPversion6*/#defineAF_PACKET.17/*Packetfamily*/....另外,上面的第三个参数768代表的是ETH_P_ALL,socket.htons(ETH_P_ALL)=768。下面我们展开看看这个packet类型socket的创建过程中做了什么,找到socket创建的源码。//file:net/socket.cSYSCALL_DEFINE3(socket,int,family,int,type,int,protocol){...retval=sock_create(family,type,protocol,&sock);}int__sock_create(structnet*net,intfamily,inttype,...){...pf=rcu_dereference(net_families[family]);err=pf->create(net,sock,protocol,kern);}在__sock_create中,从net_families中获取指定的协议。并调用其create方法完成创建。net_families是一个数组,除了我们常用的PF_INET(ipv4),它还支持很多协议族。如PF_UNIX、PF_INET6(ipv6)、PF_PACKET等。每个协议家族都可以在net_families数组中的特定位置找到其家族类型。在这个族类型中,成员函数create指向协议族对应的创建函数。根据上图我们可以看出,对于packet类型的socket,pf->create实际上调用了packet_create函数。让我们走进这个函数来一探究竟吧,这才是理解tcpdump工作原理的关键!//file:packet/af_packet.cstaticintpacket_create(structnet*net,structsocket*sock,intprotocol,intkern){...po=pkt_sk(sk);po->prot_hook.func=packet_rcv;//注册钩子if(proto){po->prot_hook.type=proto;register_prot_hook(sk);}}staticvoidregister_prot_hook(structsock*sk){structpacket_sock*po=pkt_sk(sk);dev_add_pack(&po->prot_hook);}设置回调函数为packet_rcvinpacket_create,然后通过register_prot_hook=>dev_add_pack完成注册。注册后,在全局协议ptype_all链表中增加了一个虚拟协议。我们来看看dev_add_pack是如何将协议注册到ptype_all的。回过头来看我们一开始看到的socket函数调用,传入的第三个参数proto是ETH_P_ALL。然后dev_add_pack实际上在最后给ptype_all添加了hook函数,代码如下。//file:net/core/dev.cvoiddev_add_pack(structpacket_type*pt){structlist_head*head=ptype_head(pt);list_add_rcu(&pt->list,head);}staticinlinestructlist_head*ptype_head(conststructpacket_type*pt){if(pt->type==htons(ETH_P_ALL))return&ptype_all;elsereturn&ptype_base[ntohs(pt->type)&PTYPE_HASH_MASK];}本文以ETH_P_ALL为例,但实际上有时还有其他情况。在其他情况下,协议可能会在ptype_base而不是ptype_all中注册。同样,ptype_base中的协议将在发送和接收期间实现。总结:tcpdump启动时,内部逻辑其实很简单,就是在ptype_all注册了一个虚拟协议。4.总结现在让我们回顾一下开头提到的问题。一、tcpdump的工作原理用户态的tcpdump命令是通过socket系统调用的,函数钩子挂载在内核源码中使用的ptype_all中。无论是在接收还是发送网络数据包的过程中,网络设备层都会遍历ptype_all中的协议并执行回调。tcpdump命令基于此基本原理工作。2、netfilter过滤的数据包是否可以被tcpdump捕获?关于这个问题,我们应该分开来看接收和发送过程。在接收网络数据包的过程中,由于tcpdump接近水面,所以能够完全捕获命中netfilter过滤规则的数据包。但是在发送的过程中,恰恰相反。网络数据包首先经过协议层。如果此时被netfilter过滤掉,工作在底层的tcpdump会在看到它之前消失。3、如何自己写一个抓包程序如果你想写一个类似于tcpdump的抓包程序,可以使用packetsocket。我用c写了一个简单的抓包demo,解析源IP和目的IP。源码地址:https://github.com/yanfeizhang/coder-kung-fu/blob/main/tests/network/test04/main.c编译一下,注意需要root权限才能运行。#gcc-omainmain.c#./main运行结果预览如下。