天天看點

FreeBSD之netgraph簡要解析

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不愧是網絡領頭軍!

 本文轉自 dog250 51CTO部落格,原文連結:http://blog.51cto.com/dog250/1268981

繼續閱讀