重读nat

  目前内核NAT 是基于nf_conntrack连接跟踪实现。

首先看下conntrack的相关知识!

/*
 struct sk_buff {

      struct nf_conntrack *nfct;//指向struct nf_conn实例
      ..............
}; 
*/
//最主要的就是tuplehash(跟踪连接双方向数据)和status(记录连接状态)
struct nf_conn {//每个struct nf_conn实例代表一个连接。每个skb都有一个指针,指向和它相关联的连接。
    /* Usage count in here is 1 for hash table/destruct timer, 1 per skb,
     * plus 1 for any connection(s) we are `master' for
     *
     * Hint, SKB address this struct and refcnt via skb->nfct and
     * helpers nf_conntrack_get() and nf_conntrack_put().
     * Helper nf_ct_put() equals nf_conntrack_put() by dec refcnt,
     * beware nf_ct_get() is different and don't inc refcnt.
     */
    struct nf_conntrack ct_general; //对连接的引用计数

    spinlock_t    lock;
    u16        cpu;

    /* These are my tuples; original and reply */
    /* Connection tracking(链接跟踪)用来跟踪、记录每个链接的信息(目前仅支持IP协议的连接跟踪)。
            每个链接由“tuple”来唯一标识,这里的“tuple”对不同的协议会有不同的含义,例如对tcp,udp
                 来说就是五元组: (源IP,源端口,目的IP, 目的端口,协议号),对ICMP协议来说是: (源IP, 目
            的IP, id, type, code), 其中id,type与code都是icmp协议的信息。链接跟踪是防火墙实现状态检
            测的基础,很多功能都需要借助链接跟踪才能实现,例如NAT、快速转发、等等。 */
            
    struct nf_conntrack_tuple_hash tuplehash[IP_CT_DIR_MAX];//正向和反向的连接元组信息。
/* 这是一个位图,是一个状态域。在实际的使用中,它通常与一个枚举类型ip_conntrack_status(位于include/linux/netfilter_ipv4/ip_conntrack.h,Line33)
进行位运算来判断连接的状态。其中主要的状态包括:

IPS_EXPECTED(_BIT),表示一个预期的连接

IPS_SEEN_REPLY(_BIT),表示一个双向的连接

IPS_ASSURED(_BIT),表示这个连接即使发生超时也不能提早被删除

IPS_CONFIRMED(_BIT),表示这个连接已经被确认(初始包已经发出) */
 /* 可以设置由enum ip_conntrack_status中描述的状态 */
    /* Have we seen traffic both ways yet? (bitset) */
    unsigned long status;//该连接的连接状态 由enum ip_conntrack_status中描述的状态

    /* Timer function; drops refcnt when it goes off. */
    struct timer_list timeout; //连接垃圾回收定时器  连接跟踪的超时时间 

    possible_net_t ct_net;

    /* all members below initialized via memset */
    u8 __nfct_init_offset[0];
 /*结构ip_conntrack_expect位于ip_conntrack.h,这个结构用于将一个预期的连接分配给现有的连接,也就是说本连接是这个master的一个预期连接*/
    /* If we were expected by an expectation, this will be it */
    struct nf_conn *master;//如果该连接是期望连接,指向跟其关联的主连接

#if defined(CONFIG_NF_CONNTRACK_MARK)
    u_int32_t mark;
#endif

#ifdef CONFIG_NF_CONNTRACK_SECMARK
    u_int32_t secmark;
#endif

    /* Extensions */ /*指向扩展结构,该结构中包含一些基于连接的功能扩展处理函数 */
    struct nf_ct_ext *ext;

    /* Storage reserved for other modules, must be the last member */
    union nf_conntrack_proto proto; /*存储特定协议的连接跟踪信息 也就是不同协议实现连接跟踪的额外参数 */
};
/* Connection state tracking for netfilter.  This is separated from,
   but required by, the NAT layer; it can also be used by an iptables
   extension. */
