大一统的Netfilter-一种Linux防火墙优化方法

1.背景

目前,Linux的防火墙是基于Netfilter实现的。Netfilter可谓是无所不能,Netfilter的官网的patch-o-matic-ng补丁中几乎每天都会有更新,都会有更好玩的东西。然而基本上做过测试的都知道,在大量规则存在的情况下,Netfilter会导致网络性能下降,这非常不适合做压力测试。Netfilter的网站上以及内核开发社区或者说各大论坛上的大牛们几乎都在关注功能问题,因此一个又一个很眩的功能被开发,而几乎没有人关注过性能,也许在硬件越来越便宜且高效的当世,大家对软件也不再那么苛刻了吧。
前些天在旧货市场花了一笔钱掏了几台P4的老机器,配置都是128M/256M的DDR内存,P42.4的处理器-cpu还不错,磁盘40G,...积蓄就这样没了,我的愿望是用这几台破烂玩意儿搞出一个猛家伙,如果可能,算下来还赚了呢(一台破机器不到200块,一共4台也不到1000块)...每天不管多晚回到家,只要还没烂醉(竹叶青,真露,溪汶,反正没有二锅头),我都会鼓捣一会儿我那堆破烂儿,只想有一天能出点状况啥的。
不管怎么搞,发现只要Netfilter规则越多,性能就越低,这当然是真理!然而我实在不喜欢Netfilter的匹配方式,总的来讲,虽然Netfilter规定了内核中有5个HOOK点,然而这个规定只在一个协议集中有效,比如对于INET协议集有5个HOOK点,同样的道理,在Bridge层,也有5个HOOK点,如果一个从一个多端口网卡(桥)进入本地的数据包,在到达用户态会经过4个HOOK点,其中Bridge两个,INET两个。我觉得HOOK点太多了,这就意味着一个数据包会被频繁的检查!
近来看了几集水浒传,发现其中有个细节可以被Netfilter借鉴的,有人进梁山泊,在一个关卡被抓住盘问之后,只要是可以绝对信任的自己人,一个关卡的守卫就射一只箭往前方,告诉前方的关卡此人可以直接通过。可见当年的梁山好汉们都知道用这个方式来节省时间,Netfilter为何不也用这种方式呢?因此冒出一个想法,那就是修改一下Netfiler的匹配方式。

2.三个问题

2.1.虚拟(逻辑)网卡可以自定义HOOK

对于OpenVPN这种程序而言,虽然它功能强大,可扩展性很佳,然则其性能确实不敢恭维,如果想实现OpenVPN载荷的访问控制(控制虚拟通道的五元素),修改OpenVPN代码是一个最直接的方式,然而这会使OpenVPN更慢,那么把这个逻辑用iptables配置实现也是一种很不错的方式,然而OpenVPN在server模式下大量用户连接情况下需要过多的规则,这可能会拖慢整个网络协议栈的处理速度,换句话说,大量的针对OpenVPN的规则过连累非虚拟网卡的流量,这也是不可取的。
我们需要的是在tun驱动中实现访问控制,这样就不会拖累其它流量了,其次,我们需要在tun中做了访问控制之后,tun的流量将不再经过任何的HOOK(这很笼统,实现见下文)。

2.2.一个流只需要第一个包被检查

如果一个流的第一个包被允许通过了,那么后续的包难道还有必要经过检查吗?它们甚至都不用经过检查点。这一点正如旅游团的导游一个人带队进入景点检票口,后面的游客就不会再被检查。

2.3.上述两点的局限性

我们知道Netfilter不仅仅可以实现过滤(ACCEPT或者DROP),还可以实现LOG等,并且FreeSWAN也是用Netfilter实现的,如果实现了2.1和2.2的话,这些策略就会被绕过,因为HOOK点被绕过了。换句话说,有些检查点并不是为了检查数据包是否被允许通过,而是为了“赠送”一些别的服务。
因此,只有确保大量规则中仅有ACCEPT或者DROP这种target的时候,绕过检查点才是可行的。主要关注的对IPT_CONTINUE这个target结果的处理,由于没有时间,这部分没有实现,因此需要用肉眼确认而不是将一切拜托于程序。

3.解决方案

本文对内核的修改基于2.6.32内核

3.1.基于单包的检查点绕过

需要做的工作很简单,就是在sk_buff中增加一个二维数组,因为我们希望基于每一个HOOK进行绕过控制,并且本身Netfilter的检查点就是二维的,因此这是个二维数组。
struct sk_buff { ... unsigned int handled[MAX_PF][MAX_HOOKS]; }

对于代码的修改也比较简单,主要是修改NF_HOOK宏,在其中检查该skb在该HOOK点是否能绕过
#define NF_HOOK(pf, hook, skb, indev, outdev, okfn) \ ({ int _handled; \ if ((_handled=((skb)->handled[pf][hook])) == 1) \ //如果该skb在该检查点有通行证,那么直接放过 (okfn)(skb) \ else { \ NF_HOOK_THRESH(pf, hook, skb, indev, outdev, okfn, INT_MIN) \ } })

