(九)洞悉linux下的Netfilter&iptables:網絡地址轉換原理之DNAT

網絡地址轉換:NAT

     Netfitler爲NAT在內核中維護了一張名爲nat的表,用來處理所有和地址映射相關的操作。諸如filter、nat、mangle抑或raw這些在用戶空間所認爲的“表”的概念,在內核中有的是以模塊的形式存在,如filter;有的是以子系統方式存在的,如nat,但它們都具有“表”的性質。因此,內核在處理它們時有很大一部操作都是相同的,例如表的初始化數據、表的註冊、鉤子函數的註冊等等。關於NAT表的初始化模板數據和表的註冊流程並不是本文的重點,大家可以對照第四篇博文中filter表的相關分析來研究。本文還是側重於從整體上對整個NAT子系統的設計思想和原理進行,當然,有時間我還是會簡單和大家分析一NAT表的東西。因爲最近確實太忙了,本來想着在四月份結束這個系列,無奈一轉眼就晃到了五月份,做IT的娃,都不容易啊!

     通過前面的幾篇文章我們已經知道,NAT的設計是獨立於連接跟蹤系統的,即連接跟蹤是NAT的基礎框架,我們還瞭解到連接跟蹤不會修改數據包,它只是負責維護數據包和其所屬的業務會話或數據連接狀態的相關信息而已。連接跟蹤最終是被iptables模塊所使用的,它所定義的那些狀態信息如NEW、ESTABLISHED、RELEATED等等,NAT統統不用關心。

     根據前面的hook函數掛載圖我們可以清晰的知道,對於那些需要被本機轉發的數據包,註冊在NF_IP_PRE_ROUTING點的ip_nat_in ()函數完成對其目的地址轉換的操作,註冊在NF_IP_POST_ROUTING點的ip_nat_out()函數完成源地址轉換任務。如果在編譯Linux內核源碼時打開了CONFIG_IP_NF_NAT_LOCAL選項,則註冊在NF_IP_LOCAL_OUT和NF_IP_LOCAL_IN點的hook函數就會工作,最常見的用法的是用NF_IP_LOCAL_OUT點的ip_nat_local_fn()函數來改變本機發出報文的目的地址。至於註冊在NF_IP_LOCAL_IN點的ip_nat_fn()函數一般不會起作用,只是當數據包到達該HOOK點後會例行被調用一下。因爲,NAT的所有規則只可能被配置到nat表的PREROUTING、POSTROUTING和OUTPUT三個點上,一般也很少有人去修改那些路由給本機的報文的源地址。

         NAT的分類如下圖所示:

    相信大家在看iptables用戶指南都見過這麼一句解釋:

    只有每條連接的第一個數據包纔會經過nat表,而屬於該連接的後續數據包會按照第一個數據包則會按照第一個報所執行的動作進行處理,不再經過nat表。Netfilter爲什麼要做這個限制?有什麼好處?它又是如何實現的?我們在接下來的分析中,將一一和大家探討這些問題。

    在ip_nat_rule.c文件中定義了nat表的初始化數據模板nat_table,及相應的target實體:SNAT和DNAT,並將其掛在到全局xt[PF_INET].target鏈表中。關於NAT所註冊的幾個hook函數,其調用關係我們在前幾篇博文中也見過:

 

因此,我們的核心就集中在ip_nat_in()上。也就是說,當我們弄明白了ip_nat_fn()函數,你就差不多已經掌握了nat的精髓。ip_nat_in()函數定義定在ip_nat_standalone.c文件裏。連接跟蹤作爲NAT的基礎,而建立在連接跟蹤基礎上的狀態防火牆同樣服務於NAT系統。

    關於ip_nat_fn()函數我們還是先梳理整體流程,以便大家對其有一個宏觀整體的把握,然後我們再來分析其實現細節。這裏需要大家對連接跟蹤的狀態躍遷有一定了瞭解。

 

    從流程圖可以看出,牽扯到的幾個關鍵函數都土黃色標註出來了。ip_nat_setup_info()函數主要是完成對數據包的連接跟蹤記錄ip_conntrack對象中相應成員的修改和替換,而manip_pkt()中才是真正對skb裏面源/目的地址,端口以及數據包的校驗和字段修改的地方。

 

 

目的地址轉換:DNAT

         DNAT主要適用於將內部私有地址的服務發佈到公網的情形。情形如下:

 

 

    服務器上架設了Web服務,其私有地址是B,代理防火牆服務器有一個公網地址A。想在需要通過A來訪問B上的Web服務,此時就需要DNAT出馬才行。根據前面的流程圖,我們馬上殺入內核。

當client通過Internet訪問公網地址A時,通過配置在防火牆上的DNAT將其映射到了對於私網內服務器B的訪問。接下來我們就分析一下當在client和server的交互過程中架設在防火牆上NAT是如何工作。

還是看一下hook函數在內核中的掛載分佈圖。

 

    在PREROUTING點當一個skb被連接跟蹤過後,那麼skb->ctinfo和skb->nfct兩個字段均被設置了值。在接下來的分析中,對那些梢枝末節的代碼我們都將不予理睬。盜個圖:

 

 

這裏牽扯到一個變量ip_conntrack_untracked,之前我們見過,但是還沒討論過。該變量定義在ip_conntrack_core.c文件裏,並在ip_conntrack_init()函數進行部分初始化:

atomic_set(&ip_conntrack_untracked.ct_general.use, 1);

set_bit(IPS_CONFIRMED_BIT, &ip_conntrack_untracked.status);