enum ip_conntrack_info {
    /* Part of an established connection (either direction).   表示这个数据包对应的连接在两个方向都有数据包通过,
    并且这是ORIGINAL初始方向数据包(无论是TCP、UDP、ICMP数据包,
     只要在该连接的两个方向上已有数据包通过,就会将该连接设置为IP_CT_ESTABLISHED状态。不会根据协议中的标志位进行判断,
     例如TCP的SYN等)。但它表示不了这是第几个数据包,也说明不了这个CT是否是子连接。*/
    IP_CT_ESTABLISHED,

    /* Like NEW, but related to an existing connection, or ICMP error
       (in either direction). 表示这个数据包对应的连接还没有REPLY方向数据包,当前数据包是ORIGINAL方向数据包。
       并且这个连接关联一个已有的连接,是该已有连接的子连接,?       ??tatus标志中已经设置了IPS_EXPECTED标志,该标志在init_conntrack()函数中设置)。但无法
判断是第几个数据包(不一定是第一个)*/
    IP_CT_RELATED,

    /* Started a new connection to track (only
           IP_CT_DIR_ORIGINAL); may be a retransmission. 
           表示这个数据包对应的连接还没有REPLY方向数据包,当前数据包是ORIGINAL方向数据包,该连接不是子连接。但无法判断是
第几个数据包(不一定是第一个*/
    IP_CT_NEW,

    /* >= this indicates reply direction这个状态一般不单独使用,通常以下面两种方式使用 */
    IP_CT_IS_REPLY,
/* 表示这个数据包对应的连接在两个方向都有数据包通过,并且这是REPLY应答方向数据包。但它表示不了这是
    第几个数据包,也说明不了这个CT是否是子连接。*/
    IP_CT_ESTABLISHED_REPLY = IP_CT_ESTABLISHED + IP_CT_IS_REPLY,
    /*这个状态仅在nf_conntrack_attach()函数中设置,用于本机返回REJECT,例如返回一个ICMP目的不可达报文,
      或返回一个reset报文。它表示不了这是第几个数据包
    */
    IP_CT_RELATED_REPLY = IP_CT_RELATED + IP_CT_IS_REPLY,
    /* No NEW in reply direction. */

    /* Number of distinct IP_CT types. */
    IP_CT_NUMBER,
};
/* Bitset representing status of connection. */
enum ip_conntrack_status {
    /* It's an expected connection: bit 0 set.  This bit never changed */
    IPS_EXPECTED_BIT = 0,//表示该连接是个子连接 
    IPS_EXPECTED = (1 << IPS_EXPECTED_BIT),

    /* We've seen packets both ways: bit 1 set.  Can be set, not unset. */
    IPS_SEEN_REPLY_BIT = 1,//表示该连接上双方向上都有数据包了 
    IPS_SEEN_REPLY = (1 << IPS_SEEN_REPLY_BIT),

    /* Conntrack should never be early-expired. 
    TCP:在三次握手建立完连接后即设定该标志。UDP:如果在该连接上的两个方向都有数据包通过,则再有数据包在该连接上通过时?    就设定该标志。ICMP:不设置该标志
    */
    IPS_ASSURED_BIT = 2,

    IPS_ASSURED = (1 << IPS_ASSURED_BIT),

    /* Connection is confirmed: originating packet has left box 
    表示该连接已被添加到net->ct.hash表*/
    IPS_CONFIRMED_BIT = 3,
    IPS_CONFIRMED = (1 << IPS_CONFIRMED_BIT),

    /* Connection needs src nat in orig dir.  This bit never changed. 
    在POSTROUTING处,当替换reply tuple完成时, 设置该标记*/
    IPS_SRC_NAT_BIT = 4,
    IPS_SRC_NAT = (1 << IPS_SRC_NAT_BIT),

    /* Connection needs dst nat in orig dir.  This bit never changed. 
    在PREROUTING处,当替换reply tuple完成时, 设置该标记*/
    IPS_DST_NAT_BIT = 5,
    IPS_DST_NAT = (1 << IPS_DST_NAT_BIT),

    /* Both together. */
    IPS_NAT_MASK = (IPS_DST_NAT | IPS_SRC_NAT),

