關於nat,在實際應用中還是很廣泛的,snat/dnat/dmz/等等.下面我們就結合程式碼深入分析下nat的運作.
參考:iptables.1.4.21 kernel 3.8.13
NAT英文全稱是“Network Address Translation”
顧名思義,它是一種把內部私有網路地址(IP地址)翻譯成合法網路IP地址的技術。因此我們可以認為,NAT在一定程度上,能夠有效的解決公網地址不足的問題
分類:
NAT有三種型別:靜態NAT(Static NAT)、動態地址NAT(Pooled NAT)、網路地址埠轉換NAPT(Port-Level NAT)
其中,網路地址埠轉換NAPT(Network Address Port Translation)則是把內部地址對映到外部網路的一個IP地址的不同埠上。它可以將中小型的網路隱藏在一個合法的IP地址後面。NAPT與 動態地址NAT不同,它將內部連線對映到外部網路中的一個單獨的IP地址上,同時在該地址上加上一個由NAT裝置選定的埠號.NAPT是使用最普遍的一種轉換方式
,它又細分為snat和dnat.
(1)源NAT(Source NAT,SNAT):修改資料包的源地址。源NAT改變第一個資料包的來源地址,它永遠會在資料包傳送到網路之前完成,資料包偽裝就是一具SNAT的例子。
(2)目的NAT(Destination NAT,DNAT):修改資料包的目的地址。Destination NAT剛好與SNAT相反,它是改變第一個資料懈的目的地地址,如平衡負載、埠轉發和透明代理就是屬於DNAT
應用:
NAT主要可以實現以下幾個功能:資料包偽裝、平衡負載、埠轉發和透明代理。
資料偽裝: 可以將內網資料包中的地址資訊更改成統一的對外地址資訊,不讓內網主機直接暴露在因特網上,保證內網主機的安全。同時,該功能也常用來實現共享上網。
埠轉發: 當內網主機對外提供服務時,由於使用的是內部私有IP地址,外網無法直接訪問。因此,需要在閘道器上進行埠轉發,將特定服務的資料包轉發給內網主機。
負載平衡: 目的地址轉換NAT可以重定向一些伺服器的連線到其他隨機選定的伺服器。
失效終結: 目的地址轉換NAT可以用來提供高可靠性的服務。如果一個系統有一臺通過路由器訪問的關鍵伺服器,一旦路由器檢測到該伺服器當機,它可以使用目的地址轉換NAT透明的把連線轉移到一個備份伺服器上。
透明代理: NAT可以把連線到因特網的HTTP連線重定向到一個指定的HTTP代理伺服器以快取資料和過濾請求。一些因特網服務提供商就使用這種技術來減少頻寬的使用而不用讓他們的客戶配置他們的瀏覽器支援代理連線
原理
地址轉換
NAT的基本工作原理是,當私有網主機和公共網主機通訊的IP包經過NAT閘道器時,將IP包中的源IP或目的IP在私有IP和NAT的公共IP之間進行轉換
要做SNAT的資訊包被新增到POSTROUTING鏈中。要做DNAT的資訊包被新增到PREROUTING鏈中。直接從本地出站的資訊包的規則被新增到OUTPUT 鏈中。
DNAT:若包是被送往PREROUTING鏈的,並且匹配了規則,則執行DNAT或REDIRECT目標。為了使資料包得到正確路由,必須在路由之前進行DNAT。
路由:核心檢查資訊包的頭資訊,尤其是資訊包的目的地。
處理本地程式產生的包:對nat表OUTPUT鏈中的規則實施規則檢查,對匹配的包執行目標動作。
SNAT:若包是被送往POSTROUTING鏈的,並且匹配了規則,則執行SNAT或MASQUERADE目標。系統在決定了資料包的路由之後才執行該鏈中的規則
但是nat也不是萬能的,它也是有缺陷的,解決辦法就是nat穿透技術:
其實NAT穿越技術依賴於UPnP協議的支援,也就是說NAT裝置必須支援UPnP,支援NAT穿越技術;而網路應用程式一樣也需要支援UPnP,支援NAT穿越技術,只不過,這通常都是通過呼叫相關的NAT Traversal API實現的,window XP預設已經安裝了NAT Traversal API,當然網路應用程式要呼叫它仍然需要進行一些修改,現在的MSN Messenger就支援呼叫NAT Traversal API. 這裡不再詳細說明,感興趣的可以查詢資料.
下面看看實際程式碼部分:
Nat的初始化工作和之前分析的filter幾乎一樣。Nat的ipv4部分在Iptables_nat.c 、Core部分在nf_nat_core.c,不同的就是表不一樣.
這裡我們拿snat一個實際例子分析,應用環境如圖:
很明顯,直接lan內pc無法與外網通訊,因為保留的ip地址即使外網能收到,但是回覆的時候路由也會丟棄.所以需要snat:
#iptables -t nat -A POSTROUTING -s 192.168.1.0/24 -j SNAT –to-source 202.20.65.5
或#iptables -t nat -A POSTROUTING -s 192.168.1.0/24 -j MASQUERADE // 預設會獲取wan口地址進行對映.
鉤子點POSTROUTING 對應SNAT PREROUTING 對應DNAt(因為會影響以後的路由);還需要說明的是不論prerouting的dnat or postrouting snat都在基本ct的後邊,helper和confirm的前面.(DNAT的優先順序高於SNAT)
參見:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
enum nf_ip_hook_priorities { NF_IP_PRI_FIRST = INT_MIN, NF_IP_PRI_CONNTRACK_DEFRAG = -400, NF_IP_PRI_RAW = -300, NF_IP_PRI_SELINUX_FIRST = -225, NF_IP_PRI_CONNTRACK = -200, NF_IP_PRI_MANGLE = -150, NF_IP_PRI_NAT_DST = -100, NF_IP_PRI_FILTER = 0, NF_IP_PRI_SECURITY = 50, NF_IP_PRI_NAT_SRC = 100, NF_IP_PRI_SELINUX_LAST = 225, NF_IP_PRI_CONNTRACK_HELPER = 300, NF_IP_PRI_CONNTRACK_CONFIRM = INT_MAX, NF_IP_PRI_LAST = INT_MAX, }; |
對於圖中lan–>wan(外網通訊)我們梳理下報文的處理流程:
第一個報文:192.168.1.3—>202.20.65.4(協議埠先忽略)
–>nf_conntrack_in(查詢ct,沒有則建立ct,prerouting)—>先snat處理,然後ipv4_helper處理,最後ipv4_confirm處理.(postrouing)
回覆的報文:202.20.65.4—>202.20.65.5
—>nf_conntrack_in,然後de-snat處理(查詢到之前建立的ct, prerouting)—->snat處理(postrouting)
下面程式碼分析,先看snat的處理:
hook函式為nf_nat_ipv4_out它呼叫了核心處理函式nf_nat_ipv4_fn:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
static unsigned int nf_nat_ipv4_fn(unsigned int hooknum, struct sk_buff *skb, const struct net_device *in, const struct net_device *out, int (*okfn)(struct sk_buff *)) { struct nf_conn *ct; enum ip_conntrack_info ctinfo; struct nf_conn_nat *nat; /* maniptype == SRC for postrouting. */ enum nf_nat_manip_type maniptype = HOOK2MANIP(hooknum); /* We never see fragments: conntrack defrags on pre-routing * and local-out, and nf_nat_out protects post-routing. */ NF_CT_ASSERT(!ip_is_fragment(ip_hdr(skb))); ct = nf_ct_get(skb, &ctinfo); /* Can't track? It's not due to stress, or conntrack would * have dropped it. Hence it's the user's responsibilty to * packet filter it out, or implement conntrack/NAT for that * protocol. 8) --RR */ if (!ct) return NF_ACCEPT; /* Don't try to NAT if this packet is not conntracked */ if (nf_ct_is_untracked(ct)) return NF_ACCEPT; nat = nfct_nat(ct); if (!nat) { /* NAT module was loaded late. */ if (nf_ct_is_confirmed(ct)) return NF_ACCEPT; nat = nf_ct_ext_add(ct, NF_CT_EXT_NAT, GFP_ATOMIC); if (nat == NULL) { pr_debug("failed to add NAT extension\n"); return NF_ACCEPT; } } switch (ctinfo) { case IP_CT_RELATED: case IP_CT_RELATED_REPLY: if (ip_hdr(skb)->protocol == IPPROTO_ICMP) { if (!nf_nat_icmp_reply_translation(skb, ct, ctinfo, hooknum)) return NF_DROP; else return NF_ACCEPT; } /* Fall thru... (Only ICMPs can be IP_CT_IS_REPLY) */ case IP_CT_NEW: /* Seen it before? This can happen for loopback, retrans, * or local packets. */ if (!nf_nat_initialized(ct, maniptype)) { unsigned int ret; ret = nf_nat_rule_find(skb, hooknum, in, out, ct); if (ret != NF_ACCEPT) return ret; } else { pr_debug("Already setup manip %s for ct %p\n", maniptype == NF_NAT_MANIP_SRC ? "SRC" : "DST", ct); if (nf_nat_oif_changed(hooknum, ctinfo, nat, out)) goto oif_changed; } break; default: /* ESTABLISHED */ NF_CT_ASSERT(ctinfo == IP_CT_ESTABLISHED || ctinfo == IP_CT_ESTABLISHED_REPLY); if (nf_nat_oif_changed(hooknum, ctinfo, nat, out)) goto oif_changed; } return nf_nat_packet(ct, ctinfo, hooknum, skb); oif_changed: nf_ct_kill_acct(ct, ctinfo, skb); return NF_DROP; } |
它首先根據hooknum獲取nat型別:enum nf_nat_manip_type maniptype // postrouting 為snat
獲取ct資訊.,並建立nat ext資訊,由於之前已經建立了ct,所以這裡狀態為IP_CT_NEW. ct->status未設定.
接著呼叫nf_nat_rule_find–>ipt_do_tables處理snat rules. 對於rules的處理機制流程我們已經很熟悉了.和filter不一樣的就是target的處理,
簡單看看iptables對命令的解析的ipt_ip資訊:
Ipt_do_table
先是查詢五元組的匹配ip_packet_match ipt_entry->ip(struct ipt_ip)
那麼上面的命令規則下發的ip資訊是什麼呢
In_dev :不限
Out_dev:不限
Src_ip:192.168.1.0/24
Dst_ip:不限
Protonum:不限
Sport:不限
Dport:不限
五元組匹配後,找到nat的target,解析 –to-source 202.20.65.5.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
static struct xtables_target snat_tg_reg = { .name = "SNAT", .version = XTABLES_VERSION, .family = NFPROTO_IPV4, .size = XT_ALIGN(sizeof(struct nf_nat_ipv4_multi_range_compat)), .userspacesize = XT_ALIGN(sizeof(struct nf_nat_ipv4_multi_range_compat)), .help = SNAT_help, .x6_parse = SNAT_parse, .x6_fcheck = SNAT_fcheck, .print = SNAT_print, .save = SNAT_save, .x6_options = SNAT_opts, }; |
先把ip地址資訊放在struct nf_nat_ipv4_range range中,然後和struct ipt_natinfo *info = (void *)(*cb->target)關聯其實就是填充.
1 2 3 4 5 6 7 |
/* Dest NAT data consists of a multi-range, indicating where to map to. */ struct ipt_natinfo { struct xt_entry_target t; struct nf_nat_ipv4_multi_range_compat mr; }; |
我們看看核心的nat target是如何處理的:由於是單ip引數,所以是v1
1 2 3 4 5 6 7 8 9 10 |
{ .name = "SNAT", .revision = 1, .target = xt_snat_target_v1, .targetsize = sizeof(struct nf_nat_range), .table = "nat", .hooks = (1 << NF_INET_POST_ROUTING) | (1 << NF_INET_LOCAL_IN), .me = THIS_MODULE, }, |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
static unsigned int xt_snat_target_v1(struct sk_buff *skb, const struct xt_action_param *par) { const struct nf_nat_range *range = par->targinfo; enum ip_conntrack_info ctinfo; struct nf_conn *ct; ct = nf_ct_get(skb, &ctinfo); NF_CT_ASSERT(ct != NULL && (ctinfo == IP_CT_NEW || ctinfo == IP_CT_RELATED || ctinfo == IP_CT_RELATED_REPLY)); return nf_nat_setup_info(ct, range, NF_NAT_MANIP_SRC); } |
其實nat的處理就是nf_nat_setup_info包括dnat也是.
先是獲取curr_tuple,呼叫get_unique_tuple根據curr_tuple和range資訊建立新的tuple即new_tuple
1 |
get_unique_tuple(&new_tuple, &curr_tuple, range, ct, maniptype); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
/* Manipulate the tuple into the range given. For NF_INET_POST_ROUTING, * we change the source to map into the range. For NF_INET_PRE_ROUTING * and NF_INET_LOCAL_OUT, we change the destination to map into the * range. It might not be possible to get a unique tuple, but we try. * At worst (or if we race), we will end up with a final duplicate in * __ip_conntrack_confirm and drop the packet. */ static void get_unique_tuple(struct nf_conntrack_tuple *tuple, const struct nf_conntrack_tuple *orig_tuple, const struct nf_nat_range *range, struct nf_conn *ct, enum nf_nat_manip_type maniptype) { const struct nf_nat_l3proto *l3proto; const struct nf_nat_l4proto *l4proto; struct net *net = nf_ct_net(ct); u16 zone = nf_ct_zone(ct); rcu_read_lock(); l3proto = __nf_nat_l3proto_find(orig_tuple->src.l3num); l4proto = __nf_nat_l4proto_find(orig_tuple->src.l3num, orig_tuple->dst.protonum); /* 1) If this srcip/proto/src-proto-part is currently mapped, * and that same mapping gives a unique tuple within the given * range, use that. * * This is only required for source (ie. NAT/masq) mappings. * So far, we don't do local source mappings, so multiple * manips not an issue. */ if (maniptype == NF_NAT_MANIP_SRC && !(range->flags & NF_NAT_RANGE_PROTO_RANDOM)) { /* try the original tuple first */ if (in_range(l3proto, l4proto, orig_tuple, range)) { if (!nf_nat_used_tuple(orig_tuple, ct)) { *tuple = *orig_tuple; goto out; } } else if (find_appropriate_src(net, zone, l3proto, l4proto, orig_tuple, tuple, range)) { pr_debug("get_unique_tuple: Found current src map\n"); if (!nf_nat_used_tuple(tuple, ct)) goto out; } } /* 2) Select the least-used IP/proto combination in the given range */ *tuple = *orig_tuple; find_best_ips_proto(zone, tuple, range, ct, maniptype); /* 3) The per-protocol part of the manip is made to map into * the range to make a unique tuple. */ /* Only bother mapping if it's not already in range and unique */ if (!(range->flags & NF_NAT_RANGE_PROTO_RANDOM)) { if (range->flags & NF_NAT_RANGE_PROTO_SPECIFIED) { if (l4proto->in_range(tuple, maniptype, &range->min_proto, &range->max_proto) && (range->min_proto.all == range->max_proto.all || !nf_nat_used_tuple(tuple, ct))) goto out; } else if (!nf_nat_used_tuple(tuple, ct)) { goto out; } } /* Last change: get protocol to try to obtain unique tuple. */ l4proto->unique_tuple(l3proto, tuple, range, maniptype, ct); out: rcu_read_unlock(); } |
在這個函式裡我們發現了類似ct的l3/l4協議註冊處理時的結構體,
1 2 |
const struct nf_nat_l3proto *l3proto; const struct nf_nat_l4proto *l4proto; |
關於它們的註冊:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
static int __init nf_nat_l3proto_ipv4_init(void) { int err; err = nf_nat_l4proto_register(NFPROTO_IPV4, &nf_nat_l4proto_icmp); if (err < 0) goto err1; err = nf_nat_l3proto_register(&nf_nat_l3proto_ipv4); if (err < 0) goto err2; return err; err2: nf_nat_l4proto_unregister(NFPROTO_IPV4, &nf_nat_l4proto_icmp); err1: return err; } |
1 2 3 4 5 6 7 8 9 10 11 12 |
static const struct nf_nat_l3proto nf_nat_l3proto_ipv4 = { .l3proto = NFPROTO_IPV4, .in_range = nf_nat_ipv4_in_range, .secure_port = nf_nat_ipv4_secure_port, .manip_pkt = nf_nat_ipv4_manip_pkt, .csum_update = nf_nat_ipv4_csum_update, .csum_recalc = nf_nat_ipv4_csum_recalc, .nlattr_to_range = nf_nat_ipv4_nlattr_to_range, #ifdef CONFIG_XFRM .decode_session = nf_nat_ipv4_decode_session, #endif }; |
l4例如udp的:
1 2 3 4 5 6 7 8 9 |
const struct nf_nat_l4proto nf_nat_l4proto_udp = { .l4proto = IPPROTO_UDP, .manip_pkt = udp_manip_pkt, .in_range = nf_nat_l4proto_in_range, .unique_tuple = udp_unique_tuple, #if defined(CONFIG_NF_CT_NETLINK) || defined(CONFIG_NF_CT_NETLINK_MODULE) .nlattr_to_range = nf_nat_l4proto_nlattr_to_range, #endif }; |
繼續後面的處理,判斷mainiptype 和range->flags. miainptype很明顯是src,而range->flags的值來自哪裡呢?
首先我們需要明白的是range來自使用者空間的傳遞.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
void xtables_option_tfcall(struct xtables_target *t) { if (t->x6_fcheck != NULL) { struct xt_fcheck_call cb; cb.ext_name = t->name; cb.data = t->t->data; cb.xflags = t->tflags; cb.udata = t->udata; t->x6_fcheck(&cb); } else if (t->final_check != NULL) { t->final_check(t->tflags); } if (t->x6_options != NULL) xtables_options_fcheck(t->name, t->tflags, t->x6_options); } |
而x6_fcheck:
1 2 3 4 5 6 7 8 |
static void SNAT_fcheck(struct xt_fcheck_call *cb) { static const unsigned int f = F_TO_SRC | F_RANDOM; struct nf_nat_ipv4_multi_range_compat *mr = cb->data; if ((cb->xflags & f) == f) mr->range[0].flags |= NF_NAT_RANGE_PROTO_RANDOM; } |
沒有指定埠資訊則為range.flags |= NF_NAT_RANGE_MAP_IPS;指定埠資訊則:range.flags |= NF_NAT_RANGE_PROTO_SPECIFIED;
同理dnat。在iptables命令解析的時候是先處理target然後才去check的.而range的值:
Parse_to裡先解析埠資訊,如果有的話會賦值給:
只說下單埠的情況:
1 2 3 |
range.min.tcp.port = range.max.tcp.port = htons(port); |
ip字串 ip->s_addr
range.min_ip = ip->s_addr;
range.max_ip = range.min_ip;
然後填充到entry_target的data裡
繼續回到get_unique_tuple,接著in_range返回值為0(因為ip地址唯一),我們看看find_appropriate_src做了什麼
很明顯一開始net->ct.nat_bysource為null,所以這個函式也返回0.
關鍵在find_best_ips_proto
它根據源tuple資訊和range引數,生產新的tuple(對映後的),然後nf_nat_used_tuple查詢是否已有回應報文在hash連結串列上. 最後用l4proto->unique_tuple保證tuple 的唯一性. 總之get_unique_tuple函式主要工作就是生產新的對映後的tuple。
由於ip資訊已經改變,所以new_tuple和curr_tuple肯定不一樣. 之後新對映的tuple再逆轉為repl_tuple(對映後)
呼叫nf_conntrack_alter_reply改變ct資訊:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
/* Alter reply tuple (maybe alter helper). This is for NAT, and is implicitly racy: see __nf_conntrack_confirm */ void nf_conntrack_alter_reply(struct nf_conn *ct, const struct nf_conntrack_tuple *newreply) { struct nf_conn_help *help = nfct_help(ct); /* Should be unconfirmed, so not in hash table yet */ NF_CT_ASSERT(!nf_ct_is_confirmed(ct)); pr_debug("Altering reply tuple of %p to ", ct); nf_ct_dump_tuple(newreply); ct->tuplehash[IP_CT_DIR_REPLY].tuple = *newreply; if (ct->master || (help && !hlist_empty(&help->expectations))) return; rcu_read_lock(); __nf_ct_try_assign_helper(ct, NULL, GFP_ATOMIC); rcu_read_unlock(); } |
即它改變了 ct->tuplehash[IP_CT_DIR_REPLY].tuple,其他不變.
最後處理nat ext,把nat資訊和ct關聯起來通過hlist:net->ct.nat_bysource
然後設定ct->status:
1 |
ct->status |= IPS_SRC_NAT_DONE |
既然nat規則處理完畢,剩下的工作就是處理skb裡ip報文資訊了。
根據ct裡的資訊通過nf_nat_packet修改skb指向的ip頭.
後續的鉤子函式,如果有helper處理helper;然後就是ipv4_confirmed
1 2 3 4 5 |
/* reuse the hash saved before */ hash = *(unsigned long *)&ct->tuplehash[IP_CT_DIR_REPLY].hnnode.pprev; hash = hash_bucket(hash, net); repl_hash = hash_conntrack(net, zone, &ct->tuplehash[IP_CT_DIR_REPLY].tuple); |
首先看第一個hash值,它在__nf_conntrack_alloc的時候被賦值,
Hash計算來自源五元組通過hash_conntrack_raw計算得來.那麼snat的時候,基本ct處理後就是nat了,把ct裡改變的是ct->tuplehash[IP_CT_DIR_REPLY].tuple資訊,其他不變
這樣的話 ipv4_confirm的時候,源hash不變,repl_hash重新計算(因為tuple的ip已經重新對映),然後加入inert ct的全域性連結串列net->ct.hash[hash]
既然第一報文已經順利傳送出去,那麼響應報文又是如何發給lan側的呢?
首先202.20.65.5收到報文到prerouting鉤子,nf_conntrack_in它會查詢到ct資訊
設定ctinfo 為IP_CT_ESTABLISHED_REPLY
然後看看ct timeout是否過期,然後呼叫l4proto->packet更新狀態
Ct->status : confirmed + ips_src_nat
我們知道prerouting只能進行dnat。然後會進入dnat的鉤子函式nf_nat_ipv4_in
根據ctinfo的資訊,直接進入nf_nat_packet處理.
它裡面有個關鍵部分:
1 2 3 |
/* Invert if this is reply dir. */ if (dir == IP_CT_DIR_REPLY) statusbit ^= IPS_NAT_MASK; |
然後進行dnat處理根據ct資訊修改skb指向的ip頭資訊.即所謂的de-snat.同理dnat
簡單看下五元組資訊的變化:
我們可以看看資料伍元整的流程:
Lan-wan:
Ct(snat)即ct->tuplehash資訊
orig:192.168.1.x—-202.20.65.4
reply:202.20.65.4—-202.20.65.5
回覆的報文:
查詢到ct(根據202.20.65.4—-202.20.65.5)
Prerouting上nat處理. 由於這個時候只能處理dnat,
找到ct裡源五元組即192.168.1.x—-202.20.65.4反轉為202.20.65.4—192.168.1.x
Skb根據這個資訊進行dnat對映。即完成了正常的通訊.
當然nat還有其他很多複雜的應用,這裡僅僅分析一個例項應用的流程,作為深入理解nat的開始.