NAT的初始化
前面我们在Iptables规则中提到过,NAT是Iptables中的一种表。所以NAT的初始化会在Iptables框架中注册一个表项。然后NAT也需要在Netfilter框架中注册钩子函数才能捕获数据包并对其进行修。这些都是在/net/ipv4/netfilter/iptable_nat.c
文件中实现。Netfilter钩子注册的代码如下,可以看到除了NF_INET_FORWARD
外其他挂载点都注册了钩子函数:
static struct nf_hook_ops nf_nat_ipv4_ops[] __read_mostly = {
/* Before packet filtering, change destination */
{
.hook = iptable_nat_ipv4_in,
.pf = NFPROTO_IPV4,
.hooknum = NF_INET_PRE_ROUTING,
.priority = NF_IP_PRI_NAT_DST,
},
/* After packet filtering, change source */
{
.hook = iptable_nat_ipv4_out,
.pf = NFPROTO_IPV4,
.hooknum = NF_INET_POST_ROUTING,
.priority = NF_IP_PRI_NAT_SRC,
},
/* Before packet filtering, change destination */
{
.hook = iptable_nat_ipv4_local_fn,
.pf = NFPROTO_IPV4,
.hooknum = NF_INET_LOCAL_OUT,
.priority = NF_IP_PRI_NAT_DST,
},
/* After packet filtering, change source */
{
.hook = iptable_nat_ipv4_fn,
.pf = NFPROTO_IPV4,
.hooknum = NF_INET_LOCAL_IN,
.priority = NF_IP_PRI_NAT_SRC,
},
}
我们知道NAT是在连接跟踪的基础上实现的,所以连接跟踪肯定是在NAT之前建立的。从这里注册的钩子函数的优先级可以看到,NAT的优先级是NF_IP_PRI_NAT_SRC = -100
而连接跟踪的优先级是NF_IP_PRI_CONNTRACK = -200
,在Netfilter框架中优先级数值越小,优先级越高,越先被调用。所以可以看到NAT是在连接跟踪建立之后进行的。
NAT的实现
数据包进入系统后经过的第一个挂载点是NF_INET_PRE_ROUTING
,连接跟踪也在这里注册过钩子函数,所以连接跟踪首先对这个数据包建立一个连接跟踪项。然后再进入NAT模块中进行处理。NAT中NF_INET_PRE_ROUTING
挂载点上注册的函数是iptable_nat_ipv4_in
,所以NAT的数据包入口就是iptable_nat_ipv4_in
这个函数。这个函数非常简单它直接调用nf_nat_ipv4_in
函数,并将iptable_nat_do_chain
函数传递进去了。进入nf_nat_ipv4_in
函数可以看到它只是将数据包的目的IP地址取出来了,然后调用了nf_nat_ipv4_fn
函数,可以看到NAT的主要逻辑就是在nf_nat_ipv4_fn
函数中实现的。
nf_nat_ipv4_fn
函数会首先获取这个数据包的连接跟踪条目,如果没有找到就直接放行数据包返回。然后根据连接跟踪的状态来做不同的处理,一个新建立的连接跟踪他的状态会被设置为IP_CT_NEW
,所以这里我们着重分析状态为IP_CT_NEW
的处理逻辑。当连接状态为IP_CT_NEW
时,首先调用do_chain
函数来查找Iptables中的NAT转换策略,这个函数就是上面提到iptable_nat_ipv4_in
函数传递进去的iptable_nat_do_chain
函数。找到之后然后调用nf_nat_alloc_null_binding
函数来完成实际的NAT转换,并且会同时修改连接跟踪记录。
示例一个外网出口路由器的IP地址为:113.87.160.1
内部一台PC地址为192.168.0.1向百度发送请求的连接跟踪记录:
一条原始的连接跟踪记录:
原始方向:tuplehash[IP_CT_DIR_ORIGINAL] = {192.168.0.1:12345,111.13.101.208:80,TCP}
回复方向:tuplehash[IP_CT_DIR_REPLY] = {111.13.101.208:80,192.168.0.1:12345,TCP}
经过SNAT修改之后的连接跟踪记录:
原始方向:tuplehash[IP_CT_DIR_ORIGINAL] = {192.168.0.1:12345,111.13.101.208:80,TCP}
回复方向:tuplehash[IP_CT_DIR_REPLY] = {111.13.101.208:80,113.87.160.1:12345,TCP}
数据包的源地址改为 113.87.160.1 发送出去,修改连接跟踪记录回复方向中的目的地址为113.87.160.1
这样当百度回复数据包时就可以直接找到对应的连接跟踪记录,然后根据原始方向的数据找到真实主机。
注意:有时需要同时修改端口号来确保连接跟踪条目的唯一性
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);
/* 获取连接跟踪条目 */
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;
nat = nfct_nat(ct);
/* 根据连接跟踪的状态做相应的处理 */
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;
/* 如果是一条新连接,查找Iptables策略 */
ret = do_chain(priv, skb, state, ct);
if (ret != NF_ACCEPT)
return ret;
if (nf_nat_initialized(ct, HOOK2MANIP(state->hook)))
break;
/* 做NAT的实际转换,并修改连接跟踪记录 */
ret = nf_nat_alloc_null_binding(ct, state->hook);
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;
}
/* 如果不是一条新连接,则在这里直接进行NAT转换 */
return nf_nat_packet(ct, ctinfo, state->hook, skb);
oif_changed:
nf_ct_kill_acct(ct, ctinfo, skb);
return NF_DROP;
}
NAT的实际转换过程
NAT的实际转换过程会涉及到不同协议,需要不同的转换方式,所以都是通过函数指针来调用已经注册过的具体转换函数。对于IPv4协议来说他在/net/ipv4/netfilternf_nat_l3proto_ipv4.c
文件中的nf_nat_ipv4_manip_pkt
函数,他是通过struct nf_nat_l3proto nf_nat_l3proto_ipv4
结构体注册到内核的。首先它会调用第四层协议,如UDP或者TCP协议来更改端口号。对于UDP来说最终调用到/net/netfilternf_nat_proto_udp.c
文件中的udp_manip_pkt
函数,通过struct nf_nat_l4proto nf_nat_l4proto_udp
结构体注册到内核的。然后更改IP地址并重新计算校验值。
static bool nf_nat_ipv4_manip_pkt(struct sk_buff *skb,
unsigned int iphdroff,
const struct nf_nat_l4proto *l4proto,
const struct nf_conntrack_tuple *target,
enum nf_nat_manip_type maniptype)
{
struct iphdr *iph;
unsigned int hdroff;
if (!skb_make_writable(skb, iphdroff + sizeof(*iph)))
return false;
iph = (void *)skb->data + iphdroff;
hdroff = iphdroff + iph->ihl * 4;
/* 调用第四层协议,更改端口号 */
if (!l4proto->manip_pkt(skb, &nf_nat_l3proto_ipv4, iphdroff, hdroff,
target, maniptype))
return false;
iph = (void *)skb->data + iphdroff;
/* 更改IP地址并重新计算校验值 */
if (maniptype == NF_NAT_MANIP_SRC) {
csum_replace4(&iph->check, iph->saddr, target->src.u3.ip);
iph->saddr = target->src.u3.ip;
} else {
csum_replace4(&iph->check, iph->daddr, target->dst.u3.ip);
iph->daddr = target->dst.u3.ip;
}
return true;
}
参考
《精通Linux内核网络》
基于Linux-4.12.1内核源码分析