FreeBSD的netgraph真是太帥了,它到底是個(gè)什么玩藝呢?知道Linux的Netfilter的不少,那么就用Netfilter來(lái)類比吧。netgraph是一個(gè)基于圖的鉤子系統(tǒng),正如其名稱所展示的那樣,什么樣的圖呢?很簡(jiǎn)單,就是通過(guò)邊連接的節(jié)點(diǎn),和數(shù)據(jù)結(jié)構(gòu)里面學(xué)到的一樣。netgraph系統(tǒng)掛接在內(nèi)核協(xié)議棧的特定點(diǎn)上,哪些點(diǎn)呢?這個(gè)和Netfilter很類似,但是卻不是Netfilter精心設(shè)計(jì)的那5個(gè)點(diǎn),而是更簡(jiǎn)單的每一層處理的輸入點(diǎn)和輸出點(diǎn),如下圖所示:
netgraph到底長(zhǎng)什么樣子呢?到目前為止,我們只是知道了一張圖掛上去了,這僅僅是個(gè)接口,一個(gè)開(kāi)始,既然掛上去了,數(shù)據(jù)包就從此處進(jìn)入這張圖了,把它叫做地圖更加適合,因此從此以后,數(shù)據(jù)包就要在游歷于這張地圖了,最終的結(jié)果有兩個(gè):
1.數(shù)據(jù)包從地圖的某處出來(lái),重新進(jìn)入系統(tǒng)標(biāo)準(zhǔn)的協(xié)議棧的當(dāng)初被攔截的那個(gè)地方;
2.數(shù)據(jù)包再也沒(méi)有出來(lái)回到原點(diǎn),要么被地圖吃掉了(進(jìn)入了某一房間?),要么就是從某處出去,進(jìn)入?yún)f(xié)議棧的別的地方。
以上兩點(diǎn)很類似于Netfilter的ACCEPT,STOLEN這樣的結(jié)果,仔細(xì)想想不是么?netgraph和標(biāo)準(zhǔn)協(xié)議棧的銜接如下圖所示:
既然知道了netgraph的位置,那么下面就看看它的樣子吧。還是先給出一幅圖
該圖中有兩種元素,一種是節(jié)點(diǎn),另一種是連接到節(jié)點(diǎn)的邊的兩端的頂點(diǎn)。在netgraph的術(shù)語(yǔ)中,節(jié)點(diǎn)就是Node,而頂點(diǎn)叫做hook,一條邊連接兩個(gè)hook,hook通過(guò)CONNECT/MKPEER構(gòu)成一條邊。從上圖中可以看出,一條邊的兩端必然有兩個(gè)hook,從命名上可以看出這些“邊的端點(diǎn)”其實(shí)就是真正處理數(shù)據(jù)的地方,而Node其實(shí)就是一個(gè)“數(shù)據(jù)+操作”的封裝,一個(gè)Node可以有多個(gè)hook,通過(guò)這些hook連接到其它的Node。
我們可以用OO的思想來(lái)理解這些個(gè)netgraph的概念,Node就是一個(gè)對(duì)象,每一個(gè)Node都有它所屬的Type,可以將Type理解成類。而hook其實(shí)就是一個(gè)Node對(duì)象的私有數(shù)據(jù),整個(gè)graph通過(guò)“各個(gè)hook的對(duì)接”來(lái)完成,F(xiàn)reeBSD提供了豐富的命令來(lái)完成netgraph的構(gòu)建,說(shuō)白了其實(shí)就是以下幾步驟:
1.生成一系列的Node對(duì)象;
2.為每一個(gè)Node定義一個(gè)或多個(gè)hook;
3.將特定的Node通過(guò)hook連接在一起。
如此一來(lái)整個(gè)graph就構(gòu)建好了,F(xiàn)reeBSD提供了struct ng_type,它便是代表了一個(gè)類,然后你每生成一個(gè)特定ng_type的實(shí)例就相當(dāng)于生成了一個(gè)對(duì)象,通過(guò)對(duì)該結(jié)構(gòu)體里面的一些字段的理解,我們就可以完整理解數(shù)據(jù)包在這個(gè)graph中的游歷過(guò)成了。struct ng_type定義如下:
注釋很清楚了,自不必說(shuō),如果我們看看其中一些回調(diào)函數(shù)的定義,就更能理解了。“構(gòu)造函數(shù)”和“析構(gòu)函數(shù)”都有,每一個(gè)“成員函數(shù)”的參數(shù)列表的第一個(gè)參數(shù)類型都是node_p,這難道不是this么?這里唯一要注意的就是rcvdata回調(diào)函數(shù),該函數(shù)接收從另一個(gè)Node發(fā)送過(guò)來(lái)的數(shù)據(jù),接收者是hook,而不是Node,再次強(qiáng)調(diào),Node之間通過(guò)hook相連接,而不是通過(guò)node本身,然而每一個(gè)hook都要唯一綁定一個(gè)Node對(duì)象,因此我們可以從hook解析出唯一的Node對(duì)象,卻不能從Node中直接得到hook(一個(gè)Node對(duì)象擁有N多hook呢),要分清一對(duì)一和一對(duì)多的關(guān)系。因此rcvdata的第一個(gè)參數(shù)是hook_p就是合理的了。
Node和Node之間通過(guò)hook傳遞控制信息,而網(wǎng)絡(luò)數(shù)據(jù)包則是通過(guò)一個(gè)hook向其peer hook發(fā)送消息的方式完成的,當(dāng)然所謂的發(fā)送消息大多數(shù)情況下就是函數(shù)直接調(diào)用。既然一條邊兩端有兩個(gè)hook,那么每一個(gè)hook就有一個(gè)peer,每當(dāng)我們將數(shù)據(jù)包發(fā)送到一個(gè)hook的時(shí)候,實(shí)際的效果就是數(shù)據(jù)包被發(fā)送到了該hook的peer,這是netgraph的核心邏輯實(shí)現(xiàn)的,我們可以從下面的這個(gè)核心宏中看到這一點(diǎn):
其中ng_address_hook完成了peer的定位,這個(gè)peer可以通過(guò)ngctl命令來(lái)設(shè)置。
就這樣,一個(gè)數(shù)據(jù)包在整個(gè)netgraph中通過(guò)“離開(kāi)一個(gè)Node的某個(gè)hook,進(jìn)入另一個(gè)Node的某個(gè)hook的rcvdata”的方式游歷,Node在這里的作用就是封裝私有數(shù)據(jù)和統(tǒng)一的操作,當(dāng)然,你可以重載掉一個(gè)Node內(nèi)統(tǒng)一的rcvdata回調(diào)函數(shù),而是為每一個(gè)hook都設(shè)置一個(gè)私有的rcvdata回調(diào)函數(shù),再次強(qiáng)調(diào),是hook在rcvdata,而不是Node在rcvdata,Node的rcvdata是一個(gè)該Node所有hook通用的回調(diào)函數(shù),如果沒(méi)有hook私有的rcvdata,該通用函數(shù)將被調(diào)用,ng_snd_item最終將進(jìn)入下面的邏輯:
由此看出,Node有一個(gè)默認(rèn)的對(duì)所有hook都適用的rcvdata回調(diào)函數(shù),然而各個(gè)hook可以重載掉這個(gè)默認(rèn)的rcvdata回調(diào)函數(shù)。
接下來(lái)我們看一下netgraph如何和協(xié)議棧對(duì)接,不要把操作系統(tǒng)想得太神奇,實(shí)際上完成這種工作只需要一個(gè)回調(diào)函數(shù)即可。以以太網(wǎng)接收為例,以太網(wǎng)接收處理函數(shù)中會(huì)調(diào)用ng_ether_input_p回調(diào)函數(shù),你只需要將其定義一下即可,對(duì)于很多場(chǎng)合都使用的ng_ether,它將此函數(shù)定義為:
最后通過(guò)NG_SEND_DATA_ONLY將數(shù)據(jù)包發(fā)送給priv->lower這個(gè)hook,最終數(shù)據(jù)包會(huì)進(jìn)入priv->lower的peer,調(diào)用priv->lower->peer的rcvdata回調(diào)函數(shù),在一切開(kāi)始工作之前,你首先需要構(gòu)建好整個(gè)graph。對(duì)于以太網(wǎng)發(fā)送函數(shù),也有類似的_p回調(diào)函數(shù)。
netgraph和Netfilter的區(qū)別在于它可以將graph“掛接”在特定的interface上,而Netfilter卻把HOOK直接掛在協(xié)議棧本身,interface在Netfilter中只是一個(gè)match。如此一比較,效率差異就很明顯了。以以太網(wǎng)為例,在ether_input中就會(huì)調(diào)用netgraph,如果加載了ng_ether的話,就會(huì)調(diào)用下面的函數(shù):
如果本ifp上沒(méi)有掛接任何graph,則直接返回標(biāo)準(zhǔn)協(xié)議棧處理,如果掛接了一個(gè)graph,則數(shù)據(jù)包將進(jìn)入該graph,你可以將firewall rule配置在此graph里面。對(duì)于Netfilter而言,在網(wǎng)卡接收這一層,沒(méi)有任何HOOK,只有到了IP層,才會(huì)進(jìn)入PREROUTING/INPUT/FORWARD...等HOOK,哪怕你配置了一條rule,所有的包都將接受檢查以確定是否匹配,在Netfilter的rule中,所謂的interface只是一個(gè)match。
需要說(shuō)明的是,netgraph也可以像Netfilter那樣工作,你只需要將其掛在ip_in(out)put上即可。
我們給出兩個(gè)例子來(lái)看看netgraph如何實(shí)現(xiàn)bridge和bonding,這些在Linux上都是通過(guò)虛擬net_device來(lái)實(shí)現(xiàn)的,其發(fā)送邏輯都是該虛擬net_device的hard_xmit實(shí)現(xiàn)的,而其數(shù)據(jù)接收邏輯則是硬編碼在netif_receive_skb中的,bridge是通過(guò)handle_bridge這個(gè)硬編碼hook進(jìn)入的,而bonding是通過(guò)skb_bond來(lái)實(shí)現(xiàn)的。也就是說(shuō)Linux是通過(guò)對(duì)既有的協(xié)議棧進(jìn)行硬修改來(lái)實(shí)現(xiàn)的,而netgraph則不需要這樣,對(duì)于FreeBSD,我們只需要構(gòu)建一張graph就可以實(shí)現(xiàn)bridge或者bonding,首先我們先看看bridge的實(shí)現(xiàn)邏輯,如下圖所示:
我個(gè)人以為圖示已經(jīng)很清晰了。需要注意的是,netgraph將本地的網(wǎng)卡作為了局域網(wǎng)上一張普通的網(wǎng)卡來(lái)看待,并沒(méi)有刻意區(qū)分流量是本機(jī)發(fā)出的還是從其它機(jī)器發(fā)出的,因此,如果你只是想將bridge作為一個(gè)二層設(shè)備,那么可以斷開(kāi)Hook-ethX-low和Hook-ethX-upper之間的邊即可,netgraph實(shí)現(xiàn)的bridge,你看不到虛擬設(shè)備,這種實(shí)現(xiàn)更純粹,偉大的BSD將這種思想帶給了其衍生出來(lái)的Cisco IOS。
下面是bonding的實(shí)現(xiàn)邏輯:
由于bonding網(wǎng)卡大多數(shù)負(fù)責(zé)的是本地IP層發(fā)出的數(shù)據(jù),需要和路由轉(zhuǎn)發(fā)表相配合,因此需要有一塊虛擬網(wǎng)卡,這個(gè)是通過(guò)ng_eiface的構(gòu)造函數(shù)ng_eiface_constructor實(shí)現(xiàn)的。依然無(wú)其它話可說(shuō)。
以上兩個(gè)圖展示了netgraph的魅力,既然這樣,也就可以依照這種方式實(shí)現(xiàn)VLAN,IPSec等了,要比Linux的Netfilter加設(shè)備驅(qū)動(dòng)模型的實(shí)現(xiàn)方式更“可插拔”,有netgraph,F(xiàn)reeBSD可以將所有的協(xié)議處理在一張張的graph中進(jìn)行,數(shù)據(jù)包在graph中游歷在每一個(gè)Node被接收到的hook處理,主要你能根據(jù)協(xié)議處理邏輯構(gòu)建好一張圖,將這張圖掛接在協(xié)議棧,甚至掛接在驅(qū)動(dòng)上,你就能很方便的實(shí)現(xiàn)網(wǎng)絡(luò)的任意擴(kuò)展...
最后看一下netgraph的依賴關(guān)系,在netgraph中,每張圖都是相對(duì)獨(dú)立的,數(shù)據(jù)包從某處進(jìn)入一張圖A,然后從某處出來(lái),在另一處再進(jìn)入圖B,此時(shí)它將不能再使用圖A。這和Netfilter不同,Netfilter基于HOOK設(shè)計(jì),使用一些match來(lái)進(jìn)行filter,比如NAT就需要ip_conntrack,ctdir需要ip_conntrack等等,ip_conntrack一直都面臨table full的問(wèn)題,因此你要用raw表的NOTRACK這個(gè)target來(lái)免除追蹤不感興趣流量來(lái)緩解這個(gè)問(wèn)題。有下面的需求:
從網(wǎng)段M發(fā)出到網(wǎng)段N的流量(兩個(gè)方向)打上tag待策略路由來(lái)處理,從網(wǎng)段N發(fā)出到網(wǎng)段M的流量(兩個(gè)方向)不打tag。
分析:
很顯然要使用ctdir這個(gè)match,否則將會(huì)過(guò)濾掉返回流量,于是有以下target為NOTRACK的match:
!dst N/interface $內(nèi)網(wǎng)口
然而意味著從網(wǎng)段N發(fā)出到達(dá)M的返回流量也將被conntrack,這是因?yàn)閏tdir和conntrack相互依賴才導(dǎo)致了這樣的問(wèn)題,在raw表中,你甚至都不知道數(shù)據(jù)包到底是走INPUT還是FORWARD,所以你很難讓所有這一切關(guān)聯(lián)起來(lái),雖然conntrack可以保持一個(gè)流信息在內(nèi)存中,但是卻可能存在大量不相關(guān)的流也被保存。如果使用netgraph呢?很簡(jiǎn)單,我們可以寫在兩個(gè)命令中:
No.x check-status
No.z skip No.y from N to M #對(duì)于返回流量,只檢測(cè)conntrack
No.y netgraph tag from M to N keep-status
如此即可。FreeBSD不需要conntrack,它內(nèi)建了一個(gè)動(dòng)態(tài)ruleset,凡是keep-status的流量都將自動(dòng)將返回流量加入動(dòng)態(tài)ruleset中,實(shí)際上也就是“保持了一個(gè)流信息在內(nèi)存中”,F(xiàn)reeBSD的conntrack和單獨(dú)的rule相關(guān)聯(lián)而不是和整個(gè)協(xié)議棧關(guān)聯(lián),這實(shí)際上也是netgraph的思想,我們看一下rule相關(guān)的conntrack和協(xié)議棧香瓜的conntrack的區(qū)別:
IPFW:沒(méi)有全局的conntrack信息,然而需要查詢動(dòng)態(tài)ruleset,以匹配返回流量;
Netfilter:需要查詢?nèi)值腸onntrack表,可以取出一切頭包經(jīng)過(guò)時(shí)流量的匹配結(jié)果,不需要也沒(méi)有動(dòng)態(tài)ruleset
我們看一下全局的conntrack和全局的ruleset所針對(duì)的對(duì)象有何不同。很簡(jiǎn)單,全局的conntrack針對(duì)除了NOTRACK的所有的數(shù)據(jù)包,然而如果NOTRACK需要指明方向,就會(huì)需要循環(huán)依賴,問(wèn)題將無(wú)解。全局的動(dòng)態(tài)ruleset僅僅針對(duì)匹配到的數(shù)據(jù)包,對(duì)其它的沒(méi)有匹配到的數(shù)據(jù)包除了一個(gè)查詢性能影響之外沒(méi)有其他影響,事到如今,我想查詢性能應(yīng)該不是問(wèn)題吧,再說(shuō)動(dòng)態(tài)ruleset一般都比全局conntrackset小得多,查詢conntrackset都不怕,查詢動(dòng)態(tài)ruleset就怕了么?換句話說(shuō),Netfilter的ip_conntrack是寧可枉殺一千,不能使一人漏網(wǎng),而ipfw則是精確的匹配。效率啊,BSD不愧是網(wǎng)絡(luò)領(lǐng)頭軍!

