详细讲解Linux内核角度分析tcpdump原理(1)

一、tcpdump的用途

tcpdump是Linux系统抓包工具,tcpdump基于libpcap库,根据使用者的定义对网络上的数据包进行截获,tcpdump可以将网络中传送的数据包中的”头”完全截获下来提供分析,支持针对网络层、协议、主机、网络或端口的过滤,并提供and、or、not等逻辑语句来帮助去掉无用的信息。通过tcpdump可以分析很多网络行为,比如丢包重传、详细报文、tcp分组等,总之通过tcpdunp可以为各种网络问题进行排查,可以在服务器上将捕获的数据包信息以pcap文件保存下来,通过wireshark打开,更直观地分析。

tcpdump是基于libpcap库的,数据包的过滤是基于BPF(tcpdump依附标准的bpf机器来运行,tcpdump过滤规则会被转化为一段bpf指令并加载到内核中的bpf虚拟机器上执行),使用bpf虚拟机的tcpdump完美解决了包过滤问题。总之tcpdump使用libpcap这种链路层旁路处理的形式进行包捕获,使用bpf机制实现对包的完美过滤。

好文推荐:

字节终面:CPU 是如何读写内存的?

全网最牛Linux内核分析–Intel CPU体系结构

一文让你读懂Linux五大模块内核源码,内核整体架构设计(超详细)

嵌入式前景真的好吗?那有点悬!

一文教你如何使用GDB+Qemu调试Linux内核

Linux内核必读五本书籍(强烈推荐)

全网独一无二Linux内核Makefle系统文件详解(一)(纯文字代码)

带你深度了解Linux内核架构和工作原理!

如何读懂GDB底层实现原理(从这几点入手~)

一文彻底理解Memory barrier(内存屏障)

一篇文带你搞懂,虚拟内存、内存分页、分段、段页式内存管理(超详细)

二、libpcap简单介绍

libpcap(Packet Capture Library),即数据包捕获函数库,是Unix/Linux平台下的网络数据包捕获函数库,独立于系统的用户层包捕获的API接口,为底层网络监测提供了一个可移植的框架。

利用libpcap函数库开发应用程序的基本步骤:

捕获各种数据包,例如:网络流量统计。过滤网络数据包,例如:过滤掉本地上的一些数据,类似防火墙。分析网络数据包,例如:分析网络协议,数据的采集。存储网络数据包,例如:保存捕获的数据以为将来进行分析。

libpcap库在linux上的安装过程

sudo apt-get insatll flex sudo apt-get install bison wget -c http://www.tcpdump.org/release/libpcap-1.7.4.tar.gz cd libpcap-1.7.4 ./congigure sudo make sudo make install

测试:

//demo:查找当前系统的可用网络设备 #include <stdio.h> #include <pcap.h> int main(int argc, char *argv[]) { char *dev,errbuf[1024]; dev=pcap_lookupdev(errbuf);//函数用来查找网络设备 if(dev==NULL){ printf(“%s\n”,errbuf); return 0; } printf(“Device: %s\n”, dev); return 0; }

编译:

#注意编译时加上:-lpcap gcc test.c -o test -lpcap

报错提醒:

error while loading shared libraries: libpcap.so.1: cannot open shared object file: No such file or directory

解决:

执行 locate libpcap.so.1 , 查看libpcap.so.1在系统中的路径 , 显示为 : /usr/local/lib/libpcap.so.1.2.1

以管理员权限打开编辑 /etc/ld.so.conf 文件, 末尾新一行追加 /usr/local/lib , /usr/local/lib 为 libpcap.so.1.7.4 所在目录, 保存退出

以管理员权限执行 ldconfig(如果不支持改命令用whereis ldconfig查看并设置环境变量)命令,

成功

几个重要的API

