网络协议栈(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.com发送报文是无法得到回应的。所以此时就需要经过一个SNAT服务器转换,将以192.168.0.102为源的报文修改为以内网网关服务器的外网IP为源,假设说内网默认网关地址为192.168.0.1,然后网关还有一个外网地址110.111.112.113.。当内网主机发送的报文经过SNAT服务器的时候,此时服务器需要修改这个报文的源地址为自己的外网地址110.11.112.113,从而让服务器tsecer.blog.163.com将回应报文发送给网关的外网地址,此时tsecer.blog.163.com服务队根本不知道有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的主机,它们要访问同一个服务器,例如www.google.com.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
#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来说,本地可以和某个远程主机建立任意多个连接,例如,和www.gnu.org的80端口通过本地1234连接,并且还有一个链路和www.gnu.org的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->src.u.tcp.port = hp->source;
    tuple->dst.u.tcp.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.com
                       dstIP      tsecer.blog.163.com               dstIP      tsecer.blog.163.com                                   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.com                    srcIP     110.11.112.113
dstIP      110.11.112.113                              dstIP      tsecer.blog.163.com 
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,
};

posted on 2019-03-06 21:04  tsecer  阅读(424)  评论(0编辑  收藏  举报

导航