歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux基礎 >> 關於Linux

Linux系統NAT實現機制的升級改進

一點牢騷和希望

一直以來,一直對Linux的NAT很不滿,也寫過《Linux系統如何平滑生效NAT》系列文章中的patch進行修補,還寫過一些類Cisco實現的patch,然而都效果不大好,暴雨的夜晚,長假的倒數第二晚,雖然沒有10月7日晚雨量大,可是10月6日晚上到7日凌晨,上海嘉定那邊的雨也可以堪稱暴雨了。一直想看卻一直沒有時間看的《斯巴達克斯 第三季》終於看完了,雨越大越興奮,可是巴拉巴西的《鏈接》也看完了,《羅馬人的故事》最後一卷也看完了,《黑天鵝》還沒有到貨,剩下的只有寫點代碼了...於是半瓶竹葉青陪我到天蒙蒙亮,修改了幾個內核代碼文件,debug了幾小時,小睡了兩小時後,起床去買新鮮的肉類和蔬菜以及海鮮,因為長假最後一天要一起在家吃火鍋。火鍋很爽,外面大雨如注,屋裡熱氣騰騰...就這樣,雨一直下到第二天早上。

10月7日第一天上班,我正常出門,可是到了公司已經12點多,在上地鐵的時候,涉水到膝蓋,轉彎,突然發現暗黃色漂浮物,臭氣迎面而來,素不相識的一行人為了安全走在了一起,我在頭陣...聽說是附近的廁所出問題了,污水糞便就從地下湧了上來...繼續前行,停下,還是回頭?如果只有我一人,我肯定回頭了,然而後面有倆MM,還挺時尚漂亮,都說要過去,然後再洗,另外一位本來應西裝筆挺的正裝哥們兒由於褲子太合身無法挽起,執意也要先走過去再說...我如此邋遢的該如何,可想而知...真心不想趟這渾水啊!!...

這麼多和工作無關的瑣事,我太羅嗦了。步入正題了!

alloc_null_binding的作用

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就是為了做這個操作。

徹底消除流頭匹配NAT的概念

要是沒有深入研究過Linux的NAT,只是僅僅會配置它的話,也許你還真的不知道NAT規則只對一個流的第一個起作用,確切的說,是只針對一個流的ip_conntrack結構體剛剛建立還沒有confirm的時候起作用,因為有時ip_conntrack結構體會過期。只要這樣的包離開了協議棧,流就被confirm了,接下來的屬於同一個流的其它數據包就直接使用上述那個包的保存在ip_conntrack結構體中的NAT結果了。

正是由於這個特點,使得你無法中途添加NAT規則使之立馬生效或者修改已有的NAT結果。這種有狀態的特性帶來了很多的問題。之前寫過《Linux系統如何平滑生效NAT》系列文章,做過一些修正補丁。然而那些補丁的問題在於:它們還是基於流頭匹配NAT規則的小修小補。我們知道,這種小修小補最終的結果就是不可維護,那麼何不來一個顛覆,即,不再采用流頭匹配NAT的原則,改為想什麼時候匹配就什麼匹配的原則。這其實是一種更高層次的顛覆,即流頭匹配原則是新的匹配原則的一種特例。

廢除了流頭匹配原則後,我決定把何時執行NAT的決定權留給應用程序,因此我決定注冊一個sysctl變量,當其非0時執行NAT,不管是不是已經confirm了。

什麼時候需要匹配NAT規則

既然說流頭匹配原則不好,會帶來問題(比如confirm的連接由於沒有NAT而僵持在那裡的問題),那麼肯定要指出何時執行NAT匹配是必要的,這叫有破有立。在以下的情況下,執行NAT是必要的:

1.數據流連接時,由於還沒有做NAT而導致久久連不上的情形。此時數據流的CT狀態依然是NEW;

2.數據流已經成功連接,但是需要改變一下源地址(改變目標地址意味著重新連接一個新的服務)。此時的數據流的TC狀態是ESTABLISHED;

3.數據流已經經過NAT連接,但NAT規則改變了。此時的數據流的TC狀態是ESTABLISHED;

哪些情況不能執行NAT

並不是所有的以上情況都適合執行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

