Loopback實際上是個hole
但是如果它不是一個hole,它確實可以做一些事,類似Cisco的NVI那樣。既然前期是“如果它不是一個hole”,那就需要對代碼進行一些修改。在修改之前,你必須明白的是,Linux的loopback介面為什麼是一個hole。
標準規定,所有試圖經過loopback介面去往其它地方(非本機)的資料包要全部丟棄。Linux使用loop hole做到了這一點。Linux的限制loopback流量在本機範圍的方式是,所有的loopback流量肯定經由本機發送,那麼在ip_output的時候就會將其設定為loopback_dst,然後進行入IP接收常式的時候,它已經有關聯的路由項了,進而就不會再去查詢路由表,因此凡是進入ip_input邏輯的資料包都不是本機發出的,於是在其內部就可以做比較狠的判斷的,凡是源地址是本機地址的,一律丟棄!這樣本機發出的包就不會先經由loopback口然後去往外部,下面我們看一下外部進入的包是否能經由loopback口去往外部。答案無疑是否定的,看下面的流程:資料包從物理網卡進入->被路由到lo口->將loopback_dst這個路由項關聯給資料包->loopback介面xmit資料包->類比loopback介面接收資料包->進入ip_input路由判斷->由於已經有了路由項故按照路由項轉寄。路由項的轉寄方式有兩種,對於外部進入的資料包,將不斷調用ip_forward,直到TTL變為0。因此只要進入了loopback,要麼直接丟棄,要麼瘋狂loop,是絕對出不去的。
下面我就來說一下如何來破除這些約束。首先說一下本機發出的資料包如何先經由loopback再出去,然後說明外部進入的資料包如何先經由loopback再出去,最後說明,當做NAT的時候會碰到什麼問題以及如何結合上述針對本機發包以及外部發包兩種情境的措施來解決NAT問題。
1.本機發包經由loopback發出
修改代碼是不必可少的了,因為我這是在破壞原則。幸運的是,代碼只是修改一點點而已。修改的部分就是將這種“經由loopback發往別處”的包識別出來,然後刪除其關聯的路由項。這個用Netfilter在PREROUTING上做比較簡單。另外就是將表示該本機地址的Local路由從Local表刪除,然後作為unicast路由加入main表中,這樣在做反向路由查詢的時候,就不會匹配到Local表的路由了(Linux要求反向路由的類型必須是unicast的),到此即OK!
2.外部發包經由loopback轉寄
對於這種情況,只要是刪除了資料包的loopback路由項關聯,即可被順利轉寄。因為資料包的源IP地址不可能是原生IP,因此也就不可能是Local,如果資料流想原路返回的話,它就一定有反向的unicast路由。
3.NAT的問題
在配置了SNAT的情況下,要看SNAT成了什麼地址,如果是SANT成了本機地址,那就面臨上述第1節的問題,解決方案就是將該地址從Local表中刪除,但是刪除了之後會導致其它機器arp該地址的時候,本機不再回複,因此刪除了之後還要顯式arping一下該地址的arp更新;如果SNAT成了別的地址,就涉及到了反向可達性的問題,因為下一跳不一定知道該地址的可達性。
4.NAT問題的解決
NAT的問題僅僅是在SNAT成了別的地址時才會存在。這裡又分為兩種情況,第一種情況就是SNAT成了一個不相關的其它網段的地址,這樣僅僅要求下一跳配置到該地址的路由就可以保證資料流的反向包能返回到此BOX,這個路由配置在簡單環境下可以手工配置,複雜環境下可以用動態路由的方式進行SNAT地址的宣告;第二種情況就是SNAT的地址是和下一跳同一網段的情況,這會導致資料流反向包返回到下一跳的的時候,該SNAT的地址此時成了目標地址,由於處於同一網段,所以會被直接ARP,因此需要添加一條ARP轉換規則:
arptables -t mangle -A OUTPUT -d 下一跳網關地址 -j mangle --mangle-ip-s SNAT成的地址
知道了問題所在以及解決方案,現在就可以動手了。本文的目標是實現一個類似Cisco NVI的東西,也就是一個虛擬網卡,在虛擬網卡的發送流程中實現NAT。鑒於有loopback這麼好的現成的東西,我也就不再寫虛擬網卡了,直接用loopback類比一個也好。大體流程如下:
資料包從物理網卡進入->執行DNAT->路由到loopback->執行SNAT->loopback口發出->策略路由->物理網卡發出
可以看到,路由執行了兩次,第一次是為了NAT,第二次是真正的路由。
除了使用loopback,編寫一個類似veth的虛擬網卡是一個更不錯的選擇:
Veth stands for Virtual ETHernet. It is a simple tunnel driver that works at the link layer and looks like a pair of ethernet devices interconnected
with each other.
比loopback好的是,這基本可以不修改代碼實現NVI,並且可以很容易取到資料包原始的進入介面。該驅動的邏輯非常簡單,即一個pair中包含一個主介面和一個輔助介面,資料包從主介面進入被路由到該主介面的輔助介面,注意,不改變skb的接收介面,這個所謂的路由只是為了搞一次“從物理網卡接收到發送到某另一個網卡的動作”,此時PREROUTING/POSTROUTING都已經完成了,真正的路由之後就可以從另一個主介面發出了。
這次先不急著自己寫虛擬網卡,先折騰完loopback再說,那麼現在動手吧!
1.對代碼的修改:
重新封裝RAW表的NF_INET_PRE_ROUTING鉤子函數,在ipt_hook的調用前調用下面的邏輯:
//判斷有點太魯莽,正常應該可以設計成一個匹配演算法的
if (skb->dev->flags & IFF_LOOPBACK && skb->nfct) {
skb->nfct = &nf_conntrack_untracked.ct_general;
skb->nfctinfo = IP_CT_NEW;
nf_conntrack_get(skb->nfct);
skb_dst_drop(skb);
return NF_ACCEPT;
}
這段代碼的意思是說,如果是資料包從物理網卡進入,顯然是需要匹配和應用規則(比如NAT)的,如果這件事做完了,資料也就是要通過路由匯入loopback介面了,此時就不要再使用conntrack了,然而此時skb的nfct可能已經被設定了,於是將其NOTRACK,並且將skb的路由緩衝丟棄。Linux的IP路由是這麼對待loopback的,如果路由查詢的結果出口是loopback介面,就是直接設定dst,loopback的xmit將資料包發出,調用一個netif_rx重新接收,到達ip_rcv_finish的時候,由於已經有了dst,就不必再查詢路由了。但是如果這樣的話,我們的第二次路由查詢-實際上是策略路由的查詢將無法實現,因此必須drop掉原有的dst。