tcpdump是linux上常用的网络报文抓取分析工具,主要功能是抓取系统中的网络报文,根据条件过滤出需要的报文,最后将报文打印或保存到文件中。本文将对tcpdump的实现原理进行分析,由于报文解析展示和保存的逻辑比较直观,本文主要关注报文获取和过滤部分。
为了兼容各类平台和不同版本的内核,tcpdump的代码中有大量用于兼容的逻辑和分支,本文只介绍面向最新版本linux平台接口的分支逻辑。
报文获取
tcpdump的报文获取功能是通过libpcap库实现的,步骤如下:
1. open_interface=>pcap_create。创建一个pcap struct,这个结构体包含了libpcap各种操作函数指针和状态。tcpdump支持各种平台,在pcap_create时会将函数指针和状态设置成符合当前平台环境的值。不过这里设置平台环境的操作不是通过动态检测平台环境实现的,而是在编译时刻就指定好的。
2. open_interface=>pcap_activate。调用pcap.activate_op真正开始初始化。在linux下activate_op的实现函数是pcap_active_linux。在pcap_active_linux中,会调用socket(PF_PACKET, ...)来创建一个rawsocket,后面会通过这个rawsocket从协议栈获取报文。根据设备是否为“any”,分别使用SOCK_DGRAM和SOCK_RAW报文类型,前者不包含链路层数据。使用iface_get_arptype来获取目标设备的链路层协议地址类型。如果指定了监听设备,则调用bind(fd, sockaddr_ll, proto)将socket绑定到指定链路层设备上,并设置混杂模式。
3. 调用pcap_loop获取数据。在pcap_loop中,会循环调用pcap.read_op接口来读取数据并处理。linux下read_op的实现函数为pcap_read_linux或pcap_read_linux_mmap_v1/2/3系列函数。在这个函数中,调用recvfrom或recvmsg来获取网络报文,然后调用callback函数进行处理。callback函数是在初始化时指定的,如果指定了报文输出到文件,callback为dump_packet,否则为print_packet。
这里值得注意的一点是dump_packet的参数是一个pcap_dumper_t*指针(转换成了u_char*),pcap_dumper_t又是struct pcap_dumper的typedef别名。但事实上在libpcap的任何代码里,都是找不到pcap_dumper的定义的。从实现中可以看到,实际上传递给dump_packet的参数,是由输出文件的FILE*句柄强制转换而来的pcap_dumper_t*。这里利用了C的特性,在未尝试访问struct指针的引用对象时,不会去检查struct的定义是否存在,因此可以使用一个未定义的结构的指针作为参数来隐藏真正的FILE*指针的实现。这里这样做的目的是为了避免直接指定FILE*为参数,保证接口的灵活性。如果将来希望传递其他类型的参数给dump_packet等函数,就可以仍然使用这个抽象的指针类型,而不用修改接口。其实一般来说为了达到这种目的会使用void*作为参数类型,这里使用pcap_dumper_t*可能是为了改善接口参数的可读性。
报文过滤
tcpdump的数据过滤条件可以通过命令行参数指定,也可以通过-F参数指定文件输入,后者会覆盖前者。
通过read_infile或copy_argv从文件或参数中获取过滤表达式后,会调用pcap_compile来生成bpf过滤器fcode,然后使用pcap_setfilter来设置过滤器,在Linux下实现函数为pcap_setfilter_linux。
相关知识
从上文中可以看到,tcpdump的主要功能通过libpcap实现。而libpcap提供的数据获取和过滤功能则分别通过rawsocket和bpf这两项内核功能来实现。要真正理解tcpdump和libpcap的原理,还需要对这两项技术有深入的理解。
rawsocket相关知识可以参考:https://blog.csdn.net/sinat_20184565/article/details/82788387
bpf相关知识可以参考:https://qmonnet.github.io/whirl-offload/2016/09/01/dive-into-bpf/