关于bridge-nf-call-iptables的设计问题
熟悉ebtables和iptables的都知道,后者提供的选项所能实现的功能要远远多于前者,但这并不是说IP层的功能要比数据链路层的更丰富,因为只要数据包进入网卡,协议栈代码就能“看到”整个数据包,剩下的问题就是如何来解析和过滤的问题了,只要愿意,实际上协议栈完全可以在数据链路层提供和IP层同样的过滤功能,比如ip_conntrack。 www.zhishiwu.com
然而,协议栈并没有这么实现,因为那样会造成严重的代码冗余,维护成本将会很高。Linux的bridge filter提供了bridge-nf-call-iptables机制来使bridge的Netfilter可以复用IP层的Netfilter代码。Netfilter提供了强大的过滤match,流识别机制,使每一个数据包都可以和一个五元组标示的流关联起来,这样就可以对整个流而不是单独的数据包进行更加人性化的操作,而对流的识别以及之后的过滤作用最大的就是mark机制,注意这个mark并不是数据包本身的,它只在本机协议栈内有效。Netfilter代码可以识别一个流的头包,然后会将一个mark打入该流,接下来的数据包可以直接从流中取出该mark来进行过滤而不必再遍历整个规则链了,类似下面的规则是常用的: www.zhishiwu.com
iptables -t mangle -I PREROUTING -j CONNMARK --restore-mark
iptables -t mangle -A PREROUTING -m mark ! --mark 0 -j ACCEPT
iptables -t mangle -A PREROUTING -m state --state ESTABLISHED -j ACCEPT
iptables -t mangle -N mark_Policy"
iptables -t mangle -A mark_Policy $matches1 -j MARK --set-mark 100
iptables -t mangle -A mark_Policy $matches2 -j MARK --set-mark 100
iptables -t mangle -A mark_Policy -m mark ! --mark 0 -j CONNMARK --save-mark
类似一种cache机制,只有一个流的第一个数据包才要遍历整个规则链,其余的就可以直接restore出来mark了,接下来协议栈可以根据该mark来进行过滤或者进行Policy Routing。
如果使用bridge-nf-call-iptables的话,能否使bridge层利用上述优势呢?比如抉择哪些数据包需要被本地捕获,哪些数据包需要丢弃,答案当然是模棱两可的,并不绝对。对于上面第二个问题,抉择哪些数据包需要丢弃是可以做到的,因为bridge-nf-call-iptables作用于bridge Netfilter的PREROUTING上,完全可以在FORWARD上做Drop or not的抉择,这没有任何问题,然而对于第一个问题,哪些数据包需要被本地IP层捕获,当前的实现就无能为力,然而只需要修改不多的两行bridge模块的代码,问题便迎刃而解,然而能做如此小的手术解决如此大的问题,确实需要积累很多的常识,我不是自夸,这是实话。
在给出解决办法之前,我首先给出将本应该bridge出去的数据帧捕获到本地IP层会在哪里用到,如果没有实际的需求而去修改代码,那未免太学院派了。一个典型的需求就是透明网桥模式的VPN,VPN的加密和封装需要在IP层进行,因此需要把感兴趣流捕获到IP层,不感兴趣流直接bridge出去,这是一个实际的需求,然而现有的bridge模块的代码却是解决不了,why?听我娓娓道来。
Linux的bridge代码中,bridge-nf-call-iptables体现在br_nf_pre_routing函数中,该函数也是一个Netfilter HOOK函数:
[plain]
static struct nf_hook_ops br_nf_ops[] __read_mostly = {
{
.hook = br_nf_pre_routing,
.owner = THIS_MODULE,
.pf = PF_BRIDGE,
.hooknum = NF_BR_PRE_ROUTING,
.priority = NF_BR_PRI_BRNF,
},
...
}
在该函数的最后:
[plain]
NF_HOOK(PF_INET, NF_INET_PRE_ROUTING, skb, skb->dev, NULL,
br_nf_pre_routing_finish);
调用了IP层的Netfilter PREROUTING代码,我希望先调用IP层的Netfilter,在其mangle表中设置好感兴趣流的mark,然后在bridge的nat表中将打上mark的数据帧redirect到本地的IP层,遗憾的是,这是无法做到的,因为优先级的关系,br_nf_pre_routing的优先级是NF_BR_PRI_BRNF,它位于nat的优先级之后:
[plain]
enum nf_br_hook_priorities {
NF_BR_PRI_FIRST = INT_MIN,
NF_BR_PRI_NAT_DST_BRIDGED = -300, //NAT的优先级
NF_BR_PRI_FILTER_BRIDGED = -200,
NF_BR_PRI_BRNF = 0, //br_nf_pre_routing的优先级
NF_BR_PRI_NAT_DST_OTHER = 100,
NF_BR_PRI_FILTER_OTHER = 200,
NF_BR_PRI_NAT_SRC = 300,
NF_BR_PRI_LAST = INT_MAX,
};
因此即使IP层的Netfilter为数据帧打上了mark,该mark也不可能为NAT所用,因此此时已经执行过NAT了...如果此时你说还可以在BROUTING上将数据帧热direct到local IP layer,那你的设备就完全成了一个IP层的设备,虽说还能保持bridge的语义(比如放过arp数据帧),然而这种设计会让你的产品文档很令人费解,你的心理预期也将和最终所想的谬之千里。
最后,我们来看看应该怎么修改代码来解决这个问题。最本质的,那就是修改br_nf_pre_routing这个HOOK函数的优先级,使之执行于bridge的NAT之后,这比较好办,修改br_netfilter.c代码:
[plain]
static struct nf_hook_ops br_nf_ops[] __read_mostly = {
{
.hook = br_nf_pre_routing,
.owner = THIS_MODULE,
.pf = PF_BRIDGE,
.hooknum = NF_BR_PRE_ROUTING,
#ifdef IP_FILTER_BEFORE_NAT
/**
* 2013/03/06 by 赵亚
* 使iptables的PREROUTING在ebtables的DNAT之前进行,
* 因为网桥的DNAT要使用iptables设置的mark
*/
.priority = NF_BR_PRI_NAT_DST_BRIDGED-1,
#else
.priority = NF_BR_PRI_BRNF,
#endif
...
另一处修改是br_nf_pre_routing_finish,问题涉及到执行完IP Netfilter之后,需要从哪里继续的问题,修改该函数的最后:
[plain]
#ifdef IP_FILTER_BEFORE_NAT
/**
* 2013/03/06 by 赵亚
* 重新开始NF_BR_PRI_BRNF
*/
NF_HOOK_THRESH(PF_BRIDGE, NF_BR_PRE_ROUTING, skb, skb->dev, NULL,
br_handle_frame_finish, NF_BR_PRI_NAT_DST_BRIDGED);
#else
NF_HOOK_THRESH(PF_BRIDGE, NF_BR_PRE_ROUTING, skb, skb->dev, NULL,
br_handle_frame_finish, 1);
#endif
NF_BR_PRI_BRNF被定义成了0,如果按照标准的现有2.6.32内核的实现,应该从优先级1开始执行,然而我们的修改版上,由于此时还没有执行NAT,因此需要从NAT开始执行,而我们的br_nf_pre_routing优先级被设置成了NAT的优先级减去1,那么接下来应该从NAT开始。
这个修改也不是说没有副作用的,它使得标准的实现,即NAT位于IP Netfilter之前这个假设所带来的收益完全失效,记住此点即可。