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

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

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

該圖中有兩種元素,一種是節點,另一種是連接到節點的邊的兩端的頂點。在netgraph的術語中,節點就是Node,而頂點叫做hook,一條邊連接兩個hook,hook通過CONNECT/MKPEER構成一條邊。從上圖中可以看出,一條邊的兩端必然有兩個hook,從命名上可以看出這些“邊的端點”其實就是真正處理數據的地方,而Node其實就是一個“數據+操作”的封裝,一個Node可以有多個hook,通過這些hook連接到其它的Node。
我們可以用OO的思想來理解這些個netgraph的概念,Node就是一個對象,每一個Node都有它所屬的Type,可以將Type理解成類。而hook其實就是一個Node對象的私有數據,整個graph通過“各個hook的對接”來完成,FreeBSD提供了豐富的命令來完成netgraph的構建,說白了其實就是以下幾步驟:
1.生成一系列的Node對象;
2.為每一個Node定義一個或多個hook;
3.將特定的Node通過hook連接在一起。
如此一來整個graph就構建好了,FreeBSD提供了struct ng_type,它便是代表了一個類,然后你每生成一個特定ng_type的實例就相當于生成了一個對象,通過對該結構體里面的一些字段的理解,我們就可以完整理解數據包在這個graph中的游歷過成了。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 */ };
注釋很清楚了,自不必說,如果我們看看其中一些回調函數的定義,就更能理解了。“構造函數”和“析構函數”都有,每一個“成員函數”的參數列表的第一個參數類型都是node_p,這難道不是this么?這里唯一要注意的就是rcvdata回調函數,該函數接收從另一個Node發送過來的數據,接收者是hook,而不是Node,再次強調,Node之間通過hook相連接,而不是通過node本身,然而每一個hook都要唯一綁定一個Node對象,因此我們可以從hook解析出唯一的Node對象,卻不能從Node中直接得到hook(一個Node對象擁有N多hook呢),要分清一對一和一對多的關系。因此rcvdata的第一個參數是hook_p就是合理的了。
Node和Node之間通過hook傳遞控制信息,而網絡數據包則是通過一個hook向其peer hook發送消息的方式完成的,當然所謂的發送消息大多數情況下就是函數直接調用。既然一條邊兩端有兩個hook,那么每一個hook就有一個peer,每當我們將數據包發送到一個hook的時候,實際的效果就是數據包被發送到了該hook的peer,這是netgraph的核心邏輯實現的,我們可以從下面的這個核心宏中看到這一點:
#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的定位,這個peer可以通過ngctl命令來設置。
就這樣,一個數據包在整個netgraph中通過“離開一個Node的某個hook,進入另一個Node的某個hook的rcvdata”的方式游歷,Node在這里的作用就是封裝私有數據和統一的操作,當然,你可以重載掉一個Node內統一的rcvdata回調函數,而是為每一個hook都設置一個私有的rcvdata回調函數,再次強調,是hook在rcvdata,而不是Node在rcvdata,Node的rcvdata是一個該Node所有hook通用的回調函數,如果沒有hook私有的rcvdata,該通用函數將被調用,ng_snd_item最終將進入下面的邏輯:
if ((!(rcvdata = hook->hk_rcvdata)) && (!(rcvdata = NG_HOOK_NODE(hook)->nd_type->rcvdata))) { error = 0; NG_FREE_ITEM(item); break; }
由此看出,Node有一個默認的對所有hook都適用的rcvdata回調函數,然而各個hook可以重載掉這個默認的rcvdata回調函數。
接下來我們看一下netgraph如何和協議棧對接,不要把操作系統想得太神奇,實際上完成這種工作只需要一個回調函數即可。以以太網接收為例,以太網接收處理函數中會調用ng_ether_input_p回調函數,你只需要將其定義一下即可,對于很多場合都使用的ng_ether,它將此函數定義為:
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 */ }
最后通過NG_SEND_DATA_ONLY將數據包發送給priv->lower這個hook,最終數據包會進入priv->lower的peer,調用priv->lower->peer的rcvdata回調函數,在一切開始工作之前,你首先需要構建好整個graph。對于以太網發送函數,也有類似的_p回調函數。
netgraph和Netfilter的區別在于它可以將graph“掛接”在特定的interface上,而Netfilter卻把HOOK直接掛在協議棧本身,interface在Netfilter中只是一個match。如此一比較,效率差異就很明顯了。以以太網為例,在ether_input中就會調用netgraph,如果加載了ng_ether的話,就會調用下面的函數:
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) //如果這塊網卡上沒有任何hook,將不作處理直接返回。 return; NG_SEND_DATA_ONLY(error, priv->lower, *mp); /* sets *mp = NULL */ }
如果本ifp上沒有掛接任何graph,則直接返回標準協議棧處理,如果掛接了一個graph,則數據包將進入該graph,你可以將firewall rule配置在此graph里面。對于Netfilter而言,在網卡接收這一層,沒有任何HOOK,只有到了IP層,才會進入PREROUTING/INPUT/FORWARD...等HOOK,哪怕你配置了一條rule,所有的包都將接受檢查以確定是否匹配,在Netfilter的rule中,所謂的interface只是一個match。
需要說明的是,netgraph也可以像Netfilter那樣工作,你只需要將其掛在ip_in(out)put上即可。
我們給出兩個例子來看看netgraph如何實現bridge和bonding,這些在Linux上都是通過虛擬net_device來實現的,其發送邏輯都是該虛擬net_device的hard_xmit實現的,而其數據接收邏輯則是硬編碼在netif_receive_skb中的,bridge是通過handle_bridge這個硬編碼hook進入的,而bonding是通過skb_bond來實現的。也就是說Linux是通過對既有的協議棧進行硬修改來實現的,而netgraph則不需要這樣,對于FreeBSD,我們只需要構建一張graph就可以實現bridge或者bonding,首先我們先看看bridge的實現邏輯,如下圖所示:

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

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

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