一直以來,一直對Linux的NAT很不滿,也寫過《Linux系統如何平滑生效NAT》系列文章中的patch進行修補,還寫過一些類Cisco實作的patch,然而都效果不大好,暴雨的夜晚,長假的倒數第二晚,雖然沒有10月7日晚雨量大,可是10月6日晚上到7日淩晨,上海嘉定那邊的雨也可以堪稱暴雨了。一直想看卻一直沒有時間看的《斯巴達克斯 第三季》終于看完了,雨越大越興奮,可是巴拉巴西的《連結》也看完了,《羅馬人的故事》最後一卷也看完了,《黑天鵝》還沒有到貨,剩下的隻有寫點代碼了...于是半瓶竹葉青陪我到天蒙蒙亮,修改了幾個核心代碼檔案,debug了幾小時,小睡了兩小時後,起床去買新鮮的肉類和蔬菜以及海鮮,因為長假最後一天要一起在家吃火鍋。火鍋很爽,外面大雨如注,屋裡熱氣騰騰...就這樣,雨一直下到第二天早上。
10月7日第一天上班,我正常出門,可是到了公司已經12點多,在上地鐵的時候,涉水到膝蓋,轉彎,突然發現暗黃色漂浮物,臭氣迎面而來,素不相識的一行人為了安全走在了一起,我在頭陣...聽說是附近的廁所出問題了,污水糞便就從地下湧了上來...繼續前行,停下,還是回頭?如果隻有我一人,我肯定回頭了,然而後面有倆MM,還挺時尚漂亮,都說要過去,然後再洗,另外一位本來應西裝筆挺的正裝哥們兒由于褲子太合身無法挽起,執意也要先走過去再說...我如此邋遢的該如何,可想而知...真心不想趟這渾水啊!!...
這麼多和工作無關的瑣事,我太羅嗦了。步入正題了!
Linux的NAT實作是基于ip_conntrack的,這句話已經不知道說了多少遍。一切均實作在Netflter的HOOK函數裡面,其邏輯一點也不複雜,然而有意個小小的要點,那就是:即使沒有比對到任何的NAT規則的和NAT無關的資料流,也要針對其執行一個null_binding,所謂的null_binding就是用其原有的源IP位址和目标IP位址構造一個range,然後基于這個range做轉換,這看似是一個無用的東西,其實還真的有用。
用處在哪裡呢?注意null_binding隻是不改變IP位址,其端口可能要發生改變。為何要改變和NAT無關的資料流的端口呢?因為和NAT有關的資料流可能為了五元組的唯一性已經将和NAT無關的資料流的某個端口給占用了,這就影響了和NAT無關的資料流五元組的唯一性。由于ip_conntrack是不區分是否和NAT有關的,而NAT操作要改變五元組,為了整個conntrack的五元組都是唯一的,哪怕隻有一個資料流執行了NAT,也可能占用了某個其它資料流的五元組要素,進而引發連鎖反應,是以全部要執行唯一性檢測和更新,alloc_null_binding就是為了做這個操作。
要是沒有深入研究過Linux的NAT,隻是僅僅會配置它的話,也許你還真的不知道NAT規則隻對一個流的第一個起作用,确切的說,是隻針對一個流的ip_conntrack結構體剛剛建立還沒有confirm的時候起作用,因為有時ip_conntrack結構體會過期。隻要這樣的包離開了協定棧,流就被confirm了,接下來的屬于同一個流的其它資料包就直接使用上述那個包的儲存在ip_conntrack結構體中的NAT結果了。
正是由于這個特點,使得你無法中途添加NAT規則使之立馬生效或者修改已有的NAT結果。這種有狀态的特性帶來了很多的問題。之前寫過《Linux系統如何平滑生效NAT》系列文章,做過一些修正更新檔。然而那些更新檔的問題在于:它們還是基于流頭比對NAT規則的小修小補。我們知道,這種小修小補最終的結果就是不可維護,那麼何不來一個颠覆,即,不再采用流頭比對NAT的原則,改為想什麼時候比對就什麼比對的原則。這其實是一種更高層次的颠覆,即流頭比對原則是新的比對原則的一種特例。
廢除了流頭比對原則後,我決定把何時執行NAT的決定權留給應用程式,是以我決定注冊一個sysctl變量,當其非0時執行NAT,不管是不是已經confirm了。
既然說流頭比對原則不好,會帶來問題(比如confirm的連接配接由于沒有NAT而僵持在那裡的問題),那麼肯定要指出何時執行NAT比對是必要的,這叫有破有立。在以下的情況下,執行NAT是必要的:
1.資料流連接配接時,由于還沒有做NAT而導緻久久連不上的情形。此時資料流的CT狀态依然是NEW;
2.資料流已經成功連接配接,但是需要改變一下源位址(改變目标位址意味着重新連接配接一個新的服務)。此時的資料流的TC狀态是ESTABLISHED;
3.資料流已經經過NAT連接配接,但NAT規則改變了。此時的資料流的TC狀态是ESTABLISHED;
并不是所有的以上情況都适合執行NAT比對進而執行NAT,我們不光要考慮雙向五元組标示的ip_conntrack本身,還要考慮協定本身的語義。我們看一下TCP協定,由于TCP嚴格根據五元組維持一個既有的連接配接,修改任何因子都意味着連接配接不複存在。是以:
1.對于TCP之類的有連接配接4層協定而言,隻有NEW狀态的資料流才能執行NAT,非NEW狀态意味着已經收到目标的回報,執行NAT沒有意義;
2.一個流的其中一個資料包已經做好了NAT,并且NAT規則沒有改變的情況,此時反向五元組已經被改了,沒有必要每次都去比對一遍NAT規則表;
對于能做的事情,一般而言你不做也可以,就是你可以做也可以不做,但是對于不能做的事情,基本就是嚴禁了,如果你做了,就會帶來嚴重的後果或者即使沒有嚴重的後果也完全是無用功,世界就是這麼的不對稱,有時點到為止,總是功不抵過!是以對于以上兩個小節,‘什麼時候需要比對NAT規則’中的一些點,我把控制權交給了應用程式,是以導出了一個sysctl接口,而對于‘哪些情況不能執行NAT’中的情形,則由核心來控制。
以上的所有落實下來的話就是代碼了,我沒有将标準的patch貼到文章,因為那是打patch的時候給程式看的,如果讓人看,一大堆的+++---的肯定很擾亂視線,是以我換了一種方式,即//////////////////////////包圍的為我添加的代碼段,/////////////########包圍的為我修改的代碼段。本小節的結構為:
{{檔案名\n代碼段\n總體說明},...}:
include/net/netfilter/nf_nat.h
說明:增加了一個新的CT狀态,用來訓示是否要做NAT比對。
include/net/netfilter/nf_conntrack_l4proto.h
說明:nf_conntrack_l4proto結構體增加了一個can_force_nat回調函數,将判斷是否能重新執行NAT的決定權交給4層協定自己而不是在ip_conntrack以及nat邏輯中為之代勞。
net/netfilter/nf_conntrack_proto_tcp.c
說明:添加了nf_ct_can_force_nat回調函數,訓示在ESTABLISH狀态不能重新執行NAT。
net/ipv4/netfilter/nf_nat_standalone.c
說明:為NAT的Netflter的HOOK函數添加何時執行NAT的判斷邏輯。
net/ipv4/netfilter/nf_nat_rule.c
說明:nf_nat_rule_find中如果沒有找到規則,則判斷是否是将已有規則删除了,進而恢複原始狀态。
net/ipv4/netfilter/nf_nat_core.c
說明:在nf_nat_setup_info中判斷如果是強制重新執行confirm狀态的流的NAT,則重新将其修改過的反向五元組入哈希表。
1.以上的代碼修改過以後,make,insmod...;
2.ping或者telnet一個不存在的位址;
3.加載iptables規則實作DNAT,将不存在的位址轉換為一個存在的位址;
4.echo 1 >/proc/sys/net/ipv4/netfilter/nf_force_nat
5.通了嗎?
6.删除那條iptables NAT規則,icmp不通了,telnet仍然通。
同樣的方法測試SNAT。
即時這個patch已經朝着perfect前進,它依然無法解決在Linux上簡單配置雙向靜态NAT的問題,它解決的隻是随時NAT的問題。那麼怎麼去支援雙向靜态NAT呢?目前有一種辦法(除了之前寫過的那個辦法之外)。
即完全啟用nat extension,在添加靜态NAT規則的時候,用nat後的已經修改的反向二進制組(源/目标IP位址)和正向二進制組構造兩個個虛拟的nat_conntrack,并将兩個二進制組插入一個專門的NAT哈希表,這樣不管資料從哪個方向發起,在靜态NAT的HOOK邏輯(即nf_nat_fn)中,直接去根據自己的源位址去查NAT哈希表,如果找到則取出其反向二進制組使用其中的非any位址覆寫nf_conntrack反向五元組的對應位置即可。
以上設計的本質在于,既然基于matches無法實作雙向靜态NAT,那麼為何不掃除match呢?我們需要的僅僅是下面的推導:
SNAT: 源:A==>源:C
正向: 源A->目标X
反向: 源X->目标C
||
\/
DNAT: 目标C==>目标A
正向: 源X->目标C
反向: 源B->目标X
資料結構如下:
ENUM dir {
orig,
reply,
}
tuple {
address[dir] addrs
nat_conntrack {
tuple[dir] tuples;
兩個方向的tuple均加入哈希表,永遠用正方向的IP二進制組去查找,然後取出反向二進制組使用。如果以上兩個tuple都能在配置NAT規則的時候加入系統,則資料包在nf_nat_fn中就可不用Ipt_do_table調用去比對NAT規則了,隻需要:
1.如果是PREROUTING,則用自己目标IP位址去查詢nat_hash,找到tuple後擷取對應的nat_conntrack,進而得到反向tuple,然後用反向tuple的源IP位址覆寫掉ip_conntrack的反向五元組的源IP,然後alert reply tuple即可;
2.如果是POSTROUTING,則用自己的源IP位址去查詢nat_hash,找到tuple後擷取對應的nat_conntrack,進而得到反向tuple,然後用反向tuple的目标IP位址覆寫掉ip_conntrack的反向五元組的目标IP,然後alert reply tuple即可。
本文轉自 dog250 51CTO部落格,原文連結:http://blog.51cto.com/dog250/1308291