Inside the Linux Packet Filter

来源:互联网 发布:js 仪表盘 编辑:程序博客网 时间:2024/05/29 19:31

原文地址: http://www.linuxjournal.com/article/4852

网络geek猛也许记得上一篇文章,“Linux Socket Filter: Sniffing Bytes over the Network”,发布在LJ的2001 June,它是关于在kernel中使用packet filter。那篇文章,作者大致描述了packet filter的作用;这次,我将深入描述在filter在kernel中的工作机制,以及分享一些Linux packet处理的一些看法。

Last Article's Points

上一篇文章,关于kernel处理packet的过程有一些争议。因此有必要简要的回忆一下他们当中的重要内容:

1. 接收packet是网卡驱动层做的第一项工作,更准确的说,在中断函数中处理。中断处理函数查询接收到的packet的协议类型,并适当的压入队列,留着稍后处理。

2. 在接收和协议处理过程中,如果主机阻塞,那么packet可能被丢弃。而且当packet向上传到用户空间时,网络的底层信息将会丢弃。

3. 当到达socket层,在用户空间读取数据之前,kernel会检查是否有一个打开的socket来接收数据。如果没有,那么packet将会被丢弃。

4. 然后kernel会实现一个通用的协议,调用PF_PACKET,它允许你创建一个直接从网卡驱动读取数据的socket。所以它会跳过其他的协议处理,并且可以接受任何packet。

5. Ethnet card只是把目的地址是自己的packet传到kernel,丢弃其他目的地址的packet。然而可以把Ethnet card配置成捕获所有经过它的packet的形式(promiscuous模式)。

6. 最后,你可以将一个filter和socket绑定,这样只有符合你的filter规则的packet将会被接收,上传到socket。结合PF_PACKET socket,这种机制允许你有效的嗅探LAN中的选中的packet。

即使我们用PF_PACKET创建我们的嗅探器,Linux socket filter(LSF)的功能不局限于此。事实上,filter可以用在普通的TCP/UDP socket上来过滤不想要的packet。当然,这种用法不常用。

在下文,有时会用socket和sock structure,就本文而言,二者指的是一个东西。并且后者是前者在kernel的对应。事实上,kernel及记录socket structure和sock structure,但是他们的区别于本文无关。

另一个反复出现的结构体是sk_buff(为socket buffer简称),表示kernel中的一个packet。这个结构体是这样被安排的:用一个相对快捷的方式添加/删除剪裁packet信息-没有数据需要被copy,只是调整指针。

在深入之前,有必要明确一下。尽管名字相似,Linux socket filtration和在2.3版本引进的Netfilter frame有完全不同的用途。即使Netfilter允许你把packet带到用户空间,并且自己填充他们,它的重点是处理NAT, packet mangling, connection tracking,安全目的的packet filter等。如果你仅仅需要嗅探并且根据一定的规则过滤,更直接的工具是LSF。

现在我们开始跟随一个packet从它进入一台机器知道被送到用户空间所经过的旅程。我们搜先考虑一个普通的socket(plain socket)的情况.我们在链路层的分析是基于Ethernet,因为它是最广泛使用的LAN技术。其他的链路层技术没有太大的区别。

Ethernet Card and Lower-Kernel Reception

如同前文提到的,Ethernet card一个拥有各自的链路层地址的硬件设备,并且一直监听它接口上的packet。当它监视到匹配它的MAC地址或广播(例如FF:FF:FF:FF:FF:FF)的packet,他就将packet读进他的memory。

基于接收到的packet,network card产生一个中断。处理网卡产生的中断的中断服务程序处理是网卡驱动,当它执行时中断被禁止,并且进行一下操作:

1.申请一个定义在include/linux/skbuff.h中的sk_buff structure,表示kernel角度的packet。
2. 将card buffer中的数据取到sk_buff中,可能会用到DMA。
3. 调用netif_rx(),通用网络接收函数
4. 当netif_rx()返回时,重新enable中断,接收中断服务函数。