同時,在ip_nat_core.c文件裏的ip_nat_init()函數中又有如下設置:

ip_conntrack_untracked.status |= IPS_NAT_DONE_MASK;

該變量又是何意呢?我們知道iptables維護的其實是四張表,有一張raw不是很常用。該表以-300的優先級在PREROUTING和LOCAL_OUT點註冊了ipt_hook函數,其優先級要高於連接跟蹤。當每個數據包到達raw表時skb->nfct字段缺省都被設置成了ip_conntrack_untracked,所以當該skb還沒被連接蹤的話,其skb->nfct就一直是ip_conntrack_untracked。對於沒有被連接跟蹤處理過的skb是不能進行NAT的,因此遇到這種情況代碼中直接返回ACCEPT。

從上面的流程圖可以看出,無論是alloc_null_binding_confirmed()、alloc_null_binding()還是ip_nat_rule_find()函數其本質上最終都調用了ip_nat_setup_info()函數。

 

ip_nat_setup_info()函數:

該函數中主要完成了對連接跟蹤記錄ip_conntrack.status字段的設置,同時根據可能配置在nat表中的DNAT規則對連接跟蹤記錄裏的響應tuple進行修改,最後將該ip_conntrack實例掛載到全局雙向鏈表bysource裏。

    在連接跟蹤系統里根據skb的源/目的IP分別已經構建生成初始tuple和響應tuple,我們通過一個簡單的示意圖來回顧一下其流程,並加深對ip_nat_setup_info()函數執行過程的理解。

 

 

在圖1中,根據skb的源、目的IP生成了其連接跟蹤記錄的初始和響應tuple;

在圖2中,以初始tuple爲輸入數據,根據DNAT規則來修改將要被改變的地址。這裏是當我們訪問目的地址是A的公網地址時,DNAT規則將其改成對私網地址B的訪問。然後,計算被DNAT之後的數據包新的響應。最後用新的響應tuple替換ip_conntrack實例中舊的響應tuple,因爲數據包的目的地址已經被改變了,所以其響應tuple也必須跟着變。

在圖3中,會根據初始tuple計算一個hash值出來,然後以ip_conntrack結構中的nat.info字段會被組織成一個雙向鏈表,將其插入到全局鏈表bysource裏。

最後,將ip_conntrack.status字段的IPS_DST_NATIPS_DST_NAT_DONE_BIT位均置爲1。

這裏必須明確一點:在ip_nat_setup_info()函數中僅僅是對ip_conntrack結構實例中相關字段進行了設置,並沒有修改原始數據包skb裏的源、目的IP或任何端口。

 

ip_nat_packet()函數的核心是調用manip_pkt()函數:

在manip_pkt()裏主要完成對數據包skb結構中源/目的IP和源/目的端口的修改,並且修改了IP字段的校驗和。從mainip_pkt()函數中返回就回到了ip_nat_in()函數中(節選):

ret = ip_nat_fn(hooknum, pskb, in, out, okfn);

if (ret != NF_DROP && ret != NF_STOLEN&& daddr != (*pskb)->nh.iph->daddr) {

         dst_release((*pskb)->dst);

         (*pskb)->dst = NULL;

}

return ret;

    正常情況下返回值ret一般都爲NF_ACCEPT,因此會執行if條件語句,清除skb原來的路由信息,然後在後面協議棧的ip_rcv_finish()函數中重新計算該數據包的路由。

在數據包即將離開NAT框架時,還有一個名爲ip_nat_adjust()的函數。參見hook函數的掛載示意圖。該函數主要是對那些執行了NAT的數據包的序列號進行適當的調整。如果調整出錯,則丟棄該skb;否則,將skb繼續向後傳遞,即將到達連接跟蹤的出口ip_confirm()。至於,ip_confirm()函數的功能說明我們在連接跟蹤章節已經深入討論了,想不起來的童鞋可以回頭複習一下連接跟蹤的知識點。

 

前面我們僅分析了從client發出的第一個請求報文到server服務器時,防火牆的處理工作。緊接着我們順着前面的思路繼續分析,當server收到該數據包後迴應時防火牆的處理情況。

server收到數據包時,該skb的源地址(記爲X)從未變化,但目的地址被防火牆從A改成了B。server在響應這個請求時,它發出的迴應報文目的地址是X,源地址是自己的私有地址B。大家注意到這個源、目的地址剛好匹配被DNAT之後的那個響應tuple。

當該迴應報文到達防火牆後,首先是被連接跟蹤系統處理。顯而易見,在全局的連接跟蹤表ip_conntrack_hash[]中肯定可以找到這個tuple所屬的連接跟蹤記錄ip_conntrack實例。關於狀態的變遷參見博文八。

然後,該回應報文到達NAT框架的ip_nat_in()函數,流程和前面一樣,但處理方式肯定不同。我們還是先看一下截止到目前爲止,這條連接跟蹤結構圖:

 

 

直接跳到ip_nat_packet()函數裏,當netlink通知機制將連接跟蹤狀態由NEW變爲REPLY後,此時dir=1,那麼根據初始tuple求出原來的響應tuple:源地址爲A,目的之爲X。此時,server的響應報文,源地址爲私有網段B,目的地址爲X。路由尋址是以目的地址爲依據,防火牆上有直接到client的路由,所以響應報文是可以被client正確收到的。但是,但可是,蛋炒西紅柿,對於UDP來說client收到這樣的回覆沒有任何問題的,但是對於TCPU而言確實不行的。這就引出我們接下來將要討論的SNAT。

    未完,待續…

 

 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章