本文对netfilter中NAT部分的源码进行分析,读者需要先对NAT的基本概念有一个大致了解。
1. 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处理数据包的过程。
2. 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,所以我们的SNAT规则是加在POST_ROUTING链上的。但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之前,所以不用担心分片包的问题。