netif_rx()函数为kernel做好了下一次接收的准备,它把sk_buff压入当前cpu的接收数据queue,并且通过__cpu_raise_softirq()产生NET_RX(下文介绍软中断)中断。在这一步值得注意的两点:首先,如果queue满了,那么该packet将会永久丢弃。其次,每个CPU都有一个queue,结合新的延迟处理模型(用softirq替代底部中断),它允许在SMP机器中接收packet。

如果你想看看现实的Ethernet驱动是如何工作的,你可以参考位与drivers/net/8390.c的NE 2000card PCI驱动。中断服务程序调用ei_interrupt(),ei_receive(),相应的,作出如下动作:

1. 通过dev_alloc_skb()申请一个sk_buff structure。
2. 从card buffer中读取数据(ei_block_input)并且相应的设置skb->protocol.
3. 调用netif_rx()
4. 重复接收最多是个连续的packet。

一个相对复杂的例子是3COM驱动,位于3c59x.c它使用DMA将packet从card memory拷贝到sk_buff。

Network Core Processing

让我们更进一步看一下netif_rx()函数。正如之前提到的,这个函数负责从网络驱动接收packet并将它压入queue以便于上层协议处理。他看起来像是一个汇集点-接收不同network card驱动收集的packet,并将它们提供给上层协议处理。

因为这个函数运行在禁止其他中断的中断上下文中,他必须快而短。他不能进行长时间的检查或者其他复杂的任务,因为当netif_rx()运行时,系统可能丢包。所以这个函数只是简单的从一个叫softnet_data数组queue中选出一个queue,softnet_data的index是基于当前运行的CPU。然后检查queue的状态,识别5中可能的阻塞级别:NET_RX_SUCCESS (no congestion), NET_RX_CN_LOW, NET_RX_CN_MOD, NET_RX_CN_HIGH(相应的low, moderate and high congestion),或者NET_RX_DROP (由于严重阻塞,丢弃packet)。

假如进入了严重阻塞,netif_rx()包含了一个允许queue回到非阻塞状态的调节策略,用于避免由于kernel overload导致的服务混乱。在众多好处中的一个,它可以降低DOS攻击的可能性。

在普通情况下,packet最终如queue(__skb_queue_tail()),并且调用__cpu_raise_softirq(cpuid, NET_IF_SOFTIRQ)。后者会schedule softirq执行。

netif_rx()函数返回值表明当前阻塞level。这时,中断上下问结束了,packet已经可以被上层协议处理。处理会被延迟一段时间,当中断重新被enabled,以及executing时间不是很紧要时。延迟处理机制从2.2(基于底部中断)到2.4(基于软中断)发生了彻底地变革了。

softirqs vs. Bottom Halves

原文地址: http://www.linuxjournal.com/article/4852

网络geek猛也许记得上一篇文章,“Linux Socket Filter: Sniffing Bytes over the Network”,发布在LJ的2001 June,它是关于在kernel中使用packet filter。那篇文章,作者大致描述了packet filter的作用;这次,我将深入描述在filter在kernel中的工作机制,以及分享一些Linux packet处理的一些看法。

Last Article's Points

上一篇文章,关于kernel处理packet的过程有一些争议。因此有必要简要的回忆一下他们当中的重要内容:

1. 接收packet是网卡驱动层做的第一项工作,更准确的说,在中断函数中处理。中断处理函数查询接收到的packet的协议类型,并适当的压入队列,留着稍后处理。

2. 在接收和协议处理过程中,如果主机阻塞,那么packet可能被丢弃。而且当packet向上传到用户空间时,网络的底层信息将会丢弃。

3. 当到达socket层,在用户空间读取数据之前,kernel会检查是否有一个打开的socket来接收数据。如果没有,那么packet将会被丢弃。

4. 然后kernel会实现一个通用的协议,调用PF_PACKET,它允许你创建一个直接从网卡驱动读取数据的socket。所以它会跳过其他的协议处理,并且可以接受任何packet。

5. Ethnet card只是把目的地址是自己的packet传到kernel,丢弃其他目的地址的packet。然而可以把Ethnet card配置成捕获所有经过它的packet的形式(promiscuous模式)。

