本文对netfilter中NAT部分的源码进行分析,读者需要先对NAT的基本概念有一个大致了解。
NAT模块的初始化过程主要是初始化一些全局变量以及注册NAT相关的hook函数。在下面nf_nat_init()函数和nf_nat_standalone_init()函数的流程图中用红色标记了要初始化的全局数据结构。
nf_nat_init()函数:nf_nat_standalone_init()函数:
NAT表是一个xt_table,定义如下:
static struct xt_table nat_table = { .name = "nat", .valid_hooks = NAT_VALID_HOOKS, .me = THIS_MODULE, .af = AF_INET, };
iptables的表如filter, nat,mangle表都是通过ipt_register_table()注册的,在netfilter中被使用。我们需要知道iptables的表中的每条规则都包括三部分:
entry:规则的入口,同时做一些匹配数据包的工作。
match:匹配数据包的条件大多放在这里。
target:对于符合条件的数据包要执行的动作放在这里。
NAT表中的每条规则就包括上面三个部分。
注册NAT表时传入的第三个参数nat_initial_table定义如下:
static struct { struct ipt_replace repl; struct ipt_standard entries[3]; struct ipt_error term; } nat_initial_table __net_initdata = { .repl = { .name = "nat", .valid_hooks = NAT_VALID_HOOKS, .num_entries = 4, .size = sizeof(struct ipt_standard) * 3 + sizeof(struct ipt_error), .hook_entry = { [NF_INET_PRE_ROUTING] = 0, [NF_INET_POST_ROUTING] = sizeof(struct ipt_standard), [NF_INET_LOCAL_OUT] = sizeof(struct ipt_standard) * 2 }, .underflow = { [NF_INET_PRE_ROUTING] = 0, [NF_INET_POST_ROUTING] = sizeof(struct ipt_standard), [NF_INET_LOCAL_OUT] = sizeof(struct ipt_standard) * 2 }, }, .entries = { IPT_STANDARD_INIT(NF_ACCEPT), /* PRE_ROUTING */ IPT_STANDARD_INIT(NF_ACCEPT), /* POST_ROUTING */ IPT_STANDARD_INIT(NF_ACCEPT), /* LOCAL_OUT */ }, .term = IPT_ERROR_INIT, /* ERROR */ };
xt_register_target()函数为iptables 规则注册target,这里注册了snat和dnat两个target,他们的定义如下:
static struct xt_target ipt_snat_reg __read_mostly = { .name = "SNAT", .target = ipt_snat_target, .targetsize = sizeof(struct nf_nat_multi_range_compat), .table = "nat", .hooks = 1 << NF_INET_POST_ROUTING, .checkentry = ipt_snat_checkentry, .family = AF_INET, }; static struct xt_target ipt_dnat_reg __read_mostly = { .name = "DNAT", .target = ipt_dnat_target, .targetsize = sizeof(struct nf_nat_multi_range_compat), .table = "nat", .hooks = (1 << NF_INET_PRE_ROUTING) | (1 << NF_INET_LOCAL_OUT), .checkentry = ipt_dnat_checkentry, .family = AF_INET, };
介绍完NAT模块的初始化,接下来看看NAT处理数据包的过程。
NAT模块通过挂在netfilter上的hook函数起作用,其任务就是将设置好的NAT表中的iptables规则作用于conntrack连接,使其做NAT转换,并生成新的conntrack连接。而后续相同的数据包直接根据新的conntrack连接进行NAT转换而不需要再匹配NAT表。NAT表与netfilter其他表的区别就是一个连接上的数据包只需要查找一次NAT表。
NAT有四个hook点,这四个hook点的函数都是调用nf_nat_fn(),其中PRE_ROUTING和LOCAL_OUT做DNAT,POST_ROUTING和LOCAL_IN做SNAT。但LOCAL IN和LOCAL OUT上的hook点一般不做工作,因此我们只关注PRE ROUTING和POST ROUTING的hook点。
下文中有的地方将conntrack简写为ct。
nf_nat_fn()函数:
static unsigned int nf_nat_fn(unsigned int hooknum, struct sk_buff *skb, const struct net_device *in, const struct net_device *out, int (*okfn)(struct sk_buff *)) { struct nf_conn *ct; enum ip_conntrack_info ctinfo; struct nf_conn_nat *nat; /* maniptype == SRC for postrouting. */ /* 判断应该做SNAT还是DNAT */ enum nf_nat_manip_type maniptype = HOOK2MANIP(hooknum); /* 分片包就报warning,因为在这之前已经过了defrag的hook了。 */ NF_CT_ASSERT(!(ip_hdr(skb)->frag_off & htons(IP_MF | IP_OFFSET))); /*获得skb的nf_conn结构,因为conntrack的hook在NAT之前,所以skb中应该有nf_conn了,并从skb->nfctinfo中获得当前连接跟踪的状态。 */ ct = nf_ct_get(skb, &ctinfo); if (!ct) /* 找不到tuple可能因为数据包不合法,就如序列号过大 */ return NF_ACCEPT; /* Don't try to NAT if this packet is not conntracked */ if (ct == &nf_conntrack_untracked) return NF_ACCEPT; /* 在ct->ext中查找存不存在关于NAT的extension,没找到则新建。在本函数中用不到,NAT的extension在后面介绍。 */ nat = nfct_nat(ct); if (!nat) { /* NAT module was loaded late. */ if (nf_ct_is_confirmed(ct)) { printk("CT not confirmed ct=%p\n\n",ct); return NF_ACCEPT; } /* GFP即get free page,这些宏指定了内存分配时的优先级。 这里只是分配空间 */ nat = nf_ct_ext_add(ct, NF_CT_EXT_NAT, GFP_ATOMIC); if (nat == NULL) { pr_debug("failed to add NAT extension\n"); return NF_ACCEPT; } } /* 判断连接状态 */ switch (ctinfo) { case IP_CT_RELATED: case IP_CT_RELATED+IP_CT_IS_REPLY: if (ip_hdr(skb)->protocol == IPPROTO_ICMP) { if (!nf_nat_icmp_reply_translation(ct, ctinfo, hooknum, skb)) return NF_DROP; else return NF_ACCEPT; } case IP_CT_NEW: /* 新来的,还没创建conntrack条目,需要查找NAT表 */ /* Seen it before? This can happen for loopback, retrans, or local packets.. */ /* 另外,如果只有单方向数据,这个if也会使其不需要查找nat表。 */ if (!nf_nat_initialized(ct, maniptype)) { unsigned int ret; if (hooknum == NF_INET_LOCAL_IN) /* LOCAL_IN hook doesn't have a chain! */ ret = alloc_null_binding(ct, hooknum); else /* 在nat(iptable)表中匹配该hook中的iptables规则并执行target。结果是给skb指向的ct做了NAT,且更新ct->status为IPS_DST_NAT_DONE_BIT或IPS_SRC_NAT_DONE_BIT。 */ ret = nf_nat_rule_find(skb, hooknum, in, out, ct); if (ret != NF_ACCEPT) { return ret; } } else pr_debug("Already setup manip %s for ct %p\n", maniptype == IP_NAT_MANIP_SRC ? "SRC" : "DST", ct); break; default: /* ESTABLISHED或REPLY的连接,就直接根据ct修改skb了。 */ NF_CT_ASSERT(ctinfo == IP_CT_ESTABLISHED || ctinfo == (IP_CT_ESTABLISHED+IP_CT_IS_REPLY)); } /* 前面已经修改了连接跟踪,这里正式修改了数据包里的地址 */ return nf_nat_packet(ct, ctinfo, hooknum, skb); }
其中nf_nat_rule_find()函数通过调用ipt_do_table(skb, hooknum, in, out,net->ipv4.nat_table)来修改ct做NAT,ipt_do_table()函数的工作就是匹配iptables规则matches并执行target。以下面的数据包为例:
src ip:192.168.1.102,dst ip:192.168.2.100,wan ip:192.168.2.1
数据包从路由器LAN->WAN方向传输,即从局域网到公网,所以需要做SNAT,即在POST_ROUTING处为数据包做NAT。
在nf_nat_rule_find()函数前后ct的变化如下(TCP和UDP相同):
PRE_ROUTING:
|
源ip/port |
目的ip/port |
skb->nfctinfo |
ipt_do_table()之前 |
192.168.1.102:3386 |
192.168.2.100:10115 |
IP_CT_NEW |
192.168.2.100:10115 |
192.168.1.102:3386 |
IP_CT_NEW |
|
ipt_do_table()之后 |
192.168.1.102:3386 |
192.168.2.100:10115 |
IP_CT_NEW |
192.168.2.100:10115 |
192.168.1.102:3386 |
IP_CT_NEW |
POST_ROUTING:
|
源ip/port |
目的ip/port |
skb->nfctinfo |
ipt_do_table()之前 |
192.168.1.102:3386 |
192.168.2.100:10115 |
IP_CT_NEW |
192.168.2.100:10115 |
192.168.1.102:3386 |
IP_CT_NEW |
|
ipt_do_table()之后 |
192.168.1.102:3386 |
192.168.2.100:10115 |
IP_CT_NEW |
192.168.2.100:10115 |
192.168.2.1:3386 |
IP_CT_NEW |
skb的状态skb->nfctinfo在nf_conntrack_in()根据ct的状态ct->status被更新,即下一个包进来的时候才会更新。第一个数据包根据iptables设置的NAT规则做NAT后,conntrack条目被更新。
之后的数据包再进入netfilter的时候,由于conntrack优先级在NAT之前,所以skb->nfctinfo会在进入nf_nat_fn()之前更新,因此就会使用新的conntrack做NAT而无需再查找NAT表中的规则。之后的数据包的状态可能为IP_CT_IS_REPLY或IP_CT_ESTABLISHED,如果有expect连接,则可能为IP_CT_RELATED或IP_CT_RELATED + IP_CT_IS_REPLY。
如果只有单方向的数据包,那skb的状态在nf_conntrack_in()就不会被修改,所以会一直是IP_CT_NEW,因此后续数据包会一直查找NAT表。不过一般不会一直都是单方向的,即使跑UDP不分片包的数据,一般也会看到有REPLY方向的数据包,有了REPLY方向的数据包,在conntrack钩子就会更新conntrack和skb的状态。
ICMP做NAT要做一下特殊介绍,在POST_ROUTING处的连接如下:
|
源ip/port |
目的ip/port |
skb->nfctinfo |
ipt_do_table()之前 |
192.168.1.102: 768 |
192.168.2.100:2048 |
IP_CT_NEW |
192.168.2.100: 768 |
192.168.1.102:0 |
IP_CT_NEW |
|
ipt_do_table()之后 |
192.168.1.102: 768 |
192.168.2.100: 2048 |
IP_CT_NEW |
192.168.2.100: 768 |
192.168.2.1:0 |
IP_CT_NEW |
由于ICMP没有端口号,所以会以ICMP报头中的Identifier作为源port,以ICMP报头中的Type + Code字段作为目的port。如果内网有多台主机,Identifier会发生变化,即连接上的源port会发生变化,以此区分不同的连接。而由于目的port是根据Type + Code确定的,这个值对于特定类型的ICMP报文的值是一样的。
UDP分片包做NAT:
由于defrag的hook在conntrack之前,所以不用担心分片包的问题。