    /* Connection needs TCP sequence adjusted. */
    IPS_SEQ_ADJUST_BIT = 6,
    IPS_SEQ_ADJUST = (1 << IPS_SEQ_ADJUST_BIT),

    /* NAT initialization bits.  在POSTROUTING处,已被SNAT处理,并被加入到bysource链中,设置该标记*/
    IPS_SRC_NAT_DONE_BIT = 7,
    IPS_SRC_NAT_DONE = (1 << IPS_SRC_NAT_DONE_BIT),

    IPS_DST_NAT_DONE_BIT = 8,//在PREROUTING处,已被DNAT处理,并被加入到bysource链中,设置该标记
    IPS_DST_NAT_DONE = (1 << IPS_DST_NAT_DONE_BIT),

    /* Both together */
    IPS_NAT_DONE_MASK = (IPS_DST_NAT_DONE | IPS_SRC_NAT_DONE),

    /* Connection is dying (removed from lists), can not be unset. 
    表示该连接正在被释放,内核通过该标志保证正在被释放的ct不会被其它地方再次引用。有了这个标志,当某个连接要被删
      除时,即使它还在net->ct.hash中,也不会再次被引 用*/
    IPS_DYING_BIT = 9,
    IPS_DYING = (1 << IPS_DYING_BIT),

    /* Connection has fixed timeout. 
     固定连接超时时间,这将不根据状态修改连接超时时间。通过函数nf_ct_refresh_acct()修改超时时间时检查该标志*/
    IPS_FIXED_TIMEOUT_BIT = 10,
    IPS_FIXED_TIMEOUT = (1 << IPS_FIXED_TIMEOUT_BIT),

    /* Conntrack is a template 
     由CT target进行设置(这个target只能用在raw表中,用于为数据包构建指定ct,并打上该标志),用于表明这个ct是由CT target创建*/
    IPS_TEMPLATE_BIT = 11,
    IPS_TEMPLATE = (1 << IPS_TEMPLATE_BIT),

    /* Conntrack is a fake untracked entry */
    IPS_UNTRACKED_BIT = 12,
    IPS_UNTRACKED = (1 << IPS_UNTRACKED_BIT),

    /* Conntrack got a helper explicitly attached via CT target. */
    IPS_HELPER_BIT = 13,
    IPS_HELPER = (1 << IPS_HELPER_BIT),
};

三层协议(IPv4/IPv6)

利用nf_conntrack_proto.c文件中的nf_conntrack_l3proto_registe 注册三层协议处理函数

 

 

 四层协议(TCP/UDP)

 利用 nf_conntrack_l4proto_register(struct nf_conntrack_l4proto *l4proto) 注册相关函数

 

 

处理一个连接的子连接协议

  利用nf_conntrack_helper.c文件中的nf_conntrack_helper_register(struct nf_conntrack_helper *me)以及 nf_ct_expect_related_report(struct nf_conntrack_expect *expect, u32 pid, int report)

注册相关函数

 

 扩展连接跟踪结构(nf_conn)

 

利用nf_ct_extend_register(struct nf_ct_ext_type *type) 进行扩展,并修改连接跟踪相应代码来利用这部分扩展功能;

 

 nf_conntrack模块加载时的初始化流程

 

 

nf_conntrack的初始化

就是初始化上面提到的那些数据结构,它在内核启动时调用nf_conntrack_standalone_init()函数进行初始化的。初始化完成后,构建出如下图所示的结构图,只是不包含下图中与连接有关的信息(nf_conn和nf_conntrack_expect结构)

 

 当创建子连接时,各个数据结构之间的关系

 

 

IPv4-NAT连接跟踪相关部分通过函数nf_nat_init()初始化

调用nf_ct_extend_register() 注册一个连接跟踪的扩展功能。

 

  调用register_pernet_subsys() –> nf_nat_net_init() 创建net->ipv4.nat_bysource的HASH表,大小等于net->ct.htable_size。

初始化nf_nat_protos[]数组,为TCP、UDP、ICMP协议指定专用处理结构,其它协议都指向默认处理结构

 

 

为nf_conntrack_untracked连接设置IPS_NAT_DONE_MASK标志。