6. 最后,你可以将一个filter和socket绑定,这样只有符合你的filter规则的packet将会被接收,上传到socket。结合PF_PACKET socket,这种机制允许你有效的嗅探LAN中的选中的packet。

即使我们用PF_PACKET创建我们的嗅探器,Linux socket filter(LSF)的功能不局限于此。事实上,filter可以用在普通的TCP/UDP socket上来过滤不想要的packet。当然,这种用法不常用。

在下文,有时会用socket和sock structure,就本文而言,二者指的是一个东西。并且后者是前者在kernel的对应。事实上,kernel及记录socket structure和sock structure,但是他们的区别于本文无关。

另一个反复出现的结构体是sk_buff(为socket buffer简称),表示kernel中的一个packet。这个结构体是这样被安排的:用一个相对快捷的方式添加/删除剪裁packet信息-没有数据需要被copy,只是调整指针。

在深入之前,有必要明确一下。尽管名字相似,Linux socket filtration和在2.3版本引进的Netfilter frame有完全不同的用途。即使Netfilter允许你把packet带到用户空间,并且自己填充他们,它的重点是处理NAT, packet mangling, connection tracking,安全目的的packet filter等。如果你仅仅需要嗅探并且根据一定的规则过滤,更直接的工具是LSF。

现在我们开始跟随一个packet从它进入一台机器知道被送到用户空间所经过的旅程。我们搜先考虑一个普通的socket(plain socket)的情况.我们在链路层的分析是基于Ethernet,因为它是最广泛使用的LAN技术。其他的链路层技术没有太大的区别。

Ethernet Card and Lower-Kernel Reception

如同前文提到的,Ethernet card一个拥有各自的链路层地址的硬件设备,并且一直监听它接口上的packet。当它监视到匹配它的MAC地址或广播(例如FF:FF:FF:FF:FF:FF)的packet,他就将packet读进他的memory。

基于接收到的packet,network card产生一个中断。处理网卡产生的中断的中断服务程序处理是网卡驱动,当它执行时中断被禁止,并且进行一下操作:

1.申请一个定义在include/linux/skbuff.h中的sk_buff structure,表示kernel角度的packet。
2. 将card buffer中的数据取到sk_buff中,可能会用到DMA。
3. 调用netif_rx(),通用网络接收函数
4. 当netif_rx()返回时,重新enable中断,接收中断服务函数。

netif_rx()函数为kernel做好了下一次接收的准备,它把sk_buff压入当前cpu的接收数据queue,并且通过__cpu_raise_softirq()产生NET_RX(下文介绍软中断)中断。在这一步值得注意的两点:首先,如果queue满了,那么该packet将会永久丢弃。其次,每个CPU都有一个queue,结合新的延迟处理模型(用softirq替代底部中断),它允许在SMP机器中接收packet。

如果你想看看现实的Ethernet驱动是如何工作的,你可以参考位与drivers/net/8390.c的NE 2000card PCI驱动。中断服务程序调用ei_interrupt(),ei_receive(),相应的,作出如下动作:

1. 通过dev_alloc_skb()申请一个sk_buff structure。
2. 从card buffer中读取数据(ei_block_input)并且相应的设置skb->protocol.
3. 调用netif_rx()
4. 重复接收最多是个连续的packet。

一个相对复杂的例子是3COM驱动,位于3c59x.c它使用DMA将packet从card memory拷贝到sk_buff。

Network Core Processing

让我们更进一步看一下netif_rx()函数。正如之前提到的,这个函数负责从网络驱动接收packet并将它压入queue以便于上层协议处理。他看起来像是一个汇集点-接收不同network card驱动收集的packet,并将它们提供给上层协议处理。

因为这个函数运行在禁止其他中断的中断上下文中,他必须快而短。他不能进行长时间的检查或者其他复杂的任务,因为当netif_rx()运行时,系统可能丢包。所以这个函数只是简单的从一个叫softnet_data数组queue中选出一个queue,softnet_data的index是基于当前运行的CPU。然后检查queue的状态,识别5中可能的阻塞级别:NET_RX_SUCCESS (no congestion), NET_RX_CN_LOW, NET_RX_CN_MOD, NET_RX_CN_HIGH(相应的low, moderate and high congestion),或者NET_RX_DROP (由于严重阻塞,丢弃packet)。

