netfilter 鏈接跟蹤機制與NAT原理


內核版本:2.6.12
1.
鏈接跟蹤 conntrack

1.1.netfilter框架
5個鏈:

NF_IP_PRE_ROUTING:數據包進入路由表之前

NF_IP_LOCAL_IN:通過路由表后目的地為本機

NF_IP_FORWARD:通過路由表后,目的地不為本機

NF_IP+LOCAL_OUT:由本機產生,向外轉發

NF_IP_POST_ROUTING:發送到網卡接口之前。

4個表:

filter,nat,mangle,raw,默認表是filter(沒有指定表的時候就是filter表)。

          filter一般的過濾功能

          nat: 用於nat功能(端口映射,地址映射等)

          mangle: 用於對特定數據包的修改

          raw:優先級最高,設置raw時一般是為了不再讓iptables做數據包的鏈接跟蹤處理,提高性能

表和鏈的關系:(raw連接跟蹤在下面單獨說明)

數據包流程: 當數據包到達防火牆時,如果MAC地址符合,就會由內核里相應的驅動程序接收,然后會經過一系列操作,從而決定是發送給本地的程序,還是轉發給其他機子,還是其他的什么。
 

首先來看一個以本地為目的的數據包,它要經過以下步驟才能到達要接收它的程序 :

Step

Table

Chain

Comment

1

 

 

在線路上傳輸(比如,Internet)

2

 

 

進入接口 (比如,   eth0)

3

mangle

PREROUTING

這個鏈用來mangle數據包,比如改變TOS等

4

nat

PREROUTING

這個鏈主要用來做DNAT。不要在這個鏈做過慮操作,因為某些情況下包會溜過去。

5

 

 

路由判斷,比如,包是發往本地的,還是要轉發的。

6

mangle

INPUT

在路由之后,被送往本地程序之前,mangle數據包。

7

filter

INPUT

所有以本地為目的的包都要經過這個鏈,不管它們從哪兒來,對這些包的過濾條件就設在這里。

8

 

 

到達本地程序了(比如,服務程序或客戶程序)

 

接着看看以以本地為源的數據包,它需要經過下面的步驟才能發送出去:

Step

Table

Chain

Comment

1

 

 

本地程序(比如,服務程序或客戶程序)

2

 

 

路由判斷,要使用源地址,外出接口,還有其他一些信息。

3

mangle

OUTPUT

在這兒可以mangle包。建議不要在這兒做過濾,可能有副作用。

4

nat

OUTPUT

這個鏈對從防火牆本身發出的包進行DNAT操作。

5

filter

OUTPUT

對本地發出的包過濾。

6

mangle

POSTROUTING

這條鏈主要在包DNAT之后(譯者注:作者把這一次DNAT稱作實際的路由,雖然在前面有一次路由。對於本地的包,一旦它被生成,就必須經過路由代碼的處理,但這個包具體到哪兒去,要由NAT代碼處理之后才能確定。所以把這稱作實際的路由。),離開本地之前,對包 mangle。有兩種包會經過這里,防火牆所在機子本身產生的包,還有被轉發的包。

7

nat

POSTROUTING

在這里做SNAT。但不要在這里做過濾,因為有副作用,而且有些包是會溜過去的,即使你用了DROP策略。

8

 

 

離開接口(比如: eth0)

9

 

 

在線路上傳輸(比如,Internet)

 

最后我們看一個目的是另一個網絡中的一台機子:

Step

Table

Chain

Comment

1

 

 

在線路上傳輸(比如,Internet)

2

 

 

進入接口(比如, eth0)

3

mangle

PREROUTING

mangle數據包,,比如改變TOS等。

4

nat

PREROUTING

這個鏈主要用來做DNAT。不要在這個鏈做過慮操作,因為某些情況下包會溜過去。稍后會做SNAT。

5

 

 

路由判斷,比如,包是發往本地的,還是要轉發的。

6

mangle

FORWARD

包繼續被發送至mangle表的FORWARD鏈,這是非常特殊的情況才會用到的。在這里,包被mangle(還記得mangle的意思嗎)。這次mangle發生在最初的路由判斷之后,在最后一次更改包的目的之前(譯者注:就是下面的FORWARD鏈所做的,因其過濾功能,可能會改變一些包的目的地,如丟棄包)。