将NAT模块的全局变量l3proto指向IPV4协议的nf_conntrack_l3proto结构。

设置全局指针nf_nat_seq_adjust_hook指向nf_nat_seq_adjust()函数。

设置全局指针nfnetlink_parse_nat_setup_hook指向nfnetlink_parse_nat_setup()函数。

设置全局指针nf_ct_nat_offset指向nf_nat_get_offset()函数。

IPv4-NAT功能的iptables部分通过函数nf_nat_standalone_init()初始化

调用nf_nat_rule_init() –> nf_nat_rule_net_init()在iptables中注册一个NAT表

调用 nf_nat_rule_init() 注册SNAT target和DNAT target(通过xt_register_target()函数)

 

 调用nf_register_hooks() 挂载NAT的HOOK函数,橙色部分为NAT挂载的HOOK函数

 

 IPv4-NAT的主要是通过nf_nat_ipv4_fn()钩子函数处理的

unsigned int
nf_nat_ipv4_fn(void *priv, struct sk_buff *skb, const struct nf_hook_state *state,
 unsigned int (*do_chain)(void *priv, struct sk_buff *skb, const struct nf_hook_state *state,struct nf_conn *ct))
{
    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(state->hook);

    /* 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))/*如果该conntrack是确认状态,并且没有nat扩展功能,就不需要处理NAT*/
        return NF_ACCEPT;

    nat = nf_ct_nat_ext_add(ct);
    if (nat == NULL)
        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,
                               state->hook))
                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;
            /*查找NAT表并进行报文的处理,会调用NAT target的处理函数对conntrack进行设置*/ 
            ret = do_chain(priv, skb, state, ct);//=== iptable_nat_do_chain(priv, skb, state, ct) --> ipt_snat_target  or  ipt_dnat_target
            if (ret != NF_ACCEPT)
                return ret;

            if (nf_nat_initialized(ct, HOOK2MANIP(state->hook)))
                break; 
         /*
         如果NAT表中没有配置匹配该报文的NAT规则,,就根据报文的ip地址设置一
个不进行NAT转换的NAT规则,这样做的目的是避免该连接上的后续报文都进行
NAT表的查询匹配操作。
*/ 
            ret = nf_nat_alloc_null_binding(ct, state->hook);//--->nf_nat_setup_info
            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(state->hook, ctinfo, nat,
                           state->out))
                goto oif_changed;
        }
        break;

    default:
        /* ESTABLISHED */
        NF_CT_ASSERT(ctinfo == IP_CT_ESTABLISHED ||
                 ctinfo == IP_CT_ESTABLISHED_REPLY);
        if (nf_nat_oif_changed(state->hook, ctinfo, nat, state->out))
            goto oif_changed;
    }

    return nf_nat_packet(ct, ctinfo, state->hook, skb);

oif_changed:
    nf_ct_kill_acct(ct, ctinfo, skb);
    return NF_DROP;
}

 

 nf_nat_setup_info()函数进一步描述

 

static unsigned int
__nf_nat_alloc_null_binding(struct nf_conn *ct, enum nf_nat_manip_type manip)
{
    /* Force range to this IP; let proto decide mapping for
     * per-proto parts (hence not IP_NAT_RANGE_PROTO_SPECIFIED).
     * Use reply in case it's already been mangled (eg local packet).
     */ /* 使用应答方向的ip地址,LOCAL_OUT会先经过mangle,可能改变了 */
    union nf_inet_addr ip =
        (manip == NF_NAT_MANIP_SRC ?
        ct->tuplehash[IP_CT_DIR_REPLY].tuple.dst.u3 :
        ct->tuplehash[IP_CT_DIR_REPLY].tuple.src.u3);
    struct nf_nat_range range = {
        .flags        = NF_NAT_RANGE_MAP_IPS,
        .min_addr    = ip,
        .max_addr    = ip,
    };/*该函数只是调用nf_nat_setup_info函数

给conntrack设置status的NAT标志位*/ 
    return nf_nat_setup_info(ct, &range, manip);
}
unsigned int
nf_nat_setup_info(struct nf_conn *ct,
          const struct nf_nat_range *range,
          enum nf_nat_manip_type maniptype)
{
    struct net *net = nf_ct_net(ct);
    struct nf_conntrack_tuple curr_tuple, new_tuple;
    struct nf_conn_nat *nat;

