CXD Linux Engineer

Linux协议栈--NAT源码分析

2017-09-29

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内核源码分析


Comments

Content