在虚拟网卡驱动(如有必要,也可以在物理网卡驱动)中设置skb对应的handled[pf][hook]为1,就可以让这个skb绕过相应的HOOK点。这是很有意义的,因此网卡是数据进入路由器/防火墙/主机的第一道门,因此在该第一道门中进行强控制是可行的,这对于虚拟网卡有特别的意义,因为虚拟网卡对性能要求更苛刻,而且可是实现很多自定义特性,可定制性很强。
对于tun驱动,修改其tun_get_user函数,将对netif_rx_ni的直接调用替换成NF_HOOK的调用
static __inline__ ssize_t tun_get_user(struct tun_struct *tun, const struct iovec *iv, size_t count, int noblock) { ... NF_HOOK(PF_TUN, NF_TUN_CHECK_USER, skb, tun->dev, NULL, netif_rx_ni); ... }

修改ip_forward函数,如果出口是tun网卡,那么不再经过NF_IP_FORWARD,对于这一点,实际上可以实现成可自动根据注册信息识别的,比如将tun设备注册到直通链表中,然后在ip_forward对直通链表进行扫描。
看一下上述的NF_HOOK调用,发现里面有PF_TUN和NF_TUN_CHECK_USER这对陌生的PF/HOOK对,这是我自己定义的,实际上内核中并不仅仅有对iptables的支持,还有对arptables以及ebtables之类的tables支持,每一个这样的tables都会对应一系列HOOK点,一般而言都是5个,更本质的说,每一个tables对应一个协议集(横向)或者一个协议层(纵向),之所以配置几个tables是为了配合用户态的XYtables程序,很多的Netfilter钩子函数并不和任何tables绑定,这说明它们要么是内核定死的,要么就是通过其它的方式进行配置的。
因此可以比葫芦画瓢地实现一个tuntables用户态程序,然后再比葫芦画瓢地实现一个内核的对应Netfilter tables,我们就可以配置以下的策略了:
tuntables -A CHECK -s 172.16.0.4 -p tcp --dport 80 -j DROP
这就实现了172.16.0.4这个用户访问任意主机的tcp的80端口了,注意虚拟ip地址是可以和证书绑定的,因此这可以实现用户的访问控制了。

3.2.基于流的检查点绕过以及提前丢弃

这个要比上述的基于单包的检查点绕过更简单,因为基于单包的绕过策略需要考虑的是在“什么时候”去设置skb的handled字段,而基于流的检查点绕过不需要考虑这个问题,而只需要在流的第一个包经过时给予设置就可以了,更底层的事情,ip_conntrack已经为我们做了。
同样的,在nf_conn中增加一个和skb中一样的handled字段,同时再增加一个early_drop字段,这是为了提前DROP掉设置了early_drop字段的流中的数据包,这个early_drop字段在一个流的第一个数据包经过filter表第一次被DROP掉的时候被设置。
struct nf_conn { ... unsigned int handled[MAX_PF][MAX_HOOKS]; unsigned int early_drop; }

这里的问题是,如果在ip_conntrack中根据early_drop将数据包DROP掉了,那么其它的target将不会被执行,挂在filter上的日志审计信息target也不会执行。然而2.3节中所言,我们需要肉眼识别出这类target,确保没有这类target时才能这么做。
修改后的nf_hook_thresh如下:
static inline int nf_hook_thresh(u_int8_t pf, unsigned int hook, struct sk_buff *skb, struct net_device *indev, struct net_device *outdev, int (*okfn)(struct sk_buff *), int thresh, int cond) { enum ip_conntrack_info ctinfo; struct nf_conn *conn; int ret; if (!cond) return 1; if (conn = nf_ct_get(skb, &ctinfo)) { if (conn->handled[pf][hook]) //如果该skb属于一个有通行证的流,则直接放过 return 1; } #ifndef CONFIG_NETFILTER_DEBUG if (list_empty(&nf_hooks[pf][hook])) return 1; #endif ret = nf_hook_slow(pf, hook, skb, indev, outdev, okfn, thresh); if (conn) { if (ret == 1 { //为一个流中领头的包颁发通行证 conn->handled[pf][hook] = 1; } else { //拒绝该流的领头者,意味着后续的都要被拒绝 conn->early_drop = 1; } return ret; }

在nf_conntrack_in增加对nf_conn的early_drop的处理逻辑:
unsigned int nf_conntrack_in(struct net *net, u_int8_t pf, unsigned int hooknum, struct sk_buff *skb) { ... ct = resolve_normal_ct(net, skb, dataoff, pf, protonum, l3proto, l4proto, &set_reply, &ctinfo); if (ct->early_drop) //如果一个流的领头者被拒绝,那么拒绝该流所有的后续队伍 return NF_DROP; ... }

虽然ip_conntrack本身对性能就会有很多影响,以上的这个修改的意义在于,在已经被ip_conntrack拖累的数据包不要再被filter拖累,而是尽一切力量去利用ip_conntrack能给予的利益!

4.总结

本文实现一种大一统的Netfilter框架,其根本思想在于诸多的HOOK点是可以相互作用的,在日常生活中,有关检查点的逻辑确实是这样的。如果需要得到更多的用户态的支持代码,请随时参考netfilter.org,其中有大量的很新奇的代码,其patch-o-matic-ng中拥有大量的Netfilter内核补丁,这些补丁虽然还没有或者永远不可能并入内核主干,然则其参考意义重大。

你可能感兴趣的:(大一统的Netfilter-一种Linux防火墙优化方法)