    /* nat helper or nfctnetlink also setup binding */
    nat = nf_ct_nat_ext_add(ct);
    if (nat == NULL)
        return NF_ACCEPT;
/* 获取是进行DNAT还是SNAT,其中PRE_ROUTING和LOCAL_OUT进行DNAT,LOCAL_IN和POST_ROUTING进行SNAT */
    NF_CT_ASSERT(maniptype == NF_NAT_MANIP_SRC ||
             maniptype == NF_NAT_MANIP_DST);
    BUG_ON(nf_nat_initialized(ct, maniptype));

    /* What we've got will look like inverse of reply. Normally
     * this is what is in the conntrack, except for prior
     * manipulations (future optimization: if num_manips == 0,
     * orig_tp = ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple)
     */
     /* 从应答tuple反向得到当前tuple */
    nf_ct_invert_tuplepr(&curr_tuple,
                 &ct->tuplehash[IP_CT_DIR_REPLY].tuple);
    
/* 根据当前tuple和range得到NAT转换之后的的tuple */
    get_unique_tuple(&new_tuple, &curr_tuple, range, ct, maniptype);


  /* NAT转换之后和之前的tuple不同 */
    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);  /* 通过新tuple得到reply_tuple */
        nf_conntrack_alter_reply(ct, &reply);
 /* 此时tuple类似如下 */
        /*
            //内网10.1通过100.1访问200.1,经过SNAT之后得到tuple
            tuple SNAT(10.1->200.1, 200.1->100.1) 
            
            //外网300.1通过100.1访问20.1,经过DNAT之后,得到tuple
            tuple DNAT(300.1->100.1, 20.1->300.1) 
        */
        /* Non-atomic: we own this at the moment. */
        if (maniptype == NF_NAT_MANIP_SRC)
            ct->status |= IPS_SRC_NAT;
        else
            ct->status |= IPS_DST_NAT;

        if (nfct_help(ct)) /* 扩展项的调整   比如  alg ftp sip 调整序列号 */
            nfct_seqadj_ext_add(ct);
    }

    if (maniptype == NF_NAT_MANIP_SRC) { /* SNAT */
        unsigned int srchash;

        srchash = hash_by_src(net,
                      &ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple);
        spin_lock_bh(&nf_nat_lock);
        /* nf_conntrack_alter_reply might re-allocate extension aera */
        nat = nfct_nat(ct);
        nat->ct = ct;/* 加入到nf_nat_bysource_table */
        hlist_add_head_rcu(&nat->bysource,
                   &nf_nat_bysource[srchash]);
        spin_unlock_bh(&nf_nat_lock);
    }

    /* It's done. */ /* NAT转换完成 */
    if (maniptype == NF_NAT_MANIP_DST)
        ct->status |= IPS_DST_NAT_DONE;
    else
        ct->status |= IPS_SRC_NAT_DONE;

    return NF_ACCEPT;
}
EXPORT_SYMBOL(nf_nat_setup_info);

 

 

unsigned int
nf_nat_setup_info(struct nf_conn *ct,
          const struct nf_nat_range *range,
          enum nf_nat_manip_type maniptype)
{
    struct net *net = nf_ct_net(ct);
    struct nf_conntrack_tuple curr_tuple, new_tuple;
    struct nf_conn_nat *nat;

    /* nat helper or nfctnetlink also setup binding */
    nat = nf_ct_nat_ext_add(ct);
    if (nat == NULL)
        return NF_ACCEPT;
/* 获取是进行DNAT还是SNAT,其中PRE_ROUTING和LOCAL_OUT进行DNAT,LOCAL_IN和POST_ROUTING进行SNAT */
    NF_CT_ASSERT(maniptype == NF_NAT_MANIP_SRC ||
             maniptype == NF_NAT_MANIP_DST);
    BUG_ON(nf_nat_initialized(ct, maniptype));