假如进入了严重阻塞,netif_rx()包含了一个允许queue回到非阻塞状态的调节策略,用于避免由于kernel overload导致的服务混乱。在众多好处中的一个,它可以降低DOS攻击的可能性。

在普通情况下,packet最终如queue(__skb_queue_tail()),并且调用__cpu_raise_softirq(cpuid, NET_IF_SOFTIRQ)。后者会schedule softirq执行。

netif_rx()函数返回值表明当前阻塞level。这时,中断上下问结束了,packet已经可以被上层协议处理。处理会被延迟一段时间,当中断重新被enabled,以及executing时间不是很紧要时。延迟处理机制从2.2(基于底部中断)到2.4(基于软中断)发生了彻底地变革了。

softirqs vs. Bottom Halves

详细解释底部中断(BHs)和他们的解决方案不再本文范围。但是有些东西值得简要回顾一下。

首先,他们的设计初衷都是在进程上下文中kernel应该尽少的计算。所以长时间的操作应该独立于中断,对应的驱动应该标记一个适当的BH,而不是进行实际复杂的工作。然后在application task之前,kernel应该检查BH mask,决定哪些BH被标记,然后在执行他们。

BH非常出色,但是有一个缺点:由于他们的结构,他们在CPU之间是串行执行的。也就是说,同样的BH不能同时在两个CPU上执行。很明显,这组织了SMP机器的并行化,并且有严重的性能影响。softirq代表2.4时代的BHs的进化,和同tasklets,属于kernel软中断家族。当被请求时,代码块会被执行,没有严格是响应时间保证。

与BHs的主要区别是同一个softirq会在多个CPU上同事执行。如果要求串行化,那么必须调使用kernel spinlocks。

softirq's Internals

softirq的处理核心在do_softirq()函数,位与kernel/softirq.c。这个函数检查为掩码,如果对应的掩码被设置了,那就调用对应的处理函数。对于我们现在感兴趣的NET_RX_SOFTIRQ,对应的函数是net_x_action(),位与net/core/dev.c。do_softirq()函数可能在内核的三个地方被调用:do_IRQ(),位与kernel/irq.c,它是和一般的中断入口函数;系统调用结束的位置,kernel/entry.S;和schedule(),位与kernel/sched.c,它是进程调度的主要函数。

换句话说,softirq的执行要么发生在一个硬件中断已经执行完成,一个包含系统调用的application进程从内核返回之前,或者一个新的进程被调度执行之前。用这种方法,softirq会得到足够的执行机会并且他们不会等待太长时间。

触发机制和旧风格的HBs一样。

The NET_RX softirq

我们已经看到packet从network接口到达后被queue以待处理。然后我们考虑到通过net_rx_action()是如何处理这个过程的。现在是时候看看这个函数到底干了些什么。通常的,这个操作非常简单:他从当前CPU中取出queue中的第一个packet,然后遍历两个package handler list,调用相应的处理函数。

值得花一些时间在这些list是如何建立的。这两个list叫ptype_all和ptype_base,并且相应的包含packet protocol handler。protocol handler要么在kernel启动时候注册,或者一个特殊类型的socket被创建,声明他们使用哪种协议。关联的函数是dev_add_pack()位与net/core/dev.c,这个函数用于增加一个packet类型结构(include/linux/netdevice.h)。其中包含一个指向函数的指针-当收到该类型的packet时,调用这个函数。关于注册,每一个handler的结构体要么放在ptype_all list(对应于ETH_P_ALL类型)中,要么被hash到ptype_base list中(对应于ETH_P_*)。

所以NET_RX softirq的工作就是一次调用已注册的每一个protocol handler来处理相应的packet protocol类型。通用handlers(ptype_all protocols)首先被调用,不论packet的protocol类型;然后是特定的handlers。如同我们将要看到的,PF_PACKET被注册在这两个list中的一个上,具体哪个list取决于application使用的socket类型。另一方面,普通的IP handler被注册在第二个list中,通过ETH_P_IP作为key来hash。

