网络协议栈(11)NAT转换

⽹络协议栈(11)NAT转换
⼀、NAT
为了让⼀个外部IP供多个内部主机使⽤,经常需要将⼀个主机配置为NAT服务器,从⽽对外部来看只有⼀个IP。或者说对于⼀些⽹站,可能开辟了多个服务,这些服务使⽤不同的服务器端⼝,此时单个服务器⽆法有效的完成对⽤户请求的响应,例如http服务器的80端⼝,或者说为了进⾏负荷分担,可以将同⼀个IP的服务器请求分配到多个不同的内部主机上,此时就需要进⾏⽹络地址的转换。对于多主机共享外部IP 情况,当⼀个内部IP报⽂经过NAT服务器的时候,服务器要将报⽂的源地址替换为⾃⼰的外⽹IP,对应的,解铃还须系铃⼈,NAT服务器还需要外部地址⽬的外⽹回应给NAT服务器的报⽂的⽬的地址修改为内⽹地址,从⽽让内⽹主机可以收到回应,这个就是SNAT。⽽之前说的服务器地址向内⽹地址的转换就成为DNAT(Destination Net Address Translation)。
现在只讨论⼀个内⽹地址经过⼀个NAT服务器访问外⽹的场景。假设⼀个主机只有⼀个内⽹地址192.168.0.102地址,这个是⼀个内⽹地址,在外⽹上⽆效,所以以这个地址为源向服务器tsecer.blog.163发送报⽂是⽆法得到回应的。所以此时就需要经过⼀个SNAT服务器转换,将以192.168.0.102为源的报⽂修改为以内⽹⽹关服务器的外⽹IP为源,假设说内⽹默认⽹关地址为192.168.0.1,然后⽹关还有⼀个外⽹地址110.111.112.113.。当内⽹主机发送的报⽂经过SNAT服务器的
时候,此时服务器需要修改这个报⽂的源地址为⾃⼰的外⽹地址110.11.112.113,从⽽让服务器tsecer.blog.163将回应报⽂发送给⽹关的外⽹地址,此时tsecer.blog.163服务队根本不知道有192.168.0.102这个地址,它只是认为⾃⼰是和⼀个IP为110.111.112.113的客户端交互。但是这个客户端知道⾃⼰只是在代劳⾃⼰的⼀个内⽹地址来执⾏⼀个请求。所以当应答报⽂返回之后,SNAT服务器需要把这个响应报⽂的⽬的地址修改为发起请求的内⽹地址
192.168.0.102,然后通过⾃⼰的内⽹IP192.168.0.1将响应返回给内⽹请求者。
这⾥就有⼀些问题,
北京网通宽带
⼀个SNAT服务器可能要为多个内⽹IP提供服务,作为他们的代理,然后将对应的请求再返还给原始请求者。此时服务器就必须能够区分这些请求是谁启动的,这就需要有⼀个连接跟踪功能,也就是内核中的conntrack功能。
假设有两个不同的内⽹IP,分别为192.168.0.102和192.168.01.03的主机,它们要访问同⼀个服务器,例如le.hk(⽬的端⼝都是80),⽆巧不成书,它们本地的TCP端⼝也都是1234,此时它们同时通过SNAT来进⾏访问。SNAT本着修改源地址的精神,将两个报⽂的源地址都修改为⾃⼰的外⽹IP110.111.112.113,但是如果对⽅的响应回来的时候,SNAT服务器如何区分应该将它们分别返还给哪个内部主机呢?(如果派发错误,那么A搜索“开始”,B搜索“结果”,但是它们得到的搜索⽹页
可能会刚好相反)。
⼆、conntrack
这个是netfilter功能⾄上的⼀个应⽤。⽹络就是分层⽐较明显,所以看代码的时候也要按照分层的思想来考虑实现。在这⾥netfilter是最为底层的⼀个钩⼦机制,在这个基础上可以实现防⽕墙功能,netlink功能等各种功能,⽽conntrack只是netfilter的⼀个引⽤实例,正如FTP、HTTP都是TCP的⼀个应⽤实例⼀样。再向上,之后即将介绍的NAT则是在conntrack之上的有⼀个应⽤实例,它依赖于conntrack机制,但是反过来并不成⽴。
conntrack的功能就是要跟踪系统中的各种虚拟链路的连接情况,正常情况下,⽹络中的每个报⽂都会期待有⼀个回应,所以内核中为每个链路分配了⼀个struct nf_conn结构,⽤这个结构来表⽰⼀条链路。有路就有⾏⼈,这⾥的⾏⼈就是skbuff结构,由于⼀个链路上可以有任意多的报⽂,所以在每个报⽂struct sk_buff 中可能有⼀个
#ifdef CONFIG_NETFILTER
struct nf_conntrack    *nfct;
指针,通过这个指针,由于这个nf_conntrack可能是⼀个
struct nf_conn
{
/* Usage count in here is 1 for hash table/destruct timer, 1 per skb,
plus 1 for any connection(s) we are `master' for */
struct nf_conntrack ct_general;
结构的第⼀个成员,所以通过这个指针就可以得到这个报⽂所属的nf_conn实例(事实上,这个指针经过类型转换就是⼀个nf_conn结构),这⼀点的转换可以参考
static inline struct ip_conntrack *
ip_conntrack_get(const struct sk_buff *skb, enum ip_conntrack_info *ctinfo)
{
*ctinfo = skb->nfctinfo;
return (struct ip_conntrack *)skb->nfct;
}
1、创建
resolve_normal_ct--->>>init_conntrack
中将会创建⼀个新的nf_conn实例,为了有⼀个感官的认识,还是先放⼀个调⽤链,可以看到,它是在执⾏⼀个netfilter钩⼦函数的时候路径此地
(gdb) bt
#0  resolve_normal_ct (ctinfo=0xcff979dc, set_reply=0xcff979d0,
l4proto=0xc0a5eca0, l3proto=0xc0a5eb80, protonum=1 '\001', l3num=2,
回程误差
dataoff=20, skb=0xcfd94600) at net/netfilter/nf_conntrack_core.c:809
#1  nf_conntrack_in (ctinfo=0xcff979dc, set_reply=0xcff979d0,
l4proto=0xc0a5eca0, l3proto=0xc0a5eb80, protonum=1 '\001', l3num=2,
dataoff=20, skb=0xcfd94600) at net/netfilter/nf_conntrack_core.c:851
#2  0xc07ab769 in ipv4_conntrack_local (hooknum=3, pskb=0xcff97b34, in=0x0,
out=0xcff7a000, okfn=0xc0742ec0 <dst_output>)
at net/ipv4/netfilter/nf_conntrack_l3proto_ipv4.c:207
#3  0xc071118d in nf_iterate (head=0xc0a7a618, skb=0xcff97b34, hook=3,
indev=0x0, outdev=0xcff7a000, i=0xcff97b0c, okfn=0xc0742ec0 <dst_output>,
hook_thresh=-2147483648) at net/netfilter/core.c:145
#4  0xc071127f in nf_hook_slow (pf=2, hook=3, pskb=0xcff97b34, indev=0x0,
outdev=0xcff7a000, okfn=0xc0742ec0 <dst_output>, hook_thresh=-2147483648)
at net/netfilter/core.c:181
#5  0xc0749bef in nf_hook_thresh (cond=1, thresh=-2147483648,
okfn=0xc0742ec0 <dst_output>, outdev=0xcff7a000, indev=0x0,
pskb=0xcff97b34, hook=3, pf=2) at include/linux/netfilter.h:211
#6  ip_push_pending_frames (cond=1, thresh=-2147483648,
okfn=0xc0742ec0 <dst_output>, outdev=0xcff7a000, indev=0x0,
pskb=0xcff97b34, hook=3, pf=2) at net/ipv4/ip_output.c:1259
克莱斯勒pt巡洋舰#7  0xc078574a in raw_sendmsg (iocb=0xcff97de0, sk=0xc13819a0, msg=0xcff97eb8,
len=64) at net/ipv4/raw.c:518
#8  0xc07989e2 in inet_sendmsg (iocb=0xcff97de0, sock=0xcfcae680,
msg=0xcff97eb8, size=64) at net/ipv4/af_inet.c:667
#9  0xc06d6338 in __sock_sendmsg (size=64, msg=0xcff97eb8, sock=0xcfcae680,
iocb=0xcff97de0) at net/socket.c:553
#10 sock_sendmsg (size=64, msg=0xcff97eb8, sock=0xcfcae680, iocb=0xcff97de0)
at net/socket.c:564
#11 0xc06d81c4 in sys_sendto (fd=3, buff=0xbfcfd640, len=64, flags=0,
addr=0x81db5ec, addr_len=28) at net/socket.c:1573
#12 0xc06d8e98 in sys_socketcall (call=11, args=0xbfcfd5fc)
at net/socket.c:2022
长株潭报在resolve_normal_ct中,从报⽂sk_buff结构中以及函数传⼊参数中提取出⼀些信息来组成⼀个元组(tuple),这些tuple可以认为是这个连接的⼀些关键元素,它躲到⾜以和系统中其它的连接区分开来。例如,对于TCP来说,本地可以和某个远程主机建⽴任意多个连接,例如,和的80端⼝通过本地1234连接,并且还有⼀个链路和的21端⼝通过4321本地端⼝连接。所以对于⼀个链路,只有源和⽬的IP地址是不够的,对于TCP来说还要有端⼝号,对于UDP也是如此,那么ICMP呢,不同的协议可能需要不同的附加信息来区分不同的链路,所以在resolve_normal_ct--->>>ip_ct_get_tuple--->>>protocol->pkt_to_tuple(skb, dataoff, tuple)
调⽤了⼀个更底层的协议的pkt_to_tuple,从⽽在元组中添加更多的协议相关信息。例如对于TCP可以参考是ip_conntrack_protocol_tcp--->>>tcp_pkt_to_tuple
tuple->p.port = hp->source;
tuple->p.port = hp->dest;
其中加⼊了端⼝号信息。
构建好了这个元组,就可以通过ip_conntrack_find_get函数来查这个元素是否存在,如果存在则说明这个链路之前已经存在,所以给这个sk_buff打个标签,表⽰这个报⽂属于这个链路,操作就是通过
skb->nfct = &ct->ct_general;这⾥的ct_general是结构的第⼀个元素,所以相当于取到了结构的⾸地址。
skb->nfctinfo = *ctinfo;
如果说这个链路还没有被发现,那么可以调⽤init_conntrack接⼝来创建⼀个链路。在init_conntrack函数中,参数传⼊的原始tuple是已经初始化过的⼀个元组,但是它的回应还没有确定,这个也没有关系,⼀般情况下,请求和应答都是对应的,只是⽬的地址、源地址、⽬的端⼝、源端⼝之类的互换,所以其中通过对原始元组执⾏ip_ct_invert_tuple函数得到期望的响应元组(元组中包含了报⽂的⽬的地址、源地址及端⼝之类的信息,总之这个元组是⼀个具有⽅向性的结构)。在ip_conntrack_alloc函数中完成两个元组的各⾃归位,代码中有⼀些值得注意的问题
conntrack->tuplehash[IP_CT_DIR_ORIGINAL].tuple = *orig;这⾥在的⽅向性已经体现出来,orig位于原始项,⽽回应放置于第⼆项
conntrack->tuplehash[IP_CT_DIR_REPLY].tuple = *repl;
……
list_add(&conntrack->tuplehash[IP_CT_DIR_ORIGINAL].list, &unconfirmed);新创建的⼀对元组中只有第⼀个键⼊链表,并且防⽌在unconfirmed链表,这个是独⽴于__ip_conntrack_find使⽤的ip_conntrack_hash链表,所以当使⽤ip_conntrack_find_get是⽆法到这个链路。
2、NAT加⼊
如果说源和⽬的就是这么简单的镜像,那存在的意义就不⼤了,此时就需要有⼀些NAT加⼊进来,事情就有意思了。
ipt_snat_target--->>>nf_nat_setup_info--->>get_unique_tuple--->>>find_best_ips_proto(这⾥修改了IP地址,但是端⼝并没有变化)。同样滴,端⼝的转换需要通过
中华印刷通史proto->unique_tuple(tuple, range, maniptype, ct)
来实现,同样以TCP为例,它执⾏的是tcp_unique_tuple中查本机中尚未使⽤的端⼝号。这⾥就解决了两个内⽹IP同时访问同⼀个外⽹IP 的问题,这⾥可以保证不仅修改IP,还修改源PORT,这个PORT只要保证SNAT服务器上唯⼀就⾏。函数tcp_unique_tuple中也有⼀些细节,⽐如,它将端⼝分为三个区间1、512、1024、并且试图保证端⼝范围的⼀致性。例如,4端⼝尽量映射到1到512之间。
在经过⼀系列的get_unique_tuple之后,将会获得⼀个唯⼀的连接标⽰。还是以之前的例⼦说明
转换动作①内⽹访问② get_unique_tuple (IP_NAT_MANIP_SRC)                  ③nf_ct_invert_tuplepr
srcIP    192.168.0.102                          srcIP    110.11.112.113                                              srcIP    tsecer.blog.163
dstIP      tsecer.blog.163              dstIP      tsecer.blog.163                                  dstIP      110.11.112.113
srcPort  1234                                        srcPort  4321(随机值)                                          srcPort  80
dstPort  80                                            dstPort  80                                                                dstPort  4321
经过最后的转换,
get_unique_tuple(&new_tuple, &curr_tuple, range, ct, maniptype);
if (!nf_ct_tuple_equal(&new_tuple, &curr_tuple)) {
struct nf_conntrack_tuple reply;
/* Alter conntrack table so will recognize replies. */
nf_ct_invert_tuplepr(&reply, &new_tuple);
nf_conntrack_alter_reply(ct, &reply);之类的reply元组的内容如最后的第三列所⽰。这⾥开始出现了严重的不对称现象,这⼀点也是之后完成⾃动SNAT转换的基础。
⾄于
nf_nat_setup_info(struct nf_conn *ct,
const struct nf_nat_range *range,
城市经济学unsigned int hooknum)
函数中的range参数从哪⾥来,这个应该是通过iptables传递给内核的,内核在⾃⼰的ipt_entry的ipt_target(xt_target)中保存,从⽽可以完成正确的区间分配。
3、NAT转换
假设链路已经建⽴,此时有⼀个报⽂以上节中的形式出现,并到达NAT服务器,它同样从中提取出tuple,然后会在ip_conntrack_hash中搜素到这个链路。在这个报⽂离开NAT服务器的时候,它会尝试对报⽂进⾏NAT转换,通过原始元组,它可以到它对应的应答元组,也就是上节中第三列的形式,但是这是⼀个绝对的回⾳报⽂,所以还要将其中的第三列再次进⾏invert转换,也即转换为
③、原始reply报⽂④、最后离开NAT服务器的报⽂
srcIP    tsecer.blog.163                    srcIP    110.11.112.113
dstIP      110.11.112.113                              dstIP      tsecer.blog.163
srcPort  80                                                srcPort    4321
dstPort  4321                                            dstPort  80
这个规则对应从外⽹服务器返回的报⽂同样适⽤,也就是如果收到⼀个由③表⽰的报⽂,经过对original报⽂执⾏revert同样可以得到正确的响应报⽂。关于这个转换动作可以参考nf_nat_packet函数中实现,对应代码为
/* Non-atomic: these bits don't change. */
if (ct->status & statusbit) {
struct nf_conntrack_tuple target;
/* We are aiming to look like inverse of other direction. */
nf_ct_invert_tuplepr(&target, &ct->tuplehash[!dir].tuple);这⾥为转换核⼼,⾸先是⽅向取反,然后元组取反。
if (!manip_pkt(target.dst.protonum, pskb, 0, &target, mtype))
return NF_DROP;
}
4、何时从unconfirmed移动到nf_conntrack_hash
nf_conntrack_confirm--->>>__nf_conntrack_confirm--->>>__nf_conntrack_hash_insert
static void __nf_conntrack_hash_insert(struct nf_conn *ct,
unsigned int hash,
unsigned int repl_hash)
{
ct->id = ++nf_conntrack_next_id;
list_add(&ct->tuplehash[IP_CT_DIR_ORIGINAL].list,
&nf_conntrack_hash[hash]);
list_add(&ct->tuplehash[IP_CT_DIR_REPLY].list,
&nf_conntrack_hash[repl_hash]);
}
这⾥⼤致的意思是这个conntrack跟踪挂载在优先级很⾼的⼀个hook上,⾼到早于netfilter的包过滤机制,这样,如果在过滤之前建⽴了⼀个连接,但是经过防⽕墙之后这个报⽂被丢失掉了,那么此时这个连接就永远也⽆法建⽴起来了。反过来说,如果⼀个unconfirmed的连接报⽂英勇的穿过了防⽕墙,那么这个连接就可以被确认了。
下⾯是各种钩⼦的优先级,可以看到,NF_IP_PRI_CONNTRACK 在防⽕墙NF_IP_PRI_FILTER之前执⾏的,⽽确认动作
NF_IP_PRI_CONNTRACK_CONFIRM的优先级最低,也就是最后被执⾏,这也意味着加⼊到nf_conntrack_hash中的元组是经过了所有的NAT转换之后的元组。再起强调,netfilter是⼀个三维结构
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_NAT_SRC = 100,
NF_IP_PRI_SELINUX_LAST = 225,
NF_IP_PRI_CONNTRACK_HELPER = INT_MAX - 2,
NF_IP_PRI_NAT_SEQ_ADJUST = INT_MAX - 1,
NF_IP_PRI_CONNTRACK_CONFIRM = INT_MAX,
NF_IP_PRI_LAST = INT_MAX,
};

本文发布于:2024-09-25 02:26:37,感谢您对本站的认可!

本文链接:https://www.17tex.com/xueshu/349527.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:服务器   元组   地址   链路   连接   请求
留言与评论(共有 0 条评论)
   
验证码:
Copyright ©2019-2024 Comsenz Inc.Powered by © 易纺专利技术学习网 豫ICP备2022007602号 豫公网安备41160202000603 站长QQ:729038198 关于我们 投诉建议