//避開ip_conntrack_status枚舉成員即可,然而13可謂一個重量級的數字

#define NF_FORCE_NAT_BIT 13

說明:增加了一個新的CT狀態,用來指示是否要做NAT匹配。

include/net/netfilter/nf_conntrack_l4proto.h

struct nf_conntrack_l4proto

{

...

   int (*can_force_nat)(struct nf_conn *ct, struct sk_buff *skb);

...

}

說明:nf_conntrack_l4proto結構體增加了一個can_force_nat回調函數,將判斷是否能重新執行NAT的決定權交給4層協議自己而不是在ip_conntrack以及nat邏輯中為之代勞。

net/netfilter/nf_conntrack_proto_tcp.c

//////////////////////////

static int nf_ct_can_force_nat(struct nf_conn *ct, struct sk_buff *skb)

{

       //沒什麼好說的...

       return 1;

}

//////////////////////////

...

struct nf_conntrack_l4proto nf_conntrack_l4proto_tcp4 __read_mostly =

{

...

//////////////////////////

       .can_force_nat          = nf_ct_can_force_nat,

//////////////////////////

...

};

說明:添加了nf_ct_can_force_nat回調函數,指示在ESTABLISH狀態不能重新執行NAT。

net/ipv4/netfilter/nf_nat_standalone.c

//////////////////////////

#ifdef CONFIG_SYSCTL

//增加用戶態的sysctl接口,位於/proc/sys/net/ipv4/netfilter/nf_force_nat

static struct ctl_table_header *nat_sysctl_header;

static unsigned int nf_force_nat __read_mostly = 0;

static struct ctl_table nf_nat_sysctl_table[] = {

       {

               .procname       = "nf_force_nat",

               .data           = &nf_force_nat,

               .maxlen         = sizeof(unsigned int),

               .mode           = 0644,

               .proc_handler   = proc_dointvec_jiffies,

       },

       {

               .ctl_name       = 0

       }

};

#endif

//////////////////////////

...

static unsigned int

nf_nat_fn(unsigned int hooknum,

         struct sk_buff *skb,

         const struct net_device *in,

         const struct net_device *out,

         int (*okfn)(struct sk_buff *))