    /* What we've got will look like inverse of reply. Normally
     * this is what is in the conntrack, except for prior
     * manipulations (future optimization: if num_manips == 0,
     * orig_tp = ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple)
     */
     /* 从应答tuple反向得到当前tuple */
    nf_ct_invert_tuplepr(&curr_tuple,
                 &ct->tuplehash[IP_CT_DIR_REPLY].tuple);
    
/* 根据当前tuple和range得到NAT转换之后的的tuple */
    get_unique_tuple(&new_tuple, &curr_tuple, range, ct, maniptype);


  /* NAT转换之后和之前的tuple不同 */
    if (!nf_ct_tuple_equal(&new_tuple, &curr_tuple)) {
        struct nf_conntrack_tuple reply;

        /* Alter conntrack table so will recognize replies.  new_tuple为nat后的orig tuple*/
        nf_ct_invert_tuplepr(&reply, &new_tuple);  /* 通过新tuple得到reply_tuple */
        nf_conntrack_alter_reply(ct, &reply);
 /* 此时tuple类似如下 */
        /*
            //内网10.1通过100.1访问200.1,经过SNAT之后得到tuple
            tuple SNAT(10.1->200.1, 200.1->100.1) 
            
            //外网300.1通过100.1访问20.1,经过DNAT之后,得到tuple
            tuple DNAT(300.1->100.1, 20.1->300.1) 
        */
        /* Non-atomic: we own this at the moment. */
        if (maniptype == NF_NAT_MANIP_SRC)
            ct->status |= IPS_SRC_NAT;
        else
            ct->status |= IPS_DST_NAT;

        if (nfct_help(ct)) /* 扩展项的调整   比如  alg ftp sip 调整序列号 */
            nfct_seqadj_ext_add(ct);
    }

    if (maniptype == NF_NAT_MANIP_SRC) { /* SNAT */
        unsigned int srchash;

        srchash = hash_by_src(net,
                      &ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple);
        spin_lock_bh(&nf_nat_lock);
        /* nf_conntrack_alter_reply might re-allocate extension aera */
        nat = nfct_nat(ct);
        nat->ct = ct;/* 加入到nf_nat_bysource_table */
        hlist_add_head_rcu(&nat->bysource,
                   &nf_nat_bysource[srchash]);
        spin_unlock_bh(&nf_nat_lock);
    }

    /* It's done. */ /* NAT转换完成 */
    if (maniptype == NF_NAT_MANIP_DST)
        ct->status |= IPS_DST_NAT_DONE;
    else
        ct->status |= IPS_SRC_NAT_DONE;

    return NF_ACCEPT;
}
EXPORT_SYMBOL(nf_nat_setup_info);

 

 

  每个ct在第一个包就会做好snat与dnat, nat的信息全放在reply tuple中,orig tuple不会被改变。一旦第一个包建立好nat信息后,后续再也不会修改tuple内容了。

orig tuple中的地址信息与reply tuple中的地址信息就是原始数据包的信息。例如对A->B数据包同时做snat与dnat,PREROUTING处B被dnat到D,POSTROUTING处A被snat到C。则ct的内容是:  A->B | D->C,  A->B说明了orig方向上数据包刚到达墙时的地址内容,D->C说明reply方向上数据包刚到达墙时的地址内容。

  bysource链中链接了所有CT(做过NAT和未做过NAT),通过ct->nat->bysource,HASH值的计算使用的是CT的orig tuple。其作用是,当为一个新连接做SNAT,需要得到地址映射时,首先对该链进行查找,查找此源IP、协议和端口号是否已经做过了映射。如果做过的话,就需要在SNAT转换时,映射为相同的源IP和端口号。为什么要这么做呢?因为对于UDP来说,有些协议可能会用相同端口和同一主机不同的端口(或不同的主机)进行通信。此时,由于目的地不同,原来已有的映射不可使用,需要一个新的连接。但为了保证通信的的正确性,此时,就要映射为相同的源IP和端口号。其实就是为NAT的打洞服务的。所以bysource就是以源IP、协议和端口号为hash值的一个表,这样在做snat时保证相同的ip+port影射到相同的ip+port。

  第一个包之后,ct的两个方向的tuple内容就固定了,所有的nat操作都必须在第一个包就完成。所以会有daddr = &ct->tuplehash[!dir].tuple.dst.u3;这样的操作。

  对于一个ct,nf_nat_setup_info函数最多只能进入2次,第一次DNAT,第二次SNAT。在nf_nat_follow_master函数中,第一次SNAT,第二次DNAT。