7

filter

FORWARD

包繼續被發送至這條FORWARD鏈。只有需要轉發的包才會走到這里,並且針對這些包的所有過濾也在這里進行。注意,所有要轉發的包都要經過這里,不管是外網到內網的還是內網到外網的。在你自己書寫規則時,要考慮到這一點。  

8

mangle

POSTROUTING

這個鏈也是針對一些特殊類型的包(譯者注:參考第6步,我們可以發現,在轉發包時,mangle表的兩個鏈都用在特殊的應用上)。這一步mangle是在所有更改包的目的地址的操作完成之后做的,但這時包還在本地上。

9

nat

POSTROUTING

這個鏈就是用來做SNAT的,當然也包括Masquerade(偽裝)。但不要在這兒做過濾,因為某些包即使不滿足條件也會通過。

10

 

 

離開接口(比如: eth0)

11

 

 

又在線路上傳輸了(比如,LAN)  

 

就如你所見的,包要經歷很多步驟,而且它們可以被阻攔在任何一條鏈上,或者是任何有問題的地方。

1.2.連接跟蹤(CONNTRACK),顧名思義,就是跟蹤並且記錄連接狀態。Linux為每一個經過網絡堆棧的數據包,生成一個新的連接記錄項 (Connection entry)。此后,所有屬於此連接的數據包都被唯一地分配給這個連接,並標識連接的狀態。連接跟蹤是防火牆模塊的狀態檢測的基礎,同時也是地址轉換中實 現SNAT和DNAT的前提。那么Netfilter又是如何生成連接記錄項的呢?每一個數據,都有“來源”與“目的”主機,發起連接的主機稱為“來源”,響應“來源”的請求的主機即為目的,所謂生成記錄項,就是對每一個這樣的連接的產生、傳輸及終止進行跟蹤記錄。由所有記錄項產生的表,即稱為連接跟蹤表。

 

1.2.1連接記錄

在 Linux 內核中,連接記錄由ip_conntrack結構表示,其結構如下圖所示。在該結構中,包含一個nf_conntrack類型的結構,其記錄了連接記錄被公開應用的計數,也方便其他地方對連接跟蹤的引用。每個連接記錄都對應一個指向連接超時的函數指針,當較長時間內未使用該連接,將調用該指針所指向的函數。如果針對某種協議的連接跟蹤需要擴展模塊的輔助,則在連接記錄中會有一指向ip_conntrack_helper 結構體的指針。連接記錄中的結構體ip_conntrack_tuple_hash實際記錄了連接所跟蹤的地址信息(源和目的地址)和協議的特定信息(端口)。所有連接記錄的ip_conntrack_tuple_hash以散列形式保存在連接跟蹤表中(ip_conntrack記錄存放在堆里面)。

1.2.3鏈接跟蹤表

連接跟蹤表是記錄所有連接記錄的散列表,其由全局變量ip_conntrack_hash所指向。連接跟蹤表實際是一個以散列值排列的雙向鏈表數組,鏈表中的元素即為連接記錄所包含的ip_conntrack_tuple_hash結構。

1.3傳輸協議

連接跟蹤機制可以支持多種傳輸協議,不同的協議所采用的跟蹤方式會有所不同。傳輸協議用結構ip_conntrack_protocol 保存,所有的已注冊的傳輸協議列表由全局變量ip_ct_protos 所指向的一維數組保存,且按照協議號的順序依次排列。函數ip_conntrack_protocol_register()和ip_conntrack_protocol_unregister()用於向協議列表中添加或刪除一個協議。