pcap_lookupdev():函数用于查找网络设备,返回可被 pcap_open_live() 函数调用的网络设备名指针。
/*errbuf:存放出错信息字符串,宏定义PCAP_ERRBUF_SIZE为错误缓冲区大小 返回值为设备名(第一个合适的网络接口的字符串指针) */ char *pcap_lookupdev(char *errbuf)
pcap_lookupnet():函数获得指定网络设备的网络号和掩码。
/*获取指定网卡的ip地址,子网掩码 device:网络设备名 netp:存放ip地址的指针 maskp:存放子网掩码的指针 errbuf:存放出错信息*/ int pcap_lookupnet(char *device,bpf_u_int32 *netp,bpf_u_int32 *maskp,char *errbuf );
pcap_open_live(): 函数用于打开网络设备,并且返回用于捕获网络数据包的数据包捕获描述字。对于此网络设备的操作都要基于此网络设备描述字。
/* device:网络接口名字 snaplen:数据包大小,最大为65535字节 promise:“1” 代表混杂模式,其它非混杂模式。什么为混杂模式 to_ms:指定需要等待的毫秒数,超过这个数值后,获取数据包的函数就会立即返回(这个函数不会阻塞,后面的抓包函数才会阻塞)。0 表示一直等待直到有数据包到来。 ebuf:存储错误信息。 返回值:pcap_t类型指针,后面的所有操作都要用这个指针 */ pcap_t *pcap_open_live(const char *device,int snaplen,int promisc,int to_ms,char *ebuf );
pcap_compile(): 函数用于将用户制定的过滤策略编译到过滤程序中。
/* p是嗅探器回话句柄 fp是一个bpf_program结构的指针,在pcap_compile()函数中被赋值。 str:指定的过滤条件 optimize参数控制结果代码的优化。 netmask参数指定本地网络的网络掩码 */ int pcap_compile(pcap_t *p, struct bpf_program *fp, char *str, int optimize, bpf_u_int32 netmask)
pcap_setfilter():函数用于设置过滤器。
/* p:pcap_open_live() 返回的 pcap_t 类型的指针 fp:pcap_compile() 的第二个参数 */ int pcap_setfilter( pcap_t * p, struct bpf_program * fp );
pcap_loop():函数 pcap_dispatch() 函数用于捕获数据包,捕获后还可以进行处理,此外 pcap_next() 和 pcap_next_ex() 两个函数也可以用来捕获数据包。pcap_close():函数用于关闭网络设备,释放资源。

【文章福利】小编推荐自己的Linux内核技术交流群:【891587639】整理了一些个人觉得比较好的学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!!!

点击报名免费内核学习直播课程:

Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈-学习视频教程-腾讯课堂​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639​ke.qq.com/course/4032547?flowToken=1042639ke.qq.com/course/4032547?flowToken=1042639

内核资料直通车:

三、tcpdump实现抓包原理剖析

使用strace追踪

strace tcpdump tcp port 80

可以看到tcpdump抓包创建的的套接字类型AF_PACKET

在libpcap库源码中也可以看到有调用socket系统调用:

tatic int pcap_can_set_rfmon_linux(pcap_t *handle) { … sock_fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL)); if (sock_fd == -1) { (void)snprintf(handle->errbuf, PCAP_ERRBUF_SIZE, “socket: %s”, pcap_strerror(errno)); return PCAP_ERROR; } … }

AF_PACKET和socket应用结合一般都是用于抓包分析,packet套接字提供的是L2的抓包能力。

socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL))

系统调用:

SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol) { …… retval = sock_create(family, type, protocol, &sock); if (retval < 0) return retval; return sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK)); }

socket创建函数:

int sock_create(int family, int type, int protocol, struct socket **res) { return __sock_create(current->nsproxy->net_ns, family, type, protocol, res, 0); }

调用:__sock_create

sock_create 函数主要就是创建了socket . 同时根据之前PF_PACKET 模块注册到全局变量net_families 。 找到af_packet.c 中初始化的 static const struct net_proto_family packet_family_ops。而sock_create 函数中 err = pf->create(net, sock, protocol, kern); 最终就会调用 packet_family_ops 里的packet_create