SNAT target的处理函数

static unsigned int ipt_snat_target(struct sk_buff *skb, const struct xt_target_param *par)

{

    struct nf_conn *ct;

    enum ip_conntrack_info ctinfo;
    /*取得用户配置的TARGET参数*/

    const struct nf_nat_multi_range_compat *mr = par->targinfo;
    /*SNAT target必须配置在POSTROUTING HOOK点上*/

    NF_CT_ASSERT(par->hooknum == NF_INET_POST_ROUTING);

 
    /*取得报文携带的conntrack信息及conntrack的状态*/

    ct = nf_ct_get(skb, &ctinfo);

 

    /* Connection must be valid and new. */
    /*conntrack 的状态必须是以下几种双方未建立连接的状态,否则就报错。

这是因为连接已建立后,报文就直接根据conntrack进行处理NAT即可,

不会再查nat使用SNAT target进行NAT的处理了*/

    NF_CT_ASSERT(ct && (ctinfo == IP_CT_NEW || ctinfo == IP_CT_RELATED ||

    ctinfo == IP_CT_RELATED + IP_CT_IS_REPLY));
    /*在进行SNAT前,报文必须先经过路由找到出接口,否则报错*/

    NF_CT_ASSERT(par->out != NULL);
    /*把用户配置的映射范围传给 nf_nat_setup_info进行进一步处理*/

    return nf_nat_setup_info(ct, &mr->range[0], IP_NAT_MANIP_SRC);

}

由于将 ct 挂载在 list上是在nf_conntrack_confirm里面做的,所以 snat后,此时还没有confirm,替换tuple的sip直接替换就行:

    /* Since the lookup is lockless, hash insertion must be done after
     * starting the timer and setting the CONFIRMED bit. The RCU barriers
     * guarantee that no other CPU can find the conntrack before the above
     * stores are visible.  将 orig_tuple reply_tuple 添加到 nf_conntrack_hash 
     */
    __nf_conntrack_hash_insert(ct, hash, reply_hash);

所以:在NAT网关上配置

iptables -t nat -A POSTROUTING  -p udp  -j SNAT --to-source 9.9.9.9

Client 发送 udp报文,报文格式如下

Sip:192.168.3.227 Dip:192.168.5.2 Sport:103 Dprot:105

 进入NAT网关后,在PREROUTING HOOK点先由IP conntrack进行conntrack的建立

 

 

建立conntrack后,这时struct nf_conn还不是确认状态。报文经过路由查找后,找到出接口后,走到POSTROUTING HOOK点。

被SNAT注册的hook函数处理,在nat表中找到配置的规则,把conntrack的reply反向信息修改如下

 

 

把nf_conn的status的IPS_SRC_NAT_BIT位置1,然后把报文src ip 修改为9.9.9.9。

再由conntrack的 ipv4_confirm来确认conntrack并把该conntrack加入到conntrack hash表中。

然后根据SNAT前查到的路由信息,把报文发送出去。

 

 记住:NF_IP_PRI_CONNTRACK_CONFIRM 是最后一个哦!! 在POST_ROUTEING等上其值最大 优先级最小。

    {
        .hook        = ipv4_confirm,
        .pf        = NFPROTO_IPV4,
        .hooknum    = NF_INET_POST_ROUTING,
        .priority    = NF_IP_PRI_CONNTRACK_CONFIRM,
    },

 

 

回应报文,报文格式如下