netgraph到底長(zhǎng)什么樣子呢?到目前為止,我們只是知道了一張圖掛上去了,這僅僅是個(gè)接口,一個(gè)開(kāi)始,既然掛上去了,數(shù)據(jù)包就從此處進(jìn)入這張圖了,把它叫做地圖更加適合,因此從此以后,數(shù)據(jù)包就要在游歷于這張地圖了,最終的結(jié)果有兩個(gè):
1.數(shù)據(jù)包從地圖的某處出來(lái),重新進(jìn)入系統(tǒng)標(biāo)準(zhǔn)的協(xié)議棧的當(dāng)初被攔截的那個(gè)地方;
2.數(shù)據(jù)包再也沒(méi)有出來(lái)回到原點(diǎn),要么被地圖吃掉了(進(jìn)入了某一房間?),要么就是從某處出去,進(jìn)入?yún)f(xié)議棧的別的地方。
以上兩點(diǎn)很類似于Netfilter的ACCEPT,STOLEN這樣的結(jié)果,仔細(xì)想想不是么?netgraph和標(biāo)準(zhǔn)協(xié)議棧的銜接如下圖所示:

既然知道了netgraph的位置,那么下面就看看它的樣子吧。還是先給出一幅圖

該圖中有兩種元素,一種是節(jié)點(diǎn),另一種是連接到節(jié)點(diǎn)的邊的兩端的頂點(diǎn)。在netgraph的術(shù)語(yǔ)中,節(jié)點(diǎn)就是Node,而頂點(diǎn)叫做hook,一條邊連接兩個(gè)hook,hook通過(guò)CONNECT/MKPEER構(gòu)成一條邊。從上圖中可以看出,一條邊的兩端必然有兩個(gè)hook,從命名上可以看出這些“邊的端點(diǎn)”其實(shí)就是真正處理數(shù)據(jù)的地方,而Node其實(shí)就是一個(gè)“數(shù)據(jù)+操作”的封裝,一個(gè)Node可以有多個(gè)hook,通過(guò)這些hook連接到其它的Node。
我們可以用OO的思想來(lái)理解這些個(gè)netgraph的概念,Node就是一個(gè)對(duì)象,每一個(gè)Node都有它所屬的Type,可以將Type理解成類。而hook其實(shí)就是一個(gè)Node對(duì)象的私有數(shù)據(jù),整個(gè)graph通過(guò)“各個(gè)hook的對(duì)接”來(lái)完成,F(xiàn)reeBSD提供了豐富的命令來(lái)完成netgraph的構(gòu)建,說(shuō)白了其實(shí)就是以下幾步驟:
1.生成一系列的Node對(duì)象;
2.為每一個(gè)Node定義一個(gè)或多個(gè)hook;
3.將特定的Node通過(guò)hook連接在一起。
如此一來(lái)整個(gè)graph就構(gòu)建好了,F(xiàn)reeBSD提供了struct ng_type,它便是代表了一個(gè)類,然后你每生成一個(gè)特定ng_type的實(shí)例就相當(dāng)于生成了一個(gè)對(duì)象,通過(guò)對(duì)該結(jié)構(gòu)體里面的一些字段的理解,我們就可以完整理解數(shù)據(jù)包在這個(gè)graph中的游歷過(guò)成了。struct ng_type定義如下:
struct ng_type { u_int32_t version; /* must equal NG_API_VERSION */ const char *name; /* Unique type name */ modeventhand_t mod_event; /* Module event handler (optional) */ ng_constructor_t *constructor; /* Node constructor */ ng_rcvmsg_t *rcvmsg; /* control messages come here */ ng_close_t *close; /* warn about forthcoming shutdown */ ng_shutdown_t *shutdown; /* reset, and free resources */ ng_newhook_t *newhook; /* first notification of new hook */ ng_findhook_t *findhook; /* only if you have lots of hooks */ ng_connect_t *connect; /* final notification of new hook */ ng_rcvdata_t *rcvdata; /* data comes here */ ng_disconnect_t *disconnect; /* notify on disconnect */ const struct ng_cmdlist *cmdlist; /* commands we can convert */ LIST_ENTRY(ng_type) types; /* linked list of all types */ int refs; /* number of instances */ };
注釋很清楚了,自不必說(shuō),如果我們看看其中一些回調(diào)函數(shù)的定義,就更能理解了。“構(gòu)造函數(shù)”和“析構(gòu)函數(shù)”都有,每一個(gè)“成員函數(shù)”的參數(shù)列表的第一個(gè)參數(shù)類型都是node_p,這難道不是this么?這里唯一要注意的就是rcvdata回調(diào)函數(shù),該函數(shù)接收從另一個(gè)Node發(fā)送過(guò)來(lái)的數(shù)據(jù),接收者是hook,而不是Node,再次強(qiáng)調(diào),Node之間通過(guò)hook相連接,而不是通過(guò)node本身,然而每一個(gè)hook都要唯一綁定一個(gè)Node對(duì)象,因此我們可以從hook解析出唯一的Node對(duì)象,卻不能從Node中直接得到hook(一個(gè)Node對(duì)象擁有N多hook呢),要分清一對(duì)一和一對(duì)多的關(guān)系。因此rcvdata的第一個(gè)參數(shù)是hook_p就是合理的了。
Node和Node之間通過(guò)hook傳遞控制信息,而網(wǎng)絡(luò)數(shù)據(jù)包則是通過(guò)一個(gè)hook向其peer hook發(fā)送消息的方式完成的,當(dāng)然所謂的發(fā)送消息大多數(shù)情況下就是函數(shù)直接調(diào)用。既然一條邊兩端有兩個(gè)hook,那么每一個(gè)hook就有一個(gè)peer,每當(dāng)我們將數(shù)據(jù)包發(fā)送到一個(gè)hook的時(shí)候,實(shí)際的效果就是數(shù)據(jù)包被發(fā)送到了該hook的peer,這是netgraph的核心邏輯實(shí)現(xiàn)的,我們可以從下面的這個(gè)核心宏中看到這一點(diǎn):
#define NG_FWD_ITEM_HOOK_FLAGS(error, item, hook, flags) \ do { \ (error) = \ ng_address_hook(NULL, (item), (hook), NG_NOFLAGS); \ if (error == 0) { \ SAVE_LINE(item); \ (error) = ng_snd_item((item), (flags)); \ } \ (item) = NULL; \ } while (0)
其中ng_address_hook完成了peer的定位,這個(gè)peer可以通過(guò)ngctl命令來(lái)設(shè)置。
就這樣,一個(gè)數(shù)據(jù)包在整個(gè)netgraph中通過(guò)“離開(kāi)一個(gè)Node的某個(gè)hook,進(jìn)入另一個(gè)Node的某個(gè)hook的rcvdata”的方式游歷,Node在這里的作用就是封裝私有數(shù)據(jù)和統(tǒng)一的操作,當(dāng)然,你可以重載掉一個(gè)Node內(nèi)統(tǒng)一的rcvdata回調(diào)函數(shù),而是為每一個(gè)hook都設(shè)置一個(gè)私有的rcvdata回調(diào)函數(shù),再次強(qiáng)調(diào),是hook在rcvdata,而不是Node在rcvdata,Node的rcvdata是一個(gè)該Node所有hook通用的回調(diào)函數(shù),如果沒(méi)有hook私有的rcvdata,該通用函數(shù)將被調(diào)用,ng_snd_item最終將進(jìn)入下面的邏輯:
if ((!(rcvdata = hook->hk_rcvdata)) && (!(rcvdata = NG_HOOK_NODE(hook)->nd_type->rcvdata))) { error = 0; NG_FREE_ITEM(item); break; }
由此看出,Node有一個(gè)默認(rèn)的對(duì)所有hook都適用的rcvdata回調(diào)函數(shù),然而各個(gè)hook可以重載掉這個(gè)默認(rèn)的rcvdata回調(diào)函數(shù)。
接下來(lái)我們看一下netgraph如何和協(xié)議棧對(duì)接,不要把操作系統(tǒng)想得太神奇,實(shí)際上完成這種工作只需要一個(gè)回調(diào)函數(shù)即可。以以太網(wǎng)接收為例,以太網(wǎng)接收處理函數(shù)中會(huì)調(diào)用ng_ether_input_p回調(diào)函數(shù),你只需要將其定義一下即可,對(duì)于很多場(chǎng)合都使用的ng_ether,它將此函數(shù)定義為:
static void ng_ether_input(struct ifnet *ifp, struct mbuf **mp) { const node_p node = IFP2NG(ifp); const priv_p priv = NG_NODE_PRIVATE(node); int error; /* If "lower" hook not connected, let packet continue */ if (priv->lower == NULL) return; NG_SEND_DATA_ONLY(error, priv->lower, *mp); /* sets *mp = NULL */ }
最后通過(guò)NG_SEND_DATA_ONLY將數(shù)據(jù)包發(fā)送給priv->lower這個(gè)hook,最終數(shù)據(jù)包會(huì)進(jìn)入priv->lower的peer,調(diào)用priv->lower->peer的rcvdata回調(diào)函數(shù),在一切開(kāi)始工作之前,你首先需要構(gòu)建好整個(gè)graph。對(duì)于以太網(wǎng)發(fā)送函數(shù),也有類似的_p回調(diào)函數(shù)。
netgraph和Netfilter的區(qū)別在于它可以將graph“掛接”在特定的interface上,而Netfilter卻把HOOK直接掛在協(xié)議棧本身,interface在Netfilter中只是一個(gè)match。如此一比較,效率差異就很明顯了。以以太網(wǎng)為例,在ether_input中就會(huì)調(diào)用netgraph,如果加載了ng_ether的話,就會(huì)調(diào)用下面的函數(shù):
static void ng_ether_input(struct ifnet *ifp, struct mbuf **mp) { const node_p node = IFP2NG(ifp); const priv_p priv = NG_NODE_PRIVATE(node); int error; /* If "lower" hook not connected, let packet continue */ if (priv->lower == NULL) //如果這塊網(wǎng)卡上沒(méi)有任何hook,將不作處理直接返回。 return; NG_SEND_DATA_ONLY(error, priv->lower, *mp); /* sets *mp = NULL */ }
如果本ifp上沒(méi)有掛接任何graph,則直接返回標(biāo)準(zhǔn)協(xié)議棧處理,如果掛接了一個(gè)graph,則數(shù)據(jù)包將進(jìn)入該graph,你可以將firewall rule配置在此graph里面。對(duì)于Netfilter而言,在網(wǎng)卡接收這一層,沒(méi)有任何HOOK,只有到了IP層,才會(huì)進(jìn)入PREROUTING/INPUT/FORWARD...等HOOK,哪怕你配置了一條rule,所有的包都將接受檢查以確定是否匹配,在Netfilter的rule中,所謂的interface只是一個(gè)match。
需要說(shuō)明的是,netgraph也可以像Netfilter那樣工作,你只需要將其掛在ip_in(out)put上即可。
我們給出兩個(gè)例子來(lái)看看netgraph如何實(shí)現(xiàn)bridge和bonding,這些在Linux上都是通過(guò)虛擬net_device來(lái)實(shí)現(xiàn)的,其發(fā)送邏輯都是該虛擬net_device的hard_xmit實(shí)現(xiàn)的,而其數(shù)據(jù)接收邏輯則是硬編碼在netif_receive_skb中的,bridge是通過(guò)handle_bridge這個(gè)硬編碼hook進(jìn)入的,而bonding是通過(guò)skb_bond來(lái)實(shí)現(xiàn)的。也就是說(shuō)Linux是通過(guò)對(duì)既有的協(xié)議棧進(jìn)行硬修改來(lái)實(shí)現(xiàn)的,而netgraph則不需要這樣,對(duì)于FreeBSD,我們只需要構(gòu)建一張graph就可以實(shí)現(xiàn)bridge或者bonding,首先我們先看看bridge的實(shí)現(xiàn)邏輯,如下圖所示:

我個(gè)人以為圖示已經(jīng)很清晰了。需要注意的是,netgraph將本地的網(wǎng)卡作為了局域網(wǎng)上一張普通的網(wǎng)卡來(lái)看待,并沒(méi)有刻意區(qū)分流量是本機(jī)發(fā)出的還是從其它機(jī)器發(fā)出的,因此,如果你只是想將bridge作為一個(gè)二層設(shè)備,那么可以斷開(kāi)Hook-ethX-low和Hook-ethX-upper之間的邊即可,netgraph實(shí)現(xiàn)的bridge,你看不到虛擬設(shè)備,這種實(shí)現(xiàn)更純粹,偉大的BSD將這種思想帶給了其衍生出來(lái)的Cisco IOS。
下面是bonding的實(shí)現(xiàn)邏輯:

由于bonding網(wǎng)卡大多數(shù)負(fù)責(zé)的是本地IP層發(fā)出的數(shù)據(jù),需要和路由轉(zhuǎn)發(fā)表相配合,因此需要有一塊虛擬網(wǎng)卡,這個(gè)是通過(guò)ng_eiface的構(gòu)造函數(shù)ng_eiface_constructor實(shí)現(xiàn)的。依然無(wú)其它話可說(shuō)。
以上兩個(gè)圖展示了netgraph的魅力,既然這樣,也就可以依照這種方式實(shí)現(xiàn)VLAN,IPSec等了,要比Linux的Netfilter加設(shè)備驅(qū)動(dòng)模型的實(shí)現(xiàn)方式更“可插拔”,有netgraph,F(xiàn)reeBSD可以將所有的協(xié)議處理在一張張的graph中進(jìn)行,數(shù)據(jù)包在graph中游歷在每一個(gè)Node被接收到的hook處理,主要你能根據(jù)協(xié)議處理邏輯構(gòu)建好一張圖,將這張圖掛接在協(xié)議棧,甚至掛接在驅(qū)動(dòng)上,你就能很方便的實(shí)現(xiàn)網(wǎng)絡(luò)的任意擴(kuò)展...
最后看一下netgraph的依賴關(guān)系,在netgraph中,每張圖都是相對(duì)獨(dú)立的,數(shù)據(jù)包從某處進(jìn)入一張圖A,然后從某處出來(lái),在另一處再進(jìn)入圖B,此時(shí)它將不能再使用圖A。這和Netfilter不同,Netfilter基于HOOK設(shè)計(jì),使用一些match來(lái)進(jìn)行filter,比如NAT就需要ip_conntrack,ctdir需要ip_conntrack等等,ip_conntrack一直都面臨table full的問(wèn)題,因此你要用raw表的NOTRACK這個(gè)target來(lái)免除追蹤不感興趣流量來(lái)緩解這個(gè)問(wèn)題。有下面的需求:
從網(wǎng)段M發(fā)出到網(wǎng)段N的流量(兩個(gè)方向)打上tag待策略路由來(lái)處理,從網(wǎng)段N發(fā)出到網(wǎng)段M的流量(兩個(gè)方向)不打tag。
分析:
很顯然要使用ctdir這個(gè)match,否則將會(huì)過(guò)濾掉返回流量,于是有以下target為NOTRACK的match:
!dst N/interface $內(nèi)網(wǎng)口
然而意味著從網(wǎng)段N發(fā)出到達(dá)M的返回流量也將被conntrack,這是因?yàn)閏tdir和conntrack相互依賴才導(dǎo)致了這樣的問(wèn)題,在raw表中,你甚至都不知道數(shù)據(jù)包到底是走INPUT還是FORWARD,所以你很難讓所有這一切關(guān)聯(lián)起來(lái),雖然conntrack可以保持一個(gè)流信息在內(nèi)存中,但是卻可能存在大量不相關(guān)的流也被保存。如果使用netgraph呢?很簡(jiǎn)單,我們可以寫在兩個(gè)命令中:
No.x check-status
No.z skip No.y from N to M #對(duì)于返回流量,只檢測(cè)conntrack
No.y netgraph tag from M to N keep-status
如此即可。FreeBSD不需要conntrack,它內(nèi)建了一個(gè)動(dòng)態(tài)ruleset,凡是keep-status的流量都將自動(dòng)將返回流量加入動(dòng)態(tài)ruleset中,實(shí)際上也就是“保持了一個(gè)流信息在內(nèi)存中”,F(xiàn)reeBSD的conntrack和單獨(dú)的rule相關(guān)聯(lián)而不是和整個(gè)協(xié)議棧關(guān)聯(lián),這實(shí)際上也是netgraph的思想,我們看一下rule相關(guān)的conntrack和協(xié)議棧香瓜的conntrack的區(qū)別:
IPFW:沒(méi)有全局的conntrack信息,然而需要查詢動(dòng)態(tài)ruleset,以匹配返回流量;
Netfilter:需要查詢?nèi)值腸onntrack表,可以取出一切頭包經(jīng)過(guò)時(shí)流量的匹配結(jié)果,不需要也沒(méi)有動(dòng)態(tài)ruleset
我們看一下全局的conntrack和全局的ruleset所針對(duì)的對(duì)象有何不同。很簡(jiǎn)單,全局的conntrack針對(duì)除了NOTRACK的所有的數(shù)據(jù)包,然而如果NOTRACK需要指明方向,就會(huì)需要循環(huán)依賴,問(wèn)題將無(wú)解。全局的動(dòng)態(tài)ruleset僅僅針對(duì)匹配到的數(shù)據(jù)包,對(duì)其它的沒(méi)有匹配到的數(shù)據(jù)包除了一個(gè)查詢性能影響之外沒(méi)有其他影響,事到如今,我想查詢性能應(yīng)該不是問(wèn)題吧,再說(shuō)動(dòng)態(tài)ruleset一般都比全局conntrackset小得多,查詢conntrackset都不怕,查詢動(dòng)態(tài)ruleset就怕了么?換句話說(shuō),Netfilter的ip_conntrack是寧可枉殺一千,不能使一人漏網(wǎng),而ipfw則是精確的匹配。效率啊,BSD不愧是網(wǎng)絡(luò)領(lǐng)頭軍!
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號(hào)聯(lián)系: 360901061
您的支持是博主寫作最大的動(dòng)力,如果您喜歡我的文章,感覺(jué)我的文章對(duì)您有幫助,請(qǐng)用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點(diǎn)擊下面給點(diǎn)支持吧,站長(zhǎng)非常感激您!手機(jī)微信長(zhǎng)按不能支付解決辦法:請(qǐng)將微信支付二維碼保存到相冊(cè),切換到微信,然后點(diǎn)擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對(duì)您有幫助就好】元