{

...

//////////////////////////

#ifdef CONFIG_SYSCTL

       if (nf_force_nat !=0) {

               set_bit(NF_FORCE_NAT_BIT, &ct->status);

       } else {

               clear_bit(NF_FORCE_NAT_BIT, &ct->status);

       }

#else

       clear_bit(13, &ct->status);

#endif

//////////////////////////

       switch (ctinfo) {

       case IP_CT_RELATED:

...

       case IP_CT_NEW:

/////////////########

//增加一個標簽

renat:

               /* Seen it before?  This can happen for loopback, retrans,

                  or local packets.. */

               //增加一個允許NAT的可能性

               if (!nf_nat_initialized(ct, maniptype) || test_bit(NF_FORCE_NAT_BIT, &ct->status))

/////////////########

               {

                       unsigned int ret;

...

       default:

               /* ESTABLISHED */

               NF_CT_ASSERT(ctinfo == IP_CT_ESTABLISHED ||

                            ctinfo == (IP_CT_ESTABLISHED+IP_CT_IS_REPLY));

//////////////////////////

               if (test_bit(NF_FORCE_NAT_BIT, &ct->status)) {

                       struct nf_conntrack_l3proto *l3proto;

                       struct nf_conntrack_l4proto *l4proto;

                       unsigned int dataoff;

                       u_int8_t protonum;

                       int ret;

                       l3proto = __nf_ct_l3proto_find(NFPROTO_IPV4);

                       ret = l3proto->get_l4proto(skb, skb_network_offset(skb),

                                       &dataoff, &protonum);

                       l4proto = __nf_ct_l4proto_find(NFPROTO_IPV4, protonum);

                       /**

                        *      實際上本來就應該由四層協議本身來決定是否可以強制NAT,

                        *      但是那樣就要修改conn層的回調

                        */

                       if (l4proto->can_force_nat == NULL ||

                               !l4proto->can_force_nat(ct, skb)){

                               goto renat;

                       }

               }

//////////////////////////

       }

...

}

...

static int __init nf_nat_standalone_init(void)

{

...

//////////////////////////

#ifdef CONFIG_SYSCTL

       nat_sysctl_header = register_sysctl_paths(nf_net_ipv4_netfilter_sysctl_path, nf_nat_sysctl_table);

       if (nat_sysctl_header == NULL) {

               printk("nf_nat_init: can't register nat_sysctl");

               goto cleanup_rule_init;

       }

#endif

//////////////////////////

       return ret;

cleanup_rule_init:

...

}

static void __exit nf_nat_standalone_fini(void)

{

//////////////////////////

#ifdef CONFIG_SYSCTL

       unregister_sysctl_table(nat_sysctl_header);

       nat_sysctl_header = NULL;

#endif

//////////////////////////

...

}

說明:為NAT的Netflter的HOOK函數添加何時執行NAT的判斷邏輯。

net/ipv4/netfilter/nf_nat_rule.c

int nf_nat_rule_find(struct sk_buff *skb,

                    unsigned int hooknum,

                    const struct net_device *in,

                    const struct net_device *out,

                    struct nf_conn *ct)

{

       struct net *net = nf_ct_net(ct);

       int ret;

       ret = ipt_do_table(skb, hooknum, in, out, net->ipv4.nat_table);

       if (ret == NF_ACCEPT) {

/////////////########

               if (!nf_nat_initialized(ct, HOOK2MANIP(hooknum)) || test_bit(NF_FORCE_NAT_BIT, &ct->status)) {

/////////////########

//////////////////////////

                       //如果在ipt_do_table中沒有匹配到NAT規則,並且此時允許重新NAT,則說明要把反向五元組還原成原始的反向五元組

                       //本來想在這裡做一個優化的,即如果還原了之後,在新的NAT配置上來之前,不再執行還原操作,然而這樣會有問題,

                       //注意本文第一節,由於不能保證其它的數據流是否做了NAT從而占據了不該占據的五元組,為了保證唯一性,這裡的

                       //alloc_null_binding必須持續調用,唯一可以優化的地方在於可以不用每次都調用nf_ct_invert_tuplepr以及

                       //nf_conntrack_alter_reply,而這只需要一個flag位即可。

                       /* NUL mapping */

                       if (nf_ct_is_confirmed(ct)) {

                               struct nf_conntrack_tuple reply;

                               nf_ct_invert_tuplepr(&reply, &ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple);

                               nf_conntrack_alter_reply(ct, &reply);

                       }

//////////////////////////

                       ret = alloc_null_binding(ct, hooknum);

               }

       }

       return ret;

}

說明:nf_nat_rule_find中如果沒有找到規則,則判斷是否是將已有規則刪除了,進而恢復原始狀態。

net/ipv4/netfilter/nf_nat_core.c

unsigned int

nf_nat_setup_info(struct nf_conn *ct,

                 const struct nf_nat_range *range,

                 enum nf_nat_manip_type maniptype)

{

...

/////////////########

 

       //bug僅僅on

       BUG_ON(!test_bit(NF_FORCE_NAT_BIT, &ct->status) && nf_nat_initialized(ct, maniptype));

/////////////########

...

               nf_ct_invert_tuplepr(&reply, &new_tuple);

//////////////////////////

               if (nf_ct_is_confirmed(ct)) {

                       spin_lock_bh(&nf_conntrack_lock);

                       hlist_nulls_del_rcu(&ct->tuplehash[IP_CT_DIR_ORIGINAL].hnnode);

                       hlist_nulls_del_rcu(&ct->tuplehash[IP_CT_DIR_REPLY].hnnode);

               }

//////////////////////////

               nf_conntrack_alter_reply(ct, &reply);

//////////////////////////

               if (nf_ct_is_confirmed(ct)) {

                       nf_conntrack_hash_insert(ct);

                       spin_unlock_bh(&nf_conntrack_lock);

               }

//////////////////////////

...

//////////////////////////

       clear_bit(NF_FORCE_NAT_BIT, &ct->status);

//////////////////////////

       return NF_ACCEPT;

}

說明:在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即可。

Copyright © Linux教程網 All Rights Reserved