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.netfilter的NAT实现原理
前面简要介绍 了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协议还需要另外的辅助模块处理,再次就略去,感兴趣的可以继续研究源代码以及其他相关资料。