Linux中的抓包与截包(一)

前言

俺作为一名搞网络的民工,总有一颗瞎折腾的心。

近日,被要求调研一下OVS这个宣称运行在内核态的数据包交换程序。既然是数据包交换程序,必然涉及到数据包截取层面,关于OVS的数据截取策略,俺百思不得其解。近日,俺在追代码追出些眉目,特此分享,顺便,总结下的Linux内部数据包抓取的方法及思路。

几种模型

根据俺的经验,Linux内部的数据包抓取的模式大概可以用以下几种方法或软件予以概括。

  • ovs/bridge
  • libpcap
  • openvpn
  • netfilter hook
  • proxychains/proxifler

ovs/bridge必须要求加载一个kernel module,以驱动的形式进入内核,从内核的层面截取数据包。libpcap主要使用了kernel的socket接口,使得数据包能直接复制到用户空间。openvpn使用了tun/tap驱动,直接创建一个虚拟的网络设备,并复制该网络设备的数据包直接到应用层。netfilter hook则使用netfilter框架,在内核的几个hook点注入抓包代码。

OVS/Bridge

Open vSwitch is a production quality, multilayer virtual switch licensed under the open source Apache 2.0 license. 
OVS简单来说,就是交换逻辑跑在内核态,控制逻辑跑在用户态,中间用netlink进行通信的交换机程序。 换句话说,OVS其实就是一种具有灵活转发逻辑的linux bridge设备。

OVS目前已经在3.3版本中merge进入内核。以下的所有分析基于内核4.3版本,具体源码可以通过LXR查看。

OVS如何维护所谓的flow等等逻辑就不再细说了,软件工程那一套直接往上堆就可以了。不过可能必须注意的是内核里面加锁的逻辑,否则kernel panic可不是闹着玩的。

OVS这里截取数据包的逻辑可以直接定位到net/openvswitch/vport-netdev.c文件中的ovs_netdev_link函数。该函数会在应用程代码执行ovs-vsctl add-port br0 eth0后经过netlink被调用。该函数中里面关键的一句话为112行 
err = netdev_rx_handler_register(vport->dev, netdev_frame_hook,113 vport);
继续追该函数进入/net/core/dev.c的3718行,该函数的注释为
Register a receive handler for a device. This handler will then be called from __netif_receive_skb. A negative errno code is returned on a failure. For a general description of rx_handler, see enum rx_handler_result.
大概意思为,该函数为该设备注册一个数据包接受的处理函数rx_handler,该处理函数在__netif_receive_skb中被调用, __netif_receive_skb在根据rx_handler的返回值 rx_handler_result决定后续处理。

继续跟一下rx_handler_result看看如何,定位到include/linux/netdevice.h中看到这个枚举类型有如下多个值,其中RX_HANDLER_CONSUMED最为重要。
If the rx_handler consumed to skb in some way, it should return RX_HANDLER_CONSUMED. This is appropriate when the rx_handler arranged for the skb to be delivered in some other ways. 
这个CONSUMED顾名思义,就是这个包,已经被我处理了,接下来就不用走TCP/IP协议栈了。而OVS的代码中,也使用了这个值。

继续跟到OVS所用来钩住内核的rx_handler函数
static rx_handler_result_t netdev_frame_hook(struct sk_buff **pskb)
{
struct sk_buff *skb = *pskb;
if (unlikely(skb->pkt_type == PACKET_LOOPBACK))
return RX_HANDLER_PASS;
netdev_port_receive(skb);
return RX_HANDLER_CONSUMED;
}
喏,net_port_receive后,返回了RX_HANDLER_CONSUMED,告诉了内核,OK,这个包我OVS处理了,别往上递交了。net_port_receive函数是OVS内部的逻辑了,即什么流表那一堆东西,这些软件工程的东西就不再分析了。

那么OVS的分析到这里为止,OVS这种模式使用了内核模块,并且使用了net_devicenetdev_rx_handler_register函数作为截取数据包的手段,值得学习。

同样的,大家可以分析linux bridge的源码,采用了同样的手段来截取数据包。


libpcap

libpcap is a system-independent interface for user-level packet capture. libpcap provides a portable framework for low-level network monitoring. 
似乎上面的引用没啥用,好吧,tcpdump和wireshark底层都是这货在抓包。 这货其实还蛮有用的吧?

以下的代码分析,基于libpcap1.7.4。打开网络设备逻辑直接定位到Pcap-linux.c文件的activate_new函数,该函数的注释写的很明白呀。
/*
 * Try to open a packet socket using the new kernel PF_PACKET interface.
 * Returns 1 on success, 0 on an error that means the new interface isn't
 * present (so the old SOCK_PACKET interface should be tried), and a
 * PCAP_ERROR_ value on an error that means that the old mechanism won't
 * work either (so it shouldn't be tried).
 */
大概意思是:我们用了新版kernel的PF_PACKET接口来实现这个东西,但是,考虑到兼容性,我们会在老的版本SOCK_PACKET尝试下是否可以使用。这个SOCK_PACKET是个历史遗留问题了,得追溯到2.0内核去了,因此,就不做多考虑了。

该函数中首先处理一大堆历史遗留问题。定位到3162行,直接用SOCKET_RAW打开socket接口。

/*
* Open a socket with protocol family packet. If the
* "any" device was specified, we open a SOCK_DGRAM
* socket for the cooked interface, otherwise we first
* try a SOCK_RAW socket for the raw interface.
*/
sock_fd = is_any_device ?
socket(PF_PACKET, SOCK_DGRAM, htons(ETH_P_ALL)) :
socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL)); 
注意到,处理了下any这种设备类型,使用SOCK_DGRAM来打开接口。

关于读取数据包的逻辑定位到1544行pcap_read_packet
注释依旧明白
/*
 *  Read a packet from the socket calling the handler provided by
 *  the user. Returns the number of packets received or -1 if an
 *  error occured.
 */
实在不想分析了,一大堆的宏定义,简单地说吧,直接从pcap_t里面读取数据包,然后做过滤,最终调用回调函数即可。

实际上比较简单,libpcap为了兼容性写了太多的代码,导致看起来蛮麻烦的。
简单来说,内核接口PF_PACKET协议族的SOCK_RAW实现。

参考文献:

OVS部分

http://laoar.net/blog/2015/04/27/open-vswitch/

Libpap部分