數據結構部分總結:
1.整個hash表用ip_conntrack_hash 指針數組來描述,它包含了ip_conntrack_htable_size個元素,默認是根據內存大小計算出來的;
2. 整個連接跟蹤表的大小使用全局變量ip_conntrack_max描述,與hash表的關系是ip_conntrack_max = 8 * ip_conntrack_htable_size;
3. hash鏈表的每一個節點是一個struct ip_conntrack_tuple_hash結構,它有兩個成員,一個是list,一個是tuple;
4.Netfilter將每一個數據包轉換成tuple,再根據tuple計算出hash值,這樣,就可以使用ip_conntrack_hash[hash_id]找到hash表中鏈表的入口,並組織鏈表;
5. 找到hash表中鏈表入口后,如果鏈表中不存在此“tuple”,則是一個新連接,就把tuple插入到鏈表的合適位置;
6. 圖中兩個節點tuple[ORIGINAL]和tuple[REPLY],雖然是分開的,在兩個鏈表當中,但是如前所述,它們同時又被封裝在ip_conntrack結構的tuplehash數組中;

 

大家感興趣的肯定是怎么實現鏈接跟蹤的,由於篇幅,我省略去鏈接跟蹤的初始化等等部分,重點講解一下鏈接跟蹤的實現。

1.4鏈接跟蹤的實現—ip_conntrack_in()

數據包進入Netfilter后,會調用ip_conntrack_in函數,以進入連接跟蹤模塊,ip_conntrack_in 主要完成的工作就是判斷數據包是否已在連接跟蹤表中,如果不在,則為數據包分配ip_conntrack,並初始化它,然后,為這個數據包設置連接狀態。

 1 unsigned int ip_conntrack_in(unsigned int hooknum,
 2       struct sk_buff **pskb,
 3       const struct net_device *in,
 4       const struct net_device *out,
 5       int (*okfn)(struct sk_buff *))
 6 {
 7 struct ip_conntrack *ct;
 8 enum ip_conntrack_info ctinfo;
 9 struct ip_conntrack_protocol *proto;
10 int set_reply;
11 int ret;
12 
13 /* 判斷當前數據包是否已被檢查過了 */
14 if ((*pskb)->nfct) {
15 CONNTRACK_STAT_INC(ignore);
16 return NF_ACCEPT;
17 }
18 
19 /* 分片包當會在前一個Hook中被處理,事實上,並不會觸發該條件 */
20 if ((*pskb)->nh.iph->frag_off & htons(IP_OFFSET)) { 
21 if (net_ratelimit()) {
22 printk(KERN_ERR "ip_conntrack_in: Frag of proto %u (hook=%u)\n",
23           (*pskb)->nh.iph->protocol, hooknum);
24 }
25 return NF_DROP;
26 }
27 
28 /* 將當前數據包設置為未修改 */
29 (*pskb)->nfcache |= NFC_UNKNOWN;
30 
31 /*根據當前數據包的協議,查找與之相應的struct ip_conntrack_protocol結構*/
32 proto = ip_ct_find_proto((*pskb)->nh.iph->protocol);
33 
34 /* 沒有找到對應的協議. */
35 if (proto->error != NULL 
36        && (ret = proto->error(*pskb, &ctinfo, hooknum)) <= 0) {
37 CONNTRACK_STAT_INC(error);
38 CONNTRACK_STAT_INC(invalid);
39 return -ret;
40 }
41 
42 /*在全局的連接表中,查找與當前包相匹配的連接結構,返回的是struct ip_conntrack *類型指針,它用於描述一個數據包的連接狀態*/
43 if (!(ct = resolve_normal_ct(*pskb, proto,&set_reply,hooknum,&ctinfo))) {
44 /* Not valid part of a connection */
45 CONNTRACK_STAT_INC(invalid);
46 return NF_ACCEPT;
47 }
48 
49 if (IS_ERR(ct)) {
50 /* Too stressed to deal. */
51 CONNTRACK_STAT_INC(drop);
52 return NF_DROP;
53 }
54 
55 IP_NF_ASSERT((*pskb)->nfct);
56 
57 /*Packet函數指針,為數據包返回一個判斷,如果數據包不是連接中有效的部分,返回-1,否則返回NF_ACCEPT。*/
58 ret = proto->packet(ct, *pskb, ctinfo);
59 if (ret < 0) {
60 /* Invalid: inverse of the return code tells
61     * the netfilter core what to do*/
62 nf_conntrack_put((*pskb)->nfct);
63 (*pskb)->nfct = NULL;
64 CONNTRACK_STAT_INC(invalid);
65 return -ret;
66 }
67 
68 /*設置應答狀態標志位*/
69 if (set_reply)
70 set_bit(IPS_SEEN_REPLY_BIT, &ct->status);
71 return ret;
72 }

