重读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转换
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
2022-03-03 路由fib 数据结构
2022-03-03 socketfd inode ip addr process关联