- 论坛徽章:
- 0
|
要过年了,发个贴庆祝新年。
作者:九贱
内核版本:2.6.12
个人主页:www.skynet.org.cn
欢迎大家讨论,错误之处,敬请指正。转贴请注明作者及出处。
网络地址转换的实现初步分析
网络地址转换(NAT),从本质上来讲,是通过修改IP数据首部中的地址,以实现将一个地址转换成另一个地址的技术。当然,在某些情况下,修改的不仅仅是IP首部的来源或目的地址,还包括其它要素。
随着接入Internet的计算机数量的不断猛增,IP地址资源也就愈加显得捉襟见肘。目前NAT技术更多地被使用在将一个私网IP地址网段,转换为一个或几个公网IP地址,以实现私网与Internet的互相通讯。
Netfilter在连接跟踪的基础上,实现了两种类型的地址转换:源地址转换和目的地址转换。顾名思义,源地址转换就是修改IP包中的源地址(或许还有源端口),而目的地址转换,就是修改IP包中的目的地址(同样,或许还有目的端口)。前者通常用于将内网主机私网地址转换为公网地址,访问Internet,后者通常用于将公网IP地址转换为一个或几个私网地址,实现向互联网提供服务。
模块初始化
NAT模块对应的源文件是ip_nat_standard.c,同样地,init_or_cleanup是它的初始化函数:
- static int init_or_cleanup(int init)
- {
- int ret = 0;
- need_ip_conntrack();
- if (!init) goto cleanup;
- /*初始化nat规则*/
- ret = ip_nat_rule_init();
- if (ret < 0) {
- printk("ip_nat_init: can't setup rules.\n");
- goto cleanup_nothing;
- }
- /*初始化nat所需要重要数据结构*/
- ret = ip_nat_init();
- if (ret < 0) {
- printk("ip_nat_init: can't setup rules.\n");
- goto cleanup_rule_init;
- }
- /*注册Hook*/
- ret = nf_register_hook(&ip_nat_in_ops);
- ……
- /*卸载各个注册的模块,释放初始化时申请的资源*/
- cleanup:
- ……
- return ret;
- }
复制代码
函数主要完成四个工作:
- NAT规则表的初始化;
- NAT所需的重要的数据结构的初始化;
- 注册Hook;
- 完成各清除工作;
ip_nat_rule_init工作很简单,注册NAT表和两个target:源地址转换(SNAT)和目的地址转换(DNAT):
- int __init ip_nat_rule_init(void)
- {
- int ret;
- /*注册NAT表*/
- ret = ipt_register_table(&nat_table, &nat_initial_table.repl);
- if (ret != 0)
- return ret;
- /*注册SNAT Target */
- ret = ipt_register_target(&ipt_snat_reg);
- if (ret != 0)
- goto unregister_table;
- /*注册DNAT Target*/
- ret = ipt_register_target(&ipt_dnat_reg);
- if (ret != 0)
- goto unregister_snat;
- return ret;
- }
复制代码
这些函数在filter表中已经详细分析过了,读者可以对应结构成员的赋值,自行分析。需要注意的是对SNAT和DNAT两个模块的注册,它们的target处理函数分别是ipt_snat_target和ipt_dnat_target,这个target与包过滤中的target没有质的区别,都是规则的动作部份,只是完成的功能不同罢了——它们的工作是地址转换,而包过滤中的target是拦截、放行之类的。
规则初始化之外的动作,是在ip_nat_init函数中完成的,这个初始化函数同连接跟踪的初始化非常相似:
- int __init ip_nat_init(void)
- {
- size_t i;
- /* 设置nat的hash表的大小 */
- ip_nat_htable_size = ip_conntrack_htable_size;
- /*同连接跟踪一样,nat的hash表也要维护一个list_head结构的hash链表*/
- bysource = vmalloc(sizeof(struct list_head) * ip_nat_htable_size);
- if (!bysource)
- return -ENOMEM;
- /* 初始化内建协议 */
- WRITE_LOCK(&ip_nat_lock);
- for (i = 0; i < MAX_IP_NAT_PROTO; i++)
- ip_nat_protos[i] = &ip_nat_unknown_protocol;
- ip_nat_protos[IPPROTO_TCP] = &ip_nat_protocol_tcp;
- ip_nat_protos[IPPROTO_UDP] = &ip_nat_protocol_udp;
- ip_nat_protos[IPPROTO_ICMP] = &ip_nat_protocol_icmp;
- WRITE_UNLOCK(&ip_nat_lock);
- /*初始化hash表*/
- for (i = 0; i < ip_nat_htable_size; i++) {
- INIT_LIST_HEAD(&bysource[i]);
- }
- /* FIXME: Man, this is a hack. <SIGH> */
- IP_NF_ASSERT(ip_conntrack_destroyed == NULL);
- ip_conntrack_destroyed = &ip_nat_cleanup_conntrack;
- /* Initialize fake conntrack so that NAT will skip it */
- ip_conntrack_untracked.status |= IPS_NAT_DONE_MASK;
- return 0;
- }
复制代码
Hook的注册,我们已经反复地遇到了,NAT也不例外,它需要在各个关键的Hook点上注册自己的Hook,我们仍假设Linux做为一个网关型设备,关心它在PREROUTING和POSTROUTING两个Hook点上注册的Hook,因为它们完成了最重要的源地址转换和目的地址转换:
- /* 目的地址转换的Hook,在filter包过滤之前进行 */
- static struct nf_hook_ops ip_nat_in_ops = {
- .hook = ip_nat_in,
- .owner = THIS_MODULE,
- .pf = PF_INET,
- .hooknum = NF_IP_PRE_ROUTING,
- .priority = NF_IP_PRI_NAT_DST,
- };
- /*源地址转换,在filter包过滤之后*/
- static struct nf_hook_ops ip_nat_out_ops = {
- .hook = ip_nat_out,
- .owner = THIS_MODULE,
- .pf = PF_INET,
- .hooknum = NF_IP_POST_ROUTING,
- .priority = NF_IP_PRI_NAT_SRC,
- };
复制代码
源地址转换
源地址转换注册在NF_IP_POST_ROUTING,数据包在包过滤之后,会进入ip_nat_out函数。在分析这个函数之前,我们需要理解的是,源地址转换如何进行?
源地址的转换最终要做的工作,就是修改IP包中的源地址,将其替换为iptables添加规则时指定的“转换后地址”,对于绝大多数应用而言,一般是将私网IP地址修改为公网IP地址,然后将数据包发送出去。但是,很自然地,这样修改后,回来的应答数据包没有办法知道它转换之前的样子,也就是不知道真实的来源主机(对于回应包,也就是不知道把数据应答给谁),数据包将被丢弃,所以有必要,维护一张地址转换表,详细记录数据包的转换情况,以使NAT后的数据能交互地传输。
对于许多无状态检测功能的NAT技术,这个记录转换情况的表就是一张NAT会话表,对于Netfilter而言,已经为进出数据包建立了一张状态跟踪表,自然也就没有必要重新多维护一张表了,也就是,合理地利用状态跟踪表,实现对NAT状态的跟踪和维护。
回忆一下连接跟踪的情况,当数据包进入连接跟踪后,会建立一个tuple以及相应的replay tuple,而应答的数据包,会查找与之匹配的repaly tuple,——对于源地址转换而言,应答包中的目的地址,将是转换后的地址,而不是真实的地址,所以,为了让应答的数据包能找到对应的replay tuple,很自然地,NAT模块应该修改replaly tuple中的目的地址,以使应答数据包能找到属于自己的replay,如下图。
所以,源地址转换的主要步骤大致如下:
1. 数据包进入Hook函数后,进行规则匹配;
2. 如果所有match都匹备,则进行SNAT模块的动作,即snat 的target模块;
3. 源地址转换的规则一般是…… -j SNAT –to X.X.X.X,SNAT用规则中预设的转换后地址X.X.X.X,修改连接跟踪表中的replay tuple;
4. 接着,修改数据包的来源的地址,将它替换成replay tuple中的相应地址,即规则中预设的地址,将其发送出去;
5. 对于回来的数据包,应该能在状态跟踪表中,查找与之对应的replay tuple,也就能顺藤摸瓜地找到原始的tuple中的信息,将应答包中的目的地址改回来,这样,整个数据传送就得以顺利转发了;
当然,这些只是最精简单的步骤,因为有许多重要的因素没有考虑到,比如:
1. 上面的步骤只是对应一个来源IP的一个连接,但是一台主机在同时可能发出N个连接,所以应该有相应的解决办法;
2. 同样的,转换后地址可能是一段地址池;
3. 动态协议,如FTP,应该有相应的NAT方法;
我们还是从最简单的开始,ip_nat_out是SNAT的Hook函数。
[ 本帖最后由 独孤九贱 于 2007-1-27 21:44 编辑 ] |
|