int __sock_create(struct net *net, int family, int type, int protocol, struct socket **res, int kern) { int err; struct socket *sock; const struct net_proto_family *pf; …… sock = sock_alloc();//分配socket结构空间 if (!sock) { net_warn_ratelimited(“socket: no more sockets\n”); return -ENFILE; /* Not exactly a match, but its the closest posix thing */ } sock->type = type;//记录socket类型 #ifdef CONFIG_MODULES if (rcu_access_pointer(net_families[family]) == NULL) request_module(“net-pf-%d”, family); #endif rcu_read_lock(); pf = rcu_dereference(net_families[family]);//根据family协议簇找到注册的(PF_PACKET)协议族操作表 err = -EAFNOSUPPORT; if (!pf) goto out_release; if (!try_module_get(pf->owner)) goto out_release; rcu_read_unlock(); err = pf->create(net, sock, protocol, kern);//执行该协议族(PF_PACKET)的创建函数 …… }

Linux内核中定义了net_proto_family结构体,用来指明不同的协议族对应的socket创建函数,family字段是协议族的类型,create是创建socket的函数,如下是PF_PACKET对应结构体。

static const struct net_proto_family packet_family_ops = { .family = PF_PACKET, .create = packet_create, .owner = THIS_MODULE, };

找到AF_PACKET协议族对应的create函数:可以看到po->prot_hook.func = packet_rcv;po->prot_hook其实packet_type,packet_type结构体: packet_type 结构体第一个type 很重要,对应链路层中2个字节的以太网类型。而dev.c 链路层抓取的包上报给对应模块,就是根据抓取的链路层类型,然后给对应的模块处理,例如socket(PF_PACKET, SOCK_DGRAM, htons(ETH_P_ALL)); ETH_P_ALL表示所有的底层包都会给到PF_PACKET 模块的处理函数,这里处理函数就是packet_rcv 函数。

设置了回调函数:packet_rcv,并通过register_prot_hook(sk)完成了注册,其中注册过程将再下面分析

static int packet_create(struct net *net, struct socket *sock, int protocol, int kern) { struct sock *sk; struct packet_sock *po; __be16 proto = (__force __be16)protocol; /* weird, but documented */ int err; …… po = pkt_sk(sk); sk->sk_family = PF_PACKET;//设置sk协议族为PF_PACKET po->num = proto; //数据包的类型ETH_P_ALL po->xmit = dev_queue_xmit; err = packet_alloc_pending(po); if (err) goto out2; packet_cached_dev_reset(po); sk->sk_destruct = packet_sock_destruct; sk_refcnt_debug_inc(sk); /* * Attach a protocol block */ spin_lock_init(&po->bind_lock); mutex_init(&po->pg_vec_lock); ….. po->rollover = NULL; po->prot_hook.func = packet_rcv;//设置回调函数 if (sock->type == SOCK_PACKET) po->prot_hook.func = packet_rcv_spkt; po->prot_hook.af_packet_priv = sk; if (proto) { po->prot_hook.type = proto; register_prot_hook(sk);//将这个socket挂载到ptype_all连接串列上 } …… } //packet_sock结构体 struct packet_sock { /* struct sock has to be the first member of packet_sock */ struct sock sk; …… struct net_device __rcu *cached_dev; int (*xmit)(struct sk_buff *skb); struct packet_type prot_hook ____cacheline_aligned_in_smp;//packet_create函数中通过该字段进行下一步的设置:po->prot_hook }; /*po->prot_hook其实packet_type,packet_type结构体: 数据包完成链路层的处理后,需要提交给协议栈上层继续处理,每个packet_type结构就是数据包的一个可能去向 packet_type 结构体第一个type 很重要,对应链路层中2个字节的以太网类型。而dev.c 链路层抓取的包上报给对应模块,就是根据抓取的链路层类型,然后给对应的模块处理,例如socket(PF_PACKET, SOCK_DGRAM, htons(ETH_P_ALL)); ETH_P_ALL表示所有的底层包都会给到PF_PACKET 模块的处理函数,这里处理函数就是packet_rcv 函数。 */ struct packet_type { __be16 type; /* type指定了协议的标识符,标记了packet_type收取什么类型的数据包,处理程序func会使用该标识符 ,保存了三层协议类型,ETH_P_IP、ETH_P_ARP等等*/ struct net_device *dev; /* NULL指针表示该处理程序对系统中所有网络设备都有效 */ /* func:packet_create函数通过该字段设置的回调函数:po->prot_hook.func = packet_rcv; func是该结构的主要成员,它是一个指向网络层函数的指针,ip层处理时挂载的是ip_rcv */ int (*func) (struct sk_buff *, struct net_device *, struct packet_type *, struct net_device *); bool (*id_match)(struct packet_type *ptype, struct sock *sk); void *af_packet_priv; struct list_head list; };

展开注册函数register_prot_hook(sk)

static void register_prot_hook(struct sock *sk) { struct packet_sock *po = pkt_sk(sk); if (!po->running) { if (po->fanout) __fanout_link(sk, po); else dev_add_pack(&po->prot_hook);//将pacekt_type放到ptype_all链表上。 sock_hold(sk); po->running = 1; } } //ptype_all链表: struct list_head ptype_all __read_mostly;//全局变量

展开dev_add_pack

//将pacekt_type放到ptype_all链表上。 void dev_add_pack(struct packet_type *pt) { struct list_head *head = ptype_head(pt);//获取ptype_all链表 spin_lock(&ptype_lock); list_add_rcu(&pt->list, head);//将po->prot_hook挂载到ptype_all链表 spin_unlock(&ptype_lock); } //获取ptype_all链表 static inline struct list_head *ptype_head(const struct packet_type *pt) { if (pt->type == htons(ETH_P_ALL))//type为ETH_P_ALL时,则挂在ptype_all上面 return pt->dev ? &pt->dev->ptype_all : &ptype_all; else return pt->dev ? &pt->dev->ptype_specific : &ptype_base[ntohs(pt->type) & PTYPE_HASH_MASK];////否则,挂在ptype_base[type&15]上面 }

综上:tcpdump在刚开始工作时创建了PF_PACKET套接字,并在全局的ptype_all中挂载了该套接字的pt(packet_type *pt),其中pt的字段func设置了相应的回调函数packet_rcv(后面将分析该函数),到此tcpdump抓包的socket(AF_PACKET)创建完成,相应的准备工作完成。

网络收包时tcpdump进行抓包

函数调用关系

调用关系:netif_receive_skb–>netif_receive_skb–>netif_receive_skb_internal->__netif_receive_skb–>__netif_receive_skb_core

核心函数__netif_receive_skb_core

static int __netif_receive_skb_core(struct sk_buff *skb, bool pfmemalloc) { …… //遍历ptype_all,cpdump在创建socket时已将其packet_type挂载到了遍历ptype_all list_for_each_entry_rcu(ptype, &ptype_all, list) { if (pt_prev) ret = deliver_skb(skb, pt_prev, orig_dev);//deliver函数回调用paket_type.func(),也就是packet_rcv pt_prev = ptype; } …… }

__netif_receive_skb_core函数在遍历ptype_all时,同时也执行了deliver_skb(skb, pt_prev, orig_dev);deliver函数调用了paket_type.func(),也就是packet_rcv ,如下源码所示:

static inline int deliver_skb(struct sk_buff *skb, struct packet_type *pt_prev, struct net_device *orig_dev) { if (unlikely(skb_orphan_frags_rx(skb, GFP_ATOMIC))) return -ENOMEM; refcount_inc(&skb->users); return pt_prev->func(skb, skb->dev, pt_prev, orig_dev);//调用tcpdump挂载的packet_rcv 函数 }

下面将展开packet_rcv函数进行分析;函数接收到链路层网口的数据包后,会根据应用层设置的bpf过滤数据包,符合要求的最终会加到struct sock sk 的接收缓存中。使用BPF过滤过程将在后面进行分析。

static int packet_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev) { …… if (sk->sk_type != SOCK_DGRAM)// 当 SOCK_DGRAM类型的时候,会截取掉链路层的数据包,从而返回给应用层的数据包是不包含链路层数据的 skb_push(skb, skb->data – skb_mac_header(skb)); else if (skb->pkt_type == PACKET_OUTGOING) { /* Special case: outgoing packets have ll header at head */ skb_pull(skb, skb_network_offset(skb)); } } …… //最后将底层网口符合应用层的数据复制到接收缓存队列中 res = run_filter(skb, sk, snaplen); //将用户指定的过滤条件使用BPF进行过滤 …… spin_lock(&sk->sk_receive_queue.lock); po->stats.stats1.tp_packets++; sock_skb_set_dropcount(sk, skb); __skb_queue_tail(&sk->sk_receive_queue, skb);//将skb放到当前的接收队列中 spin_unlock(&sk->sk_receive_queue.lock); sk->sk_data_ready(sk); return 0; …… }

综上一旦关联上链路层抓到的包就会copy一份给上层接口(即PF_PACKET 注册的回调函数packet_rev). 而回调函数会根据应用层设置的bpf过滤数据包,最终放入接收缓存的数据包肯定是符合应用层想截取的数据。因此最后一步recvfrom 也就是从接收缓存的数据包copy给应用层,如下源码:

static int packet_recvmsg(struct socket *sock, struct msghdr *msg, size_t len, int flags) { …… skb = skb_recv_datagram(sk, flags, flags & MSG_DONTWAIT, &err);//从接收缓存中接收数据 …… err = skb_copy_datagram_msg(skb, 0, msg, copied);//将最终的数据copy到用户空间 …… }

到这,网络接收数据包时的抓包过程就结束了

网络发包时tcpdump进行抓包

Linux协议栈中提供的报文发送函数有两个,一个是链路层提供给网络层的发包函数dev_queue_xmit(),另一个就说软中断发吧包函数之间调用的sch_direct_xmit(),这两个函数最终都会调用dev_hard_start_xmit()

struct sk_buff *dev_hard_start_xmit(struct sk_buff *first, struct net_device *dev, struct netdev_queue *txq, int *ret) { …… while (skb) { struct sk_buff *next = skb->next; skb->next = NULL; rc = xmit_one(skb, dev, txq, next != NULL);//调用xmit_one来发送一个到多个数据包 …… }

xmit_one():发送一个到多个数据包

static int xmit_one(struct sk_buff *skb, struct net_device *dev, struct netdev_queue *txq, bool more) { …… if (!list_empty(&ptype_all) || !list_empty(&dev->ptype_all)) dev_queue_xmit_nit(skb, dev);//通过调用dev_queue_xmit这个网络设备接口层函数发送给driver, …… }

dev_queue_xmit_nit():将数据包发送给driver

void dev_queue_xmit_nit(struct sk_buff *skb, struct net_device *dev) { …… list_for_each_entry_rcu(ptype, ptype_list, list) { /* Never send packets back to the socket * they originated from – MvS ([email]miquels@drinkel.ow.org[/email]) */ if (skb_loop_sk(ptype, skb)) continue; if (pt_prev) { deliver_skb(skb2, pt_prev, skb->dev); pt_prev = ptype; continue; } …… }

在遍历ptype_all时,同时也执行了deliver_skb(skb, pt_prev, orig_dev);deliver函数调用了paket_type.func(),也就是packet_rcv ,如下源码所示:

static inline int deliver_skb(struct sk_buff *skb, struct packet_type *pt_prev, struct net_device *orig_dev) { if (unlikely(skb_orphan_frags_rx(skb, GFP_ATOMIC))) return -ENOMEM; refcount_inc(&skb->users); return pt_prev->func(skb, skb->dev, pt_prev, orig_dev);//调用tcpdump挂载的packet_rcv 函数 }

下面的流程就和网络收包时tcpdump进行抓包一样了(packet_rcv函数中会将用户设置的过滤条件,通过BPF进行过滤,并将过滤的数据包添加到接收队列中,应用层在libpcap库中调用recvfrom 。 PF_PACKET 协议簇模块调用packet_recvmsg 将接收队列中的数据copy应用层)

tcpdump进行抓包的内核流程梳理

应用层通过libpcap库:调用系统调用创建socket,sock_fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));tcpdump在socket创建过程中创建packet_type(struct packet_type),并挂载到全局的ptype_all链表上。(同时在packet_type设置回调函数packet_rcv网络收包/发包时,会在各自的处理函数(收包时:__netif_receive_skb_core,发包时:dev_queue_xmit_nit)中遍历ptype_all链表,并同时执行其回调函数,这里tcpdump的注册的回调函数就是packet_rcvpacket_rcv函数中会将用户设置的过滤条件,通过BPF进行过滤,并将过滤的数据包添加到接收队列中应用层调用recvfrom 。 PF_PACKET 协议簇模块调用packet_recvmsg 将接收队列中的数据copy应用层,到此将数据包捕获到。

总结

本文主要从tcpdump抓包时调用的libpcap库开始梳理,从socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL))进入系统调用,再从内核角度对Tcpdump在收包和发包的流程分析了一遍,其实还有一个重点:BPF的过滤过程,如下源码所示:run_filter(skb, sk, snaplen),下次文章将对BPF的过滤过程进行一些分析。

static int packet_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev) { …… res = run_filter(skb, sk, snaplen); //将用户指定的过滤条件使用BPF进行过滤 …… __skb_queue_tail(&sk->sk_receive_queue, skb);//将skb放到当前的接收队列中 …… }

– – 内核技术中文网 – 构建全国最权威的内核技术交流分享论坛 (0voice.com)

转载地址:详细讲解Linux内核角度分析tcpdump原理(1) – 圈点 – 内核技术中文网 – 构建全国最权威的内核技术交流分享论坛 (0voice.com)

© 版权声明
THE END
喜欢就支持一下吧
点赞12 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片