對於不了解內核的,特別是內核網絡的人來說,內核的網路處理就像一個巧克力盒子.不打開就不會知道裡面是什麼,打開了就會覺得裡面是豐富多彩的.
本文試圖從一個原始數據包處理流程的角度,結合源代碼(相應的函數)簡單扼要地分析FreeBSD的內核網絡處理.
主機對主機的方式是比較簡單的,數據包從鏈路層上來,一路上行,達到用戶空間的應用程序,一個數據包的生命期就結束了.對於像網關或防火牆之類包轉發的方式,處理起來就相對復雜了一些,這也是許多人迷惑不解之處.
上面是開場白,接下來就轉入正題.
老規矩,先建立場景,場景總是要假設並建立起來的.設:
hostA -- GW -- hostB
主機A通過GW互訪hostB
談到數據的通訊,總是雙向的,如同2人談話,如果僅僅是一個人說,那就成了演講--廣播.GW就是扮演了一個傳遞員的角色,2人的話傳來傳去,粗俗的話,優化的GW或防火牆十有八九是不傳的,免得制造矛盾.
對於主機如何產生包,本文不作詳細討論.關心此項內容的,可以參見TCP/UDP處理以及內核中的socket等系統調用.本文的重點放在GW上,分析GW是如何處理轉發數據包的.
hostA想要訪問hostB的FTP(21端口):
0. 先廣播詢問並獲得網關的MAC地址.誰是網關,速速報來!!!
1. 連接hostB的FTP端口
2. 成功後,發送數據包
....
hostA找到網關的MAC地址後,發往非本網段的數據包的目標MAC地址都是網關的MAC地址,但目標IP地址不是網關的.
下面就看看GW都作了哪些工作
1. GW聽到一個包
NIC <-- 硬中斷發生了
| 調用驅動的rxeof函數.包處理開始.對於polling
| 方式,是CPU主動去網卡讀包,這樣硬中斷數會少,
| 但是如果處理不及時,數據包就丟了.對於小包,而
| 且網卡芯片上的buf很大時,polling方式的好處就很
| 大了.反過來,在遭受小包攻擊時,系統的中斷數就
| 會異常高,這是因為需要不停地響應處理.
|
if_xxx.c <-- rxeof
| m_devget申請mbuf,從網卡的buf拷貝數據到mbuf,
| 一個數據包出現.剝離ether_header後,調用
| ether_input(ifp, eh, m)
|
if_ethersubr.c <-- ether_input:
a. 一定要獲取ether_header,拿不到就釋放mbuf
丟掉這個包.
後續的處理中,該數據包隨時面臨著被丟棄的危險
b. bpf想要看看這個包,那就給他看看,反正他不會
更改這個包,tcpdump可以通過bpf看到這個包
c. netgraph也要處理嗎,嗚,處理就處理,不怕.
netgraph是FreeBSD獨特的網絡處理進程,並移
植到了其他BSD,這裡是一個鉤子,掛接在驅動
層可以處理最原始的數據包.
正常的鉤子入口在 ng_ether中.
d. 是網橋模式嗎?如果是的話,數據包就從這裡轉
到另一網卡的發送隊列中了.參見bridge.c
預處理作完了,該ether_demux(ifp, eh, m)出場了
<-- ether_demux:
開始為IP預處理
a. 這個包需要流量控制嗎?先轉到ipfw再處理它
b. 這個包是我的嗎?上層准備接收了嗎,否那就丟
棄這個包
c. 如果是多播,就置位多播,告訴上層是多播
預處理就要結束了,根據包類型,分揀到不同的上層隊列中
----------------------------------------------------------------------
說完了2層的處理,下面就是3層的了.文件的目錄也就從dev(pci),net轉到netinet.
2. 三層--arp處理
if_ether.c <-- arp的處理
首先出場的是arpintr,看名字知道是處理中斷的.
從隊列中取出一個包,不管三七二十一,看看包頭,
注意這時的包已經沒有ether_header了.如果是arp
類型的包,並符合處理要求,轉到in_arpinput(m).
當然如果不合規矩照丟不誤.
<-- in_arpinput(m)
針對各種情況判斷處理,其中會調用arplookup
判斷處理後,發送reply.將路由指針rt置NULL,
調用ether_output,雖然調用的是if_output,但大
多數網卡驅動都將此函數指針設為ether_output.
這時,數據包就回到了2層,發送回去了.之所以,
用"回到",因為表面上看來是這樣的,還是相同的
mbuf,只是內容不同了.arp的請求應答包是對稱的.
<-- arplookup(addr, create, proxy)
完成arp的緩沖,將此MAC地址放到rt路由表中,以備
將來發送包時查詢使用.
這個文件中還有一個重要的函數 - arpresolve,用於通過IP地址獲取MAC地址,如果在rt樹中沒有找到(或超時了),就調用arprequest,廣播獲取與此IP對應的MAC地址.
系統命令arp就是通過ioctl和這個文件打交道.
3. 三層--IP處理
ip_input.c <-- 流入網關的IP處理
ipintr,自然就是IP隊列的中斷處理了,它的任務很
簡單,從隊列中取出一個mbuf,也就是一個數據包.
將其交給ip_input處理.
<-- ip_input
a. 先判斷要不要進行ipfw等的處理,是的話,跳轉c
處理
b. 接下來,拿到IP頭,針對IP頭判斷處理
c. ipfw和ipfilter開始處理
在ipfw和ipfilter中,這個包可能會被丟棄,
轉發,這時流入包的處理就會到此結束
d. 經過了包過濾的開包流檢,開始處理IP選項,
當然了多播也不要忘了處理一下
e. 判斷一下,是送給自己IP的嗎?如果不是,要不
要調用ip_forward,傳出網關?
f. 看來需要傳遞給上層處理了,根據不同的協議
TCP/UDP,調用位於4層的協議處理函數,該他們
干活了.
<-- ip_forward
這是該文件中另一個重要的函數
該函數,會根據目標地址,查找路由,如果找到路由了,
就調用ip_output,將數據包轉發走,否則回應一個
ICMP,告訴發送方出錯了.
真不容易,這個數據包經過了重重關卡,終於要繼續前進,准備出城了.且慢,出城也不是那麼容易了,這比乘火車坐飛機的安檢嚴多了.真是寧可錯殺一千不漏一個.
ip_output.c <-- 流出網關的IP處理
ip_output,IP流出的處理主體函數,處理的方式類似
包流入的處理,先是
a. 先判斷要不要進行ipfw的處理,是的話,跳轉d
處理
b. 嗯,要判斷是不是來自4層,看看是否要處理一下
IP頭
c. 看看路由表,這個包該何去何從?不要忘了多播喲!
當然了,如果是IP的廣播包,也要處理的.
例如PPPOE會發送IP的廣播包
d. 又開始ipfw和ipfilter的處理了
e. 對於loopback的包,怎麼能放出去呢,丟掉它
f. ip包DF了嗎,包太大又不讓分拆的話,只好對不
起了,丟棄它.否則拆分它,形成mbuf簇,每個
簇由多個鏈構成.ip_fragment做的就是這件事
包轉發幾乎涉及不到包重組.
g. 到此,終於可以通過if_ouput -- ether_output
將包傳送到了二層
----------------------------------------------------------------------
在三層上,是各種安全處理的最佳地點,這時候,原始的包該處理的都處理,剩下的就是怎麼根據IP完成各種各樣的規則處理了.在這一層,數據包可以被還原為一個發送方的IP包,並能夠進一步解包成TCP/UDP,形成會話甚至應用.由於分層的結構,采用SMP對包作進一步處理時,並不會對下層造成很大的影響(mbuf處理不及時,造成mbuf耗盡等等)
4. 二層--ether_output
if_ethersubr.c <-- ether_output:
a. 需要判斷路由?那就看看,不合適的話就丟棄這個包
b. 看看arp表,有目的地址的MAC?沒有就去要一個回
來,沒要來?那就返回吧,出不去了
c. 添加ether_header
d. 什麼,目標地址是自己,if_simloop這個包
e. 看看netgraph要處理嗎?
f. 將包轉給ether_output_frame繼續處理
<-- ether_output_frame
a. 網橋要處理嗎?
b. ipfw還要處理一下?
c. 都處理完了吧,那就把包送到網卡的輸出隊列中吧,
等候網卡驅動處理了
if_xxx.c <-- xxx_intr
網卡設備的中斷處理,負責發送接受等工作
<-- if_start
從隊列中取出包,調用xxx_encap,將包轉換為frame
最後再看一眼bpf.
----------------------------------------------------------------------
### if_simloop在if_loop.c文件中
千辛萬苦,數據包終於走出了網關.
網絡處理程序的分支非常多,但是只要抓住主線,就會非常清晰其處理流程.其中涉及
到的處理函數也就那麼幾個.
其中涉及到的數據結構也非常得多,隊列,mbuf鏈(簇),ifp,rt等是非常重要的數據體,很多時候如果不清楚這些結構,讀懂這些程序是非常困難的.同時針對某協議的封裝格式也要了解清楚,TCP/UDP->IP->mbuf,層層封裝的,不要僅僅是停留在書本上.