在初始化的時候,我們就提過,連接跟蹤模塊將所有支持的 協議,都使用struct ip_conntrack_protocol 結構封裝,注冊至全局數組ip_ct_protos,這里首先調用函數ip_ct_find_proto根據當前數據包的協議值,找到協議注冊對應的模 塊。然后調用resolve_normal_ct 函數進一步處理.

接下來我們再看一下resolve_normal_ct是怎么實現的:
resolve_normal_ct 函數是連接跟蹤中最重要的函數之一,它的主要功能就是判斷數據包在連接跟蹤表是否存在,如果不存在,則為數據包分配相應的連接跟蹤節點空間並初始化,然后設置連接狀態:
 

 1 CODE:/* On success, returns conntrack ptr, sets skb->nfct and ctinfo */
 2 static inline struct ip_conntrack *
 3 resolve_normal_ct(struct sk_buff *skb,
 4    struct ip_conntrack_protocol *proto,
 5    int *set_reply,
 6    unsigned int hooknum,
 7    enum ip_conntrack_info *ctinfo)
 8 {
 9 struct ip_conntrack_tuple tuple;
10 struct ip_conntrack_tuple_hash *h;
11 struct ip_conntrack *ct;
12 
13 IP_NF_ASSERT((skb->nh.iph->frag_off & htons(IP_OFFSET)) == 0);
14 
15 /*前面提到過,需要將一個數據包轉換成tuple,這個轉換,就是通過ip_ct_get_tuple函數實現的*/
16 if (!ip_ct_get_tuple(skb->nh.iph, skb, skb->nh.iph->ihl*4, 
17 &tuple,proto))
18 return NULL;
19 
20 /*查看數據包對應的tuple在連接跟蹤表中是否存在 */
21 h = ip_conntrack_find_get(&tuple, NULL);
22 if (!h) {
23 /*如果不存在,初始化之*/
24 h = init_conntrack(&tuple, proto, skb);
25 if (!h)
26 return NULL;
27 if (IS_ERR(h))
28 return (void *)h;
29 }
30 /*根據hash表節點,取得數據包對應的連接跟蹤結構*/
31 ct = tuplehash_to_ctrack(h);
32 
33 /* 判斷連接的方向 */
34 if (DIRECTION(h) == IP_CT_DIR_REPLY) {
35 *ctinfo = IP_CT_ESTABLISHED + IP_CT_IS_REPLY;
36 /* Please set reply bit if this packet OK */
37 *set_reply = 1;
38 } else {
39 /* Once we've had two way comms, always ESTABLISHED. */
40 if (test_bit(IPS_SEEN_REPLY_BIT, &ct->status)) {
41 DEBUGP("ip_conntrack_in: normal packet for %p\n",
42           ct);
43          *ctinfo = IP_CT_ESTABLISHED;
44 } else if (test_bit(IPS_EXPECTED_BIT, &ct->status)) {
45 DEBUGP("ip_conntrack_in: related packet for %p\n",
46           ct);
47 *ctinfo = IP_CT_RELATED;
48 } else {
49 DEBUGP("ip_conntrack_in: new packet for %p\n",
50           ct);
51 *ctinfo = IP_CT_NEW;
52 }
53 *set_reply = 0;
54 }

在初始化的時候,我們就提過,連接跟蹤模塊將所有支持的 協議,都使用struct ip_conntrack_protocol 結構封裝,注冊至全局數組ip_ct_protos,這里首先調用函數ip_ct_find_proto根據當前數據包的協議值,找到協議注冊對應的模 塊。然后調用resolve_normal_ct 函數進一步處理.

接下來我們再看一下resolve_normal_ct是怎么實現的:


resolve_normal_ct 函數是連接跟蹤中最重要的函數之一,它的主要功能就是判斷數據包在連接跟蹤表是否存在,如果不存在,則為數據包分配相應的連接跟蹤節點空間並初始化,然后設置連接狀態:

 1 CODE:/* On success, returns conntrack ptr, sets skb->nfct and ctinfo */
 2 static inline struct ip_conntrack *
 3 resolve_normal_ct(struct sk_buff *skb,
 4    struct ip_conntrack_protocol *proto,
 5    int *set_reply,
 6    unsigned int hooknum,
 7    enum ip_conntrack_info *ctinfo)
 8 {
 9 struct ip_conntrack_tuple tuple;
10 struct ip_conntrack_tuple_hash *h;
11 struct ip_conntrack *ct;
12 
13 IP_NF_ASSERT((skb->nh.iph->frag_off & htons(IP_OFFSET)) == 0);
14 
15 /*前面提到過,需要將一個數據包轉換成tuple,這個轉換,就是通過ip_ct_get_tuple函數實現的*/
16 if (!ip_ct_get_tuple(skb->nh.iph, skb, skb->nh.iph->ihl*4, 
17 &tuple,proto))
18 return NULL;
19 
20 /*查看數據包對應的tuple在連接跟蹤表中是否存在 */
21 h = ip_conntrack_find_get(&tuple, NULL);
22 if (!h) {
23 /*如果不存在,初始化之*/
24 h = init_conntrack(&tuple, proto, skb);
25 if (!h)
26 return NULL;
27 if (IS_ERR(h))
28 return (void *)h;
29 }
30 /*根據hash表節點,取得數據包對應的連接跟蹤結構*/
31 ct = tuplehash_to_ctrack(h);
32 
33 /* 判斷連接的方向 */
34 if (DIRECTION(h) == IP_CT_DIR_REPLY) {
35 *ctinfo = IP_CT_ESTABLISHED + IP_CT_IS_REPLY;
36 /* Please set reply bit if this packet OK */
37 *set_reply = 1;
38 } else {
39 /* Once we've had two way comms, always ESTABLISHED. */
40 if (test_bit(IPS_SEEN_REPLY_BIT, &ct->status)) {
41 DEBUGP("ip_conntrack_in: normal packet for %p\n",
42           ct);
43          *ctinfo = IP_CT_ESTABLISHED;
44 } else if (test_bit(IPS_EXPECTED_BIT, &ct->status)) {
45 DEBUGP("ip_conntrack_in: related packet for %p\n",
46           ct);
47 *ctinfo = IP_CT_RELATED;
48 } else {
49 DEBUGP("ip_conntrack_in: new packet for %p\n",
50           ct);
51 *ctinfo = IP_CT_NEW;
52 }
53 *set_reply = 0;
54 }

鏈接跟蹤的大致原理如此:對於數據包,首先檢查它的tuple是否存在於hash表中,若存在就能找到對應的連接記錄,若不存在就新建一個連接記錄,將對應的兩個tuple都加入到hash表中去。里並沒有詳細分析鏈接跟蹤的全部實現,只是簡要介紹了它工作的主要原理以為下面的NAT實現做一點准備,不同協議的連接狀態動態變遷以及更加復雜的細節就不做深入說明了,若要深入理解,請查閱源代碼及其他資料。

 

1.5簡要模擬一下鏈接跟蹤的流程:

1.本地主機創建了一個數據包:skb_0(202.2.2.1:120à202.10.10.1:30,TCP);

2.數據包在經過鏈 NF_IP_LOCAL_OUT時,會首先進入鏈接跟蹤模塊。模塊根據skb_0(202.2.2.1:120à202.10.10.1:30,TCP)調用函數ip_ct_get_tuple將數據包轉換成一個tuple;

3.然后調用ct = tuplehash_to_ctrack()嘗試獲取它的連接跟蹤記錄項,發現不存在該記錄項,於是新建一個和它關聯的連接跟蹤記錄,並將連接跟蹤記錄的狀態標識為 NEW。要注意的地方時當新建一個連接跟蹤記錄時,連接跟蹤記錄中會生成兩個tuple,一個是 原始方向的tuple[ORIGINAL]={ 202.2.2.1:120à202.10.10.1:30,TCP };於此同時,系統會自動生成應答方向的tuple[REPLY]={ 202.10.10.1:30à202.2.2.1:120,TCP };記住要點,一個連接跟蹤記錄會持有兩個方向上的tuple記錄;
            4.該數據包被發送給目的主機;

5.目的主機收到后,發送恢復數據包skb_1(202.10.10.1:30à202.2.2.1:120,TCP);給源主機;

6.數據包skb_1到達主機的NF_IP_PRE_ROUNTING鏈,首先進入連接跟蹤模塊,調用函數ip_ct_get_tuple將數據包轉換成一個tuple,可以知道這個tuple={ 202.10.10.1:30à202.2.2.1:120,TCP };

7.對上面得到的tuple調用ct = tuplehash_to_ctrack(),系統變得到了該數據包屬於哪一個連接記錄,然后將該連接記錄項中的狀態改成ESTABLISHED.

8.后面從本機發送的和從外面主機發過來的數據包都可以根據這個機制查找到相應的屬於自己的連接記錄項,如果不能查找到連接記錄項,系統會丟棄該數據包。這邊是簡單的防火牆的實現。

過程如下圖所示:

這里描述了連接跟蹤最基本的實現原理,其實連接跟蹤的實現中還有很多其他細節,這里就不做深入說明了。

2.netfilterNAT實現原理

前面簡要介紹 了conntrack的原理,因為它是實現NAT的基礎。
            Netfilter在連接跟蹤的基礎上,實現了兩種類型的地址轉換:源地址轉換目的地址轉換。顧名思義,源地址轉換就是修改IP包中的源地址(或許還有源端口),而目的地址轉換,就是修改IP包中的目的地址(同樣,或許還有目的端口)。前者通常用於將內網主機私網地址轉換為公網地址,訪問Internet,后者通常用於將公網IP地址轉換為一個或幾個私網地址,兩者結合在一起,實現向互聯網提供服務。

下面我們就來分析它的實現過程:

 

2.1 NAT模塊的初始化

模塊初始化的工作包括初始化NAT規則,這些規則是由用戶自己配置的;然后需要初始化NAT模塊工作所需要的數據結構;注冊鈎子。主要就是做這三方面的事情。初始化函數為init_or_cleanup(),這個函數也可以用來清除以前初始化的內容,位於文件ip_nat_standard.c中。

 1 static int init_or_cleanup(int init)
 2 {
 3     int ret = 0;
 4     need_ip_conntrack();
 5     if (!init) goto cleanup;
 6     ret = ip_nat_rule_init();   /* 初始化nat規則 */
 7 
 8     if (ret < 0) {
 9         printk("ip_nat_init: can't setup rules.\n");
10         goto cleanup_nothing;
11     }
12 
13     ret = ip_nat_init();   /*初始化nat所需要重要數據結構*/
14     if (ret < 0) {
15         printk("ip_nat_init: can't setup rules.\n");
16         goto cleanup_rule_init;
17     }
18 
19     /*注冊Hook*/
20     ret = nf_register_hook(&ip_nat_in_ops);     /*對應於NF_IP_PRE_ROUNTING,會調用函數ip_nat_fn()進行地址轉換操作*/
21     ret = nf_register_hook(&ip_nat_out_ops); /*對應於NF_IP_POST_ROUNTING,會調用函數ip_nat_fn()進行地址轉換操作*/
22 
23     ret = nf_register_hook(&ip_nat_adjust_in_ops);
24     ret = nf_register_hook(&ip_nat_adjust_out_ops);
25     ret = nf_register_hook(&ip_nat_local_out_ops); /*對應於NF_IP_LOACL_OUT,間接調用函數ip_nat_fn()進行地址轉換操作*/
26     ret = nf_register_hook(&ip_nat_local_in_ops); /*對應於NF_IP_LOACL_IN,會調用函數ip_nat_fn()進行地址轉換操作*/
27     return ret;
28  cleanup:   /*卸載各個注冊的模塊,釋放初始化時申請的資源*/
29     ……….
30     return ret;
31 }

可以看出,在四個hook點處都可以執行nat轉換操作,而且nat轉換操作的執行函數式ip_nat_fn(),后面會詳細說明它的實現。

我們主要關心它在PREROUTING和POSTROUTING兩個Hook點上注冊的Hook,因為它們完成了最重要的源地址轉換和目的地址轉換:

 1 /* 目的地址轉換的Hook,在filter包過濾之前進行 */
 2 static struct nf_hook_ops ip_nat_in_ops = {
 3     .hook        = ip_nat_in,
 4     .owner        = THIS_MODULE,
 5     .pf        = PF_INET,
 6     .hooknum    = NF_IP_PRE_ROUTING,
 7     .priority    = NF_IP_PRI_NAT_DST, //-100,越小優先級越高
 8 };
 9 
10 /*源地址轉換,在filter包過濾之后*/
11 static struct nf_hook_ops ip_nat_out_ops = {
12     .hook        = ip_nat_out,
13     .owner        = THIS_MODULE,
14     .pf        = PF_INET,
15     .hooknum    = NF_IP_POST_ROUTING,
16     .priority    = NF_IP_PRI_NAT_SRC, //100
17 };

接下來,看一下ip_nat_rule_init(),注冊NAT表和兩個target:源地址轉換(SNAT)和目的地址轉換(DNAT),NAT表用於獲取NAT規則的,兩個traget會關聯上 SNAT和DNAT的操作函數,它們的target處理函數分別是ipt_snat_target和ipt_dnat_target。這兩個函數都會調用ip_nat_setup_info()還建立於NAT相關的信息。

ip_nat_setup_info()會在后面進行說明。

 1 //初始化nat規則
 2 int __init ip_nat_rule_init(void)
 3 {
 4     int ret;
 5     ret = ipt_register_table(&nat_table, &nat_initial_table.repl);     /* 注冊nat表 */
 6     if (ret != 0)
 7         return ret;
 8 
 9     /* 注冊了兩個target,一個是snat一個是dnat  */
10     ret = ipt_register_target(&ipt_snat_reg);
11     if (ret != 0)
12         goto unregister_table;
13     ret = ipt_register_target(&ipt_dnat_reg);
14     if (ret != 0)
15         goto unregister_snat;
16     return ret;
17 
18  unregister_snat:
19     ipt_unregister_target(&ipt_snat_reg);
20  unregister_table:
21     ipt_unregister_table(&nat_table);
22     return ret;
23 }

再看一下ip_nat_init(),規則之外的初始化都在這里面:

 1 int __init ip_nat_init(void)
 2 {
 3     size_t i;
 4     ip_nat_htable_size = ip_conntrack_htable_size; /* nat的hash表大小和conntrack的hash表相同 */
 5     bysource = vmalloc(sizeof(struct list_head) ;/* ip_nat_htable_size); /* 初始化了一個叫bysource的全局鏈表指針 */
 6     if (!bysource)
 7         return -ENOMEM;
 8 
 9     /* Sew in builtin protocols. */
10     WRITE_LOCK(&ip_nat_lock);
11     for (i = 0; i < MAX_IP_NAT_PROTO; i++)
12         ip_nat_protos[i] = &ip_nat_unknown_protocol;
13     ip_nat_protos[IPPROTO_TCP] = &ip_nat_protocol_tcp;
14     ip_nat_protos[IPPROTO_UDP] = &ip_nat_protocol_udp;
15     ip_nat_protos[IPPROTO_ICMP] = &ip_nat_protocol_icmp;
16     WRITE_UNLOCK(&ip_nat_lock);
17 
18     for (i = 0; i < ip_nat_htable_size; i++) {  /*初始化hash表*/
19         INIT_LIST_HEAD(&bysource[i]);
20     }
21     IP_NF_ASSERT(ip_conntrack_destroyed == NULL);
22     ip_conntrack_destroyed = &ip_nat_cleanup_conntrack;
23 
24     ip_conntrack_untracked.status |= IPS_NAT_DONE_MASK;
25     return 0;
26 }

2.2源地址轉換 SNAT

源地址轉換注冊在NF_IP_POST_ROUTING,數據包在包過濾之后,會進入ip_nat_out函數。源地址的轉換最終要做的工作,就是修改IP包中的源地址,將其替換為iptables添加規則時指定的“轉換后地址”,對於絕大多數應用而言,一般是將私網IP地址修改為公網IP地址,然后將數據包發送出去。但是,很自然地,這樣修改后,回來的應答數據包沒有辦法知道它轉換之前的樣子,也就是不知道真實的來源主機(對於回應包,也就是不知道把數據應答給誰),數據包將被丟棄,所以有必要,維護一張地址轉換表,詳細記錄數據包的轉換情況,以使NAT后的數據能交互地傳輸。對於Netfilter而言,已經為進出數據包建立了一張狀態跟蹤表,自然也就沒有必要重新多維護一張表了,也就是,合理地利用狀態跟蹤表,實現對NAT狀態的跟蹤和維護。

 

源地址轉換的主要步驟大致如下:

源地址轉換的主要步驟大致如下:
1. 數據包進入Hook(例如ip_nat_out)函數后,進行規則匹配;
2. 如果所有match都匹備,則進行SNAT模塊的動作,即snat 的target模塊;
3. 源地址轉換的規則一般是…… -j SNAT –to X.X.X.X,SNAT用規則中預設的轉換后地址X.X.X.X,修改連接跟蹤表中的replay tuple;(原因:當數據包進入連接跟蹤后,會建立一個tuple以及相應的replay tuple,而應答的數據包,會查找與之匹配的repaly tuple,——對於源地址轉換而言,應答包中的目的地址,將是轉換后的地址,而不是真實的地址,所以,為了讓應答的數據包能找到對應的replay tuple,很自然地,NAT模塊應該修改replaly tuple中的目的地址,以使應答數據包能找到屬於自己的replay)
4. 接着,修改數據包的來源的地址,將它替換成replay tuple中的相應地址,即規則中預設的地址,將其發送出去;
5. 對於回來的數據包,應該能在狀態跟蹤表中,查找與之對應的replay tuple,也就能順藤摸瓜地找到原始的tuple中的信息,將應答包中的目的地址改回來,這樣,整個數據傳送就得以順利轉發了;

下面通過模擬一個新建的數據包的完整的SNAT過程來理解netfilter的SNAT過程:

本地主機的ip地址為192.168.18.2

本地主機對外地址為 192.168.20.2

目的主機地址為     202.10.10.1

使用的協議為       TCP

 

下面我給大家詳細解釋這個流程:

 

DNAT過程也是以此類推,就不做說明了。

 

bysource用途: 當一個數據包接通過NAT時,需要得到地址映射時,進行查找的,查找此源IP、協議和端口號是否已經做過了映射。如果做過的話,就需要有NAT轉換時,映射為相同的源IP和端口號。為什么這么做呢?

 

考慮這樣一種感情情況:

本地主機給目的主機發送數據包如下:

skb_0(192.168.18.2:5040à202.10.10.1:5000,UDP);

skb_1(192.168.18.2:5040à202.10.10.1:5001,UDP);

skb_2(192.168.18.2:5040à202.10.10.1:5002,UDP);

skb_3(192.168.18.2:5040à202.10.10.1:5003,UDP);

skb_4(192.168.18.2:5040à202.10.10.1:5004,UDP);

 

可以發現,它們不同的地方就是目的主機的端口,其他的部分完全一樣。

對於UDP來說,有些協議可以會用相同端口和同一主機不同的端口(或不同的主機)進行通信。此時問題就來了,若為每一個像上面的數據包都建立一個新的連接記錄其實是不正確的,因為應用程序可能需要的是同一個連接。為保證正確性, skb_1/skb_2/skb_3/skb/4 應該映射為 和skb_0關聯的相同的連接,所以bysource是以源IP、協議和端口號為hash值的一個表,以便完成上述功能。簡單來講,就是為NAT打洞服務的。關於打洞的更多細節請查閱其他方面資料。

2.3小結:

這里,只分析了最簡單情況下的NAT實現過程,還涉及到許多細節沒有分析。

可以看到,NAT的實現和連接跟蹤是分不開的,連接跟蹤是NAT實現的基礎也是關鍵。

更加復雜的FTP協議,ICMP協議還需要另外的輔助模塊處理,再次就略去,感興趣的可以繼續研究源代碼以及其他相關資料。

 



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM