關於bridge-nf-call-iptables的設計問題
熟悉ebtables和iptables的都知道,後者提供的選項所能實現的功能要遠遠多於前者,但這並不是說IP層的功能要比數據鏈路層的更豐富,因為只要數據包進入網卡,協議棧代碼就能“看到”整個數據包,剩下的問題就是如何來解析和過濾的問題了,只要願意,實際上協議棧完全可以在數據鏈路層提供和IP層同樣的過濾功能,比如ip_conntrack。 www.2cto.com
然而,協議棧並沒有這麼實現,因為那樣會造成嚴重的代碼冗余,維護成本將會很高。Linux的bridge filter提供了bridge-nf-call-iptables機制來使bridge的Netfilter可以復用IP層的Netfilter代碼。Netfilter提供了強大的過濾match,流識別機制,使每一個數據包都可以和一個五元組標示的流關聯起來,這樣就可以對整個流而不是單獨的數據包進行更加人性化的操作,而對流的識別以及之後的過濾作用最大的就是mark機制,注意這個mark並不是數據包本身的,它只在本機協議棧內有效。Netfilter代碼可以識別一個流的頭包,然後會將一個mark打入該流,接下來的數據包可以直接從流中取出該mark來進行過濾而不必再遍歷整個規則鏈了,類似下面的規則是常用的: www.2cto.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之前這個假設所帶來的收益完全失效,記住此點即可。