如果queue包含了太多的packet,net_rx_action()会循环执行直到到达处理packet的上限(netdev_max_backlog)或者已经花费了足够多的时间(the time limit is 1 jiffy, i.e., 10ms on most kernels)。如果net_rx_action()跳出循环并且留下一个非空的queu,NET_RX_SOFTIRQ会再次被enabled来允许稍后继续处理。

The IP Packet Handler

IP protocol接收函数,ip_rcv()(net/ipv4/ip_input.c),在kernel启动的时候由packet类型structure指定的(ip_init(), net/ipv4/ip_output.c)。很明显,注册的协议类型是ETH_P_IP。

所以, ip_rcv()在处理softirq时由net_rx_action()中调用,当一个ETH_P_IP的packet dequeued时。这个函数执行IP packet的所有的初期化检查,主要包含它的完整性(IP checksum, IP header fields 和minimum significant packet length)。如果packet看起来正确,ip_rcv_finishe()将会被调用。顺便说一句,这个函数的调用经过了Netfilter prerouting控制点,主要是通过NF_HOOK实现的。

ip_rcv_finish(),仍然在Ip_input.c,主要处理IP的routing功能。它检查packet是否应该forward到另一台machine,或者该packet的目的是不是自己。在前一种情况下,执行routing,packet将会通过适当的interface发送出去;否则,将会本地处理。这一切通过ip_route_input()实现,在ip_rcv_finish()开始处被调用,用过通过在skb->dst>input中设置适当的函数指针来决定下一步该如何处理。对于属于本地的packet,这个函数指针对应的是ip_local_deliver()。ip_rcv_finish()通过调用skb->dst->input()结束。

这时,packet将会向上层协议传输。控制交给了ip_local_deliver();这个函数仅仅是处理IPfragment的聚合(对于ip 数据包被fragmented的情况)然后是ip_local_deliver_finish()函数。在调用它之前,另一个Netfilter hook(the ip-local-ip)被执行。

后者是IP-level处理的最后一个函数;ip_local_deliver_finish()执行layer 3未完成仍然pending的任务。IP header被剪裁来使packet可以被送到layer 4的协议。一个检查来确认packet是否属于raw IP socket,对应的handler(raw_v4_input())被调用。

Raw IP是一个允许application直接伪造/接收他们自己IP packet的协议,没有实际的layer 4的处理。它的主要用途是为了满足一些需要发送特殊packet的tool的功能。众所周知的例子是”ping“和”traceroute“,他们使用raw IP来创建一些有特殊值header的packet。另一个可能使用raw IP的例子是在user level实现定制化的网络协议(例如RSVP)。Raw IP被认为是PF_PACKET协议族中的标准一员。

更普遍的,虽然packet将会被送到更进一步的kernel protocol header。为了决定它是哪一种,将会检查IP header中的protocol field。这时kernel用的方法和net_rx_action()函数类似;定义一个hash,在inet_protos中,它包含所有注册了的post-IP protocol header。hash的key,是从IP header的protocol field中获得。inet_protos hash在kernel启动时候被inet_init(net/ipv4/af_inet.c)填充,它重复调用inet_add_protocol()来注册TCP, UDP, ICMP和IGMP handler(后者只有在允许多播时使用)。完整的protocol 表在net/ipv4/protocol.c中定义。

对于每一个protein,定义一个handler函数:tcp_v4_rcv, udp_rcv(), icmp_rcv()和igmp_rcv()通过名字即可对应上述描述的协议。他们当中的一个函数会被调用来处理packet。函数的返回值用于决定是否返回一个ICMP Ddestination Unreachable消息给发送者。这用于上层协议没找到这个packet属于的socket的情况。正如前文,嗅探网络数据的一个问题就是有一个独立于port/address的socket来接收packet。这里(刚提到的*_rcv函数)就是那个限制的发生地。

Conclusion

此时,packet已经走过了他的一半的旅程。剩下的将是layer 4处理(TCP/UDP),PF_PACKETs处理和socket filter hook实现。