Sip:192.168.5.2 Sport:105 Dip:9.9.9.9 Dport:103

 到达NAT网关后,在PREROUTING HOOK点上,先由conntrack hook处理函数来更新conntrack的连接状态,并把查找到的conntrack 赋值给skb->nfct。

  然后进入NAT的hook函数nf_nat_in,发现报文是reply方向的,并且skb->nfct->status是置位了IPS_SRC_NAT_BIT,就进行DNAT来进行报文的处理。
根据conntrack A的信息来把报文的dip 修改为 192.168.3.227。然后查找路由后发送出去。

  以后每次两个反向的报文进入NAT网关,都会查到建立的conntrack,根据报文的方向以及conntrack中status的信息,
来决定NAT的处理方式,使用该报文反方向的信息来修改报文的IP地址,完成NAT功能

unsigned int nf_nat_packet(struct nf_conn *ct,
               enum ip_conntrack_info ctinfo,
               unsigned int hooknum,
               struct sk_buff *skb)resolve_normal_ct
{
    const struct nf_nat_l3proto *l3proto;
    const struct nf_nat_l4proto *l4proto;
    enum ip_conntrack_dir dir = CTINFO2DIR(ctinfo);
    unsigned long statusbit;
    enum nf_nat_manip_type mtype = HOOK2MANIP(hooknum); /* 获取进行SNAT还是DNAT */

    if (mtype == NF_NAT_MANIP_SRC)
        statusbit = IPS_SRC_NAT;
    else
        statusbit = IPS_DST_NAT;

    /* Invert if this is reply dir. */
    if (dir == IP_CT_DIR_REPLY)  /* 应答方向需要取反 也就是应答报文 192.168.5.2-->.9.9.9.9--  取反做dnat 以前做的snat, 这个tuple 其方向是reply */
        statusbit ^= IPS_NAT_MASK;

    /* Non-atomic: these bits don't change. */
    if (ct->status & statusbit) { /* 需要做NAT */
        struct nf_conntrack_tuple target;
 -------------------------------------------
         /* 将ip地址和端口的NAT转换结果写入skb */
        if (!l3proto->manip_pkt(skb, 0, l4proto, &target, mtype))
            return NF_DROP;nf_nat_ipv4_manip_pkt; 
    }
    return NF_ACCEPT;
}

 

有子连接的NAT实现

有两个关键点:1.主链接能正确的构建出NAT后的expect来识别子连接。2.能够修改主链接数据通道的信息为NAT后的信息。这两点都在动态协议的help中完成,下面我们来看一下它的流程图:

 

 

无子连接的NAT

   一个ct用于跟踪一个连接的双方向数据,ct->orig_tuple用于跟踪初始方向数据,ct->reply_tuple用于跟踪应答方向数据。当根据初始方向数据构建ct->orig_tuple时,同时要构建出ct->reply_tuple,用于识别同一连接上应答方向数据。

 如果初始方向的数据在通过防火墙后被做了NAT转换,为识别出NAT数据的应答数据包,则对ct->reply_tuple也要做NAT转换。同时ct上做好相应NAT标记。

因此,上面的信息在初始方向第一个数据包通过后,就要求全部建立好,并且不再改变。

  一个连接上不同方向的数据,都有相对应的tuple(orig_tuple和reply_tuple),所以该连接后续数据都将被识别出来。如果ct上有NAT标记,则根据要去往方向(即另一个方向)的tuple对数据做NAT转换。所以会有ct->tuplehash[!dir].tuple这样的操作。

有子连接的NAT

  子连接是由主连接构建的expect项识别出来的。

   help用于构建expect项,它期待哪个方向的连接,则用那个方向的tuple和数据包中数据通道信息构建expect项。例如期待和当前数据包相反方向的连接,则用相反方向的tuple中的信息(ct->tuplehash[!dir].tuple)。调用help时,NAT转换都已完成(tuple中都包含有正确的识别各自方向的信息),所以这时所使用的信息都是正确和所期望的信息。

 如果子连接还可能有子连接,则构建expect项时,初始化一个helper结构,并赋值给expect->helper指针。

 如果该连接已被做了NAT转换,则对数据包中数据通道信息也要做NAT转换

 

posted @ 2023-03-03 17:23  codestacklinuxer  阅读(170)  评论(0编辑  收藏  举报