linux内核协议栈 netfilter 之 ip 层 netfilter 的 NAT 模块 hook 及 target 代码剖析

本文深入探讨Linux内核协议栈中Netfilter的NAT模块,详细分析了在IP层的NAT转换过程,包括PRE_ROUTING、POST_ROUTING、OUTPUT和LOCAL_IN四个钩子函数的执行逻辑,以及SNAT和DNAT功能的实现。文章还介绍了Nat表配置实践,讲解了如何通过iptables进行NAT转换配置,同时涉及了本地loopback重定向的处理逻辑和端口冲突问题解决。
摘要由CSDN通过智能技术生成

 

目录

1 钩子函数

1.1 PRE_ROUTING 钩子 nf_nat_ipv4_in()

1.2 POST_ROUTING 钩子 nf_nat_ipv4_out()

1.3 OUTPUT 钩子 nf_nat_ipv4_local_fn()

1.4 LOCAL_IN 钩子 nf_nat_ipv4_fn()

1.5 公共钩子调用 nf_nat_ipv4_fn()

1.5.1 NAT转换判断 nf_nat_initialized()

1.5.2 NAT 转换操作 nf_nat_rule_find()

1.5.3 数据包进行 NAT 转换 nf_nat_packet()

1.6 连接跟踪的地址转换 nf_nat_setup_info()【核心】

2 target 函数

2.1 SNAT 功能

2.2 DNAT 功能

3 实例分析

3.1 SNAT

3.1.1 环境说明

3.1.2 数据SNAT转换分析

3.2 DNAT

3.2.1 环境说明

3.2.2 数据的DNAT分析

4 nat表配置演练(iptables )

4.1 配置转发设备

4.2 本地loopback重定向

4.2.1 loopback 接口(火星报文)

4.2.2 route_localnet 路由逻辑 ip_route_input_slow()

4.3 本地地址重定向


1 钩子函数

NAT模块主要是在NF_IP_PREROUTING、NF_IP_POSTROUTING、NF_IP_LOCAL_OUT、NF_IP_LOCAL_IN四个节点上进行NAT操作,在上一节中我们知道nat表中只有PREROUTING、POSTROUTING、LOCAL_OUT三条链,而没有NF_IP_LOCAL_IN链,所以不能创建在LOCAL_IN hook点的SNAT操作。

而NAT模块在注册hook函数时又在LOCAL_IN点注册了hook函数,且hook函数也调用了NAT转换的通用处理函数,难道也要对LOCAL_IN的数据包进行NAT转换吗?

其实,在LOCAL_IN注册hook函数主要不是为了进行NAT转换,因为在系统为一个源ip为A的转发数据包进行了SNAT后,可能会对源端口获取一个随机的值,这时如果源ip为A的数据包要发送给网关时,可能源端口就是刚才NAT转换的那个源端口,此时为了保证连接跟踪项的原始方向的tuple变量的唯一性,就需要在LOCAL_IN的hook点通过调用NAT转换的通用处理函数,改变源端口值,重新获取一个新的唯一的且未被使用的tuple变量。这应该就是LOCAL_IN也需要hook回调函数的原因吧。

总结一下就是 LOCAL_IN 钩子函数,主要是用来修改端口号,使得连接跟踪的 tuple 唯一。

1.1 PRE_ROUTING 钩子 nf_nat_ipv4_in()

这个函数是NAT模块在PRE_ROUTING hook点上注册的回调函数,该函数主要是实现DNAT功能,该函数的定义如下,主要实现如下两个功能:

  1. 调用函数 nf_nat_ipv4_fn 实现DNAT转换
  2. 当转换后数据包的目的 ip 地址改变后,需要调用 skb_dst_drop() ,将 skb 对 dst_entry 的引用减一,然后将 skb->_skb_refdst 置为NULL
static unsigned int
nf_nat_ipv4_in(unsigned int hooknum,
	       struct sk_buff *skb,
	       const struct net_device *in,
	       const struct net_device *out,
	       int (*okfn)(struct sk_buff *))
{
	unsigned int ret;
	__be32 daddr = ip_hdr(skb)->daddr;

	ret = nf_nat_ipv4_fn(hooknum, skb, in, out, okfn);
	if (ret != NF_DROP && ret != NF_STOLEN &&
	    daddr != ip_hdr(skb)->daddr)
		skb_dst_drop(skb);

	return ret;
}

该函数主要是通过调用 nf_nat_ipv4_fn ,该函数是一个通用 NAT 转换函数,待会着重分析这个函数

1.2 POST_ROUTING 钩子 nf_nat_ipv4_out()

这个函数是 NAT 模块在 POST_ROUTING hook点的hook回调函数,该函数实现如下功能:

调用函数 nf_nat_ipv4_fn 实现SNAT转换

static unsigned int
nf_nat_ipv4_out(unsigned int hooknum,
		struct sk_buff *skb,
		const struct net_device *in,
		const struct net_device *out,
		int (*okfn)(struct sk_buff *))
{
#ifdef CONFIG_XFRM
	const struct nf_conn *ct;
	enum ip_conntrack_info ctinfo;
	int err;
#endif
	unsigned int ret;

	/* root is playing with raw sockets. */
	if (skb->len < sizeof(struct iphdr) ||
	    ip_hdrlen(skb) < sizeof(struct iphdr))
		return NF_ACCEPT;

	ret = nf_nat_ipv4_fn(hooknum, skb, in, out, okfn);
#ifdef CONFIG_XFRM
	if (ret != NF_DROP && ret != NF_STOLEN &&
	    !(IPCB(skb)->flags & IPSKB_XFRM_TRANSFORMED) &&
	    (ct = nf_ct_get(skb, &ctinfo)) != NULL) {
		enum ip_conntrack_dir dir = CTINFO2DIR(ctinfo);

		if ((ct->tuplehash[dir].tuple.src.u3.ip !=
		     ct->tuplehash[!dir].tuple.dst.u3.ip) ||
		    (ct->tuplehash[dir].tuple.dst.protonum != IPPROTO_ICMP &&
		     ct->tuplehash[dir].tuple.src.u.all !=
		     ct->tuplehash[!dir].tuple.dst.u.all)) {
			err = nf_xfrm_me_harder(skb, AF_INET);
			if (err < 0)
				ret = NF_DROP_ERR(err);
		}
	}
#endif
	return ret;
}

1.3 OUTPUT 钩子 nf_nat_ipv4_local_fn()

这个函数是 NAT 模块在 OUTPUT hook点的hook回调函数,该函数实现如下功能:

  1. 功能:实现DNAT转换功能
  2. 调用函数 nf_nat_ipv4_fn实现 DNAT 转换,调用 ip_route_me_harder,重新进行路由操作(与PRE_ROUTING不同的是,对于 OUTPUT的hook回调函数,当目的地址改变后,需要在该函数里调用 ip_route_me_harder重新查找路由,而在PRE_ROUTING链中,则是将skb->dst置为空, 然后在数据包往下执行时会自行重新查找路由。OUTPUT链接收的数据均是已经路由 的数据包,且后续调用函数中不会再有查找路由的操作,所以要nf_nat_local_fn 里实现路由查找)。
static unsigned int
nf_nat_ipv4_local_fn(unsigned int hooknum,
		     struct sk_buff *skb,
		     const struct net_device *in,
		     const struct net_device *out,
		     int (*okfn)(struct sk_buff *))
{
	const struct nf_conn *ct;
	enum ip_conntrack_info ctinfo;
	unsigned int ret;
	int err;

	/* root is playing with raw sockets. */
	if (skb->len < sizeof(struct iphdr) ||
	    ip_hdrlen(skb) < sizeof(struct iphdr))
		return NF_ACCEPT;

	ret = nf_nat_ipv4_fn(hooknum, skb, in, out, okfn);
	if (ret != NF_DROP && ret != NF_STOLEN &&
	    (ct = nf_ct_get(skb, &ctinfo)) != NULL) {
		enum ip_conntrack_dir dir = CTINFO2DIR(ctinfo);

		if (ct->tuplehash[dir].tuple.dst.u3.ip !=
		    ct->tuplehash[!dir].tuple.src.u3.ip) {
			err = ip_route_me_harder(skb, RTN_UNSPEC);
			if (err < 0)
				ret = NF_DROP_ERR(err);
		}
#ifdef CONFIG_XFRM
		else if (!(IPCB(skb)->flags & IPSKB_XFRM_TRANSFORMED) &&
			 ct->tuplehash[dir].tuple.dst.protonum != IPPROTO_ICMP &&
			 ct->tuplehash[dir].tuple.dst.u.all !=
			 ct->tuplehash[!dir].tuple.src.u.all) {
			err = nf_xfrm_me_harder(skb, AF_INET);
			if (err < 0)
				ret = NF_DROP_ERR(err);
		}
#endif
	}
	return ret;
}

1.4 LOCAL_IN 钩子 nf_nat_ipv4_fn()

NAT模块在NF_LOCAL_IN的hook回调函数就是直接调用 nf_nat_ipv4_fn,此处需要注意以下信息:

对于NF_LOCAL_IN链来说,因为nat表中并没有INPUT链,所以对于NF_LOCAL_IN点来说,并不会修改数据包的ip地址,也就是调用alloc_null_binding实现NAT转换,最大的可能就是修改数据包的源端口号,以实现数据连接跟踪项的reply的nf_conntrack_tuple变量是唯一的,且没有被其他连接跟踪项使用。

这也就是为什么需要在NF_LOCAL_IN HOOK点注册HOOK回调函数而又没有在nat表中注册INPUT链的原因。

1.5 公共钩子调用 nf_nat_ipv4_fn()

该函数主要功能就是实现数据的NAT操作(包括SNAT与DNAT),具体来说,就是对一个数据流对应的连接跟踪项仅执行一次SNAT、DNAT,而当数据流对应的连接跟踪项的NAT操作执行完成以后,对于后续的数据包,则直接根据连接跟踪项的 reply 方向的 nf_conntrac_tuple 变量的值进行NAT转换,然后将数据再交给协议栈处理。核心流程如下:

  1. 首先判断数据包是否符合要求(必须不是分段的),数据包对应的连接跟踪项是否符合转换要求等
  2. 对于期望连接来说,对于icmp报文,需要对报文进行NAT转换
  3. 只对new状态的且未进行NAT转换的连接跟踪项,调用nf_nat_rule_find进行连接跟踪项的NAT转换
  4. 进行了上述3的操作后,则会调用 nf_nat_packet() 对数据包进行NAT转换操作。

注意:连接跟踪项的NAT转换只会发生在连接跟踪项刚被创建且还没有进行confirm时,且每个NAT类型的连接跟踪项只会执行一次NAT转换。

/* SRC manip occurs POST_ROUTING or LOCAL_IN */
#define HOOK2MANIP(hooknum) ((hooknum) != NF_INET_POST_ROUTING && \
			     (hooknum) != NF_INET_LOCAL_IN)

static unsigned int
nf_nat_ipv4_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. */
	enum nf_nat_manip_type maniptype = HOOK2MANIP(hooknum);

	/* 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 */
	//对于连接跟踪项为nf_conntrack_untracked,则说明不对该数据包进行连接跟踪,此时直接返回ACCEPT
	if (nf_ct_is_untracked(ct))
		return NF_ACCEPT;

	nat = nfct_nat(ct);
	if (!nat) {
		/* NAT module was loaded late. */
		if (nf_ct_is_confirmed(ct))
			return NF_ACCEPT;
		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;
		}
	}
	/*
	1、对于期望连接original与reply方向的数据包,对于icmp协议的数据包,进行nat操作;
	2、对于期望连接、及非期望连接的NEW状态下的连接跟踪项,只有连接跟踪项的NAT操作没有进行
	的情况下才进行NAT转换操作,具体通过nf_nat_rule_find,查找iptables的nat表中有没有匹配该数据流的NAT规则,若有则根据
	  NAT类型,调用相应的target进行NAT操作(SNAT target 、DNAT target)
	*/
	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,
							   hooknum))
				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;

			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 == NF_NAT_MANIP_SRC ? "SRC" : "DST",
				 ct);
			if (nf_nat_oif_changed(hooknum, ctinfo, nat, out))
				goto oif_changed;
		}
		break;

	default:
		/* ESTABLISHED */
		NF_CT_ASSERT(ctinfo == IP_CT_ESTABLISHED ||
			     ctinfo == IP_CT_ESTABLISHED_REPLY);
		if (nf_nat_oif_changed(hooknum, ctinfo, nat, out))
			goto oif_changed;
	}
	
	//调用nf_nat_packet,根据连接跟踪项的reply tuple变量实现对数据包的NAT操作
	return nf_nat_packet(ct, ctinfo, hooknum, skb);

oif_changed:
	nf_ct_kill_acct(ct, ctinfo, skb);
	return NF_DROP;
}

1.5.1 NAT转换判断 nf_nat_initialized()

这个函数主要是判断传递的连接跟踪项,有没有进行过manip类型的NAT转换。

若 manip 的值为 IP_NAT_MANIP_SRC,则判断连接跟踪项的 status 的 IPS_SRC_NAT_DONE_BIT 位是否为 1,若为 1,则说明该连接跟踪项已经进行了 SNAT 转换,不需要再次转换;对于 DNAT 的判断与上述 SNAT 的判断类似。根据这个函数,就可以避免多次对一个连接跟踪项进行SNAT或者DNAT操作。

enum nf_nat_manip_type {
	NF_NAT_MANIP_SRC,
	NF_NAT_MANIP_DST
};


static inline int nf_nat_initialized(struct nf_conn *ct,
				     enum nf_nat_manip_type manip)
{
	if (manip == NF_NAT_MANIP_SRC)
		return ct->status & IPS_SRC_NAT_DONE;
	else
		return ct->status & IPS_DST_NAT_DONE;
}

1.5.2 NAT 转换操作 nf_nat_rule_find()

功能:实现对数据包关联的连接跟踪项的NAT转换操作。

1.调用 ipt_do_table,查找 nat 表中有没有匹配该连接跟踪项的 nat 规则,若有则根据 NAT 类型调用相应的 target 实现对连接跟踪项的NAT操作(SNAT target 、DNAT target),且将该连接跟踪项的status值中设置已进行NAT转换标志(关于ipt_do_table函数的分析,参见 《linux内核协议栈 netfilter 之 ip 层的 filter 表注册及规则的添加以及钩子函数》)。

2.在调用完ipt_do_table后,该连接跟踪项还没有进行NAT转换,则调用alloc_null_binding进行NAT转换。alloc_null_binding 并不会修改连接跟踪项的reply方向的tupl 、e变量的三层 ip 地址,只有在该连接跟踪项使用的tuple变量值不唯一时,则更新连接跟踪项的reply方向的tuple变量的四层协议相关的关键字(也就是端口号之类的)即可。

static unsigned int nf_nat_rule_find(struct sk_buff *skb, unsigned int hooknum,
				     const struct net_device *in,
				     const struct net_device *out,
				     struct nf_conn *ct)
{
	struct net *net = nf_ct_net(ct);
	unsigned int ret;

	ret = ipt_do_table(skb, hooknum, in, out, net->ipv4.nat_table);
	if (ret == NF_ACCEPT) {
		if (!nf_nat_initialized(ct, HOOK2MANIP(hooknum)))
			ret = alloc_null_binding(ct, hooknum);
	}
	return ret;
}

1.5.3 数据包进行 NAT 转换 nf_nat_packet()

当一个连接跟踪项已经被NAT转换后,后续的数据包则直接进入函数nf_nat_packet,对数据包中的ip地址、端口等进行NAT转换操作。

当一个连接跟踪项刚被被NAT转换后,则其第一个数据包也要接进入函数nf_nat_packet,对数据包中的ip地址、端口等进行NAT转换操作。

代码具体实现如下:

  1. 当为SNAT操作,且是reply方向的PREROUTING时,经过下面的异或后同样可以调用manip_pkt,而因为此时为DNAT,因此就实现了De-SNAT;
  2. 当为DNAT操作,且是reply方向的PREROUTING时,经过下面的异或后同样可以调用manip_pkt,而因为此时为SNAT因此就实现了De-DNAT;
  3. 当为SNAT操作,且是original方向的POSTROUTING时,则调用manip_pkt执行SNAT操作;
  4. 当为DNAT操作,且是original方向的PREROUTING/OUTPUT时,则调用manip_pkt执行DNAT操作。
/* Do packet manipulations according to nf_nat_setup_info. */
unsigned int nf_nat_packet(struct nf_conn *ct,
			   enum ip_conntrack_info ctinfo,
			   unsigned int hooknum,
			   struct sk_buff *skb)
{
	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);

	//根据hook点设置statusbit的值
	if (mtype == NF_NAT_MANIP_SRC)
		statusbit = IPS_SRC_NAT;
	else
		statusbit = IPS_DST_NAT;

	/* Invert if this is reply dir. */
	//对于reply方向,需要执行异或操作
	if (dir == IP_CT_DIR_REPLY)
		statusbit ^= IPS_NAT_MASK;

	/* Non-atomic: these bits don't change. */
	//当连接跟踪项的status变量与statusbit进行位与的结果不为0时:
	//调用函数manip_pkt根据NAT类型修改数据包的ip地址。
	if (ct->status & statusbit) {
		struct nf_conntrack_tuple target;

		/* We are aiming to look like inverse of other direction. */
		nf_ct_invert_tuplepr(&target, &ct->tuplehash[!dir].tuple);

		l3proto = __nf_nat_l3proto_find(target.src.l3num);
		l4proto = __nf_nat_l4proto_find(target.src.l3num,
						target.dst.protonum);
		if (!l3proto->manip_pkt(skb, 0, l4proto, &target, mtype))
			return NF_DROP;
	}
	return NF_ACCEPT;
}

1.6 连接跟踪的地址转换 nf_nat_setup_info()【核心】

这个函数会对4个hook点进来的连接跟踪项进行NAT转换,所以这个函数至此SNAT、DNAT转换,根据HOOK点的类型能够决定转换的类型。该函数最精髓的地方就是调用函数get_unique_tuple,获取一个唯一的且未被其他已经进行NAT转换的连接跟踪项使用的nf_conntrack_tuple变量。当转换成功后,置标记位。

该函数执行的步骤如下:

1.判断传入的hook点是否是NAT相关的hook点,NAT只在PRE_ROUTING、POST_ROUTING、LOCAL_OUT、LOCAL_IN这四个hook点起作用

2.若此时连接跟踪项的status变量中的   IPS_SRC_NAT_DONE_BIT或者IPS_DST_NAT_DONE_BIT位已经被置位了,则打印bug信息,并调用kernel panic

3.根据reply方向的nf_conntrack_tuple结构的变量,获取其反方向的nf_conntrack_tuple结构的变量

4. 调用get_unique_tuple,根据传递的tuple变量,获取一个新的且经过NAT转换的tuple变量,其方向依然是原始方向

5.当新的tuple变量的值与当前的原始方向的tuple变量的值不相等时,进行NAT转换(因为只有在两个值不同时才需要NAT操作):

  • a) 对传递过来的新的 tuple 变量的值,调用 get_unique_tuple,获取该 tuple 变量反方向的 tuple 变量值,即新的 reply 方向的值
  • b) 调用 nf_conntrack_alter_reply 将连接跟踪项的reply方向的 tuplehash[IP_CT_DIR_REPLY].tuple替换为 a)中得到的reply方向的tuple变量,当连接跟踪项不是期望连接项,且还没有创建期望连接时,对根据新的reply反向的tuple变量,在helpers链表中查找新的符合要求的helper变量,并替换调用连接跟踪项中的原来的nf_conntrack_helper变量
  • c) 根据连接跟踪项的NAT类型,设置连接跟踪项的status中相应位(IPS_SRC_NAT/IPS_DST_NAT)

6.若连接跟踪项的当前status变量的IPS_DST_NAT_DONE 与 IPS_SRC_NAT_DONE位均没有置位,则需要将经过NAT操作后的连接跟踪项添加到bysource[]相应的链表中去(调用hash_by_src根据传入的原始方向的tuple变量计算hash值,根据该hash值获取相应的链表bysourece[hash])

7. 根据NAT类型,将连接跟踪项的status变量的IPS_DST_NAT_DONE或者IPS_SRC_NAT_DONE位置位。

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 = nfct_nat(ct);
	if (!nat) {
		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;
		}
	}

	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)
	 */
	 
	//根据输入的nf_conntrack_tuple变量,获取其反方向的nf_conntrack_tuple变量。
	nf_ct_invert_tuplepr(&curr_tuple,
			     &ct->tuplehash[IP_CT_DIR_REPLY].tuple);
				 
	//该函数根据传递的 curr_tuple 与range变量,得到一个新的 new_tuple,
	//此new_tuple的ip地址或者端口号已经进行了NAT转换。
	get_unique_tuple(&new_tuple, &curr_tuple, range, ct, maniptype);

	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);
		nf_conntrack_alter_reply(ct, &reply);

		/* 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 (maniptype == NF_NAT_MANIP_SRC) {
		unsigned int srchash;

		srchash = hash_by_src(net, nf_ct_zone(ct),
				      &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;
		hlist_add_head_rcu(&nat->bysource,
				   &net->ct.nat_bysource[srchash]);
		spin_unlock_bh(&nf_nat_lock);
	}

	/* It's done. */
	//在函数的最后有个置位IPS_DST_NAT_DONE_BIT、IPS_SRC_NAT_DONE_BIT的操作,
	//这就是为了保证一个数据连接跟踪项在某一个NAT转换类型(SNAT、DNAT)上只能初始化一次。
	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);

2 target 函数

2.1 SNAT 功能

1.调用nf_ct_get,获取传入数据包关联的nf_conn变量

2.此处进行SNAT只是设置连接跟踪项中的reply方向的nf_conntrack_tuple变量,因此:

  •    对于主连接,仅设置连接跟踪项的状态为NEW的SNAT操作,因为对于状态不为NEW的连接跟踪项,其reply方向的nf_conntrack_tuple结构的变量的目的地址和端口号已经修改过了,不需要再次修改了;
  •    对于期望连接来说,当期望连接刚建立时,其状态仅为IP_CT_RELATED或者IP_CT_RELATED+IP_CT_IS_REPLY,所以也只对这两种情况的期望连接,进行SNAT操作。

3.调用nf_nat_setup_info,根据targinfo中的地址范围与端口值修改连接跟踪项的reply方向的 nf_conntrack_tuple 变量中的值。

执行这个target只是修改了数据包对应的连接跟踪项的reply方向的tuple变量,并没有修改数据包的ip地址,而修改数据包的ip地址是nat模块的hook函数中执行的(在执行了target操作后才会执行,调用函数nf_nat_packet实现)。


//revision	= 0,
static unsigned int
xt_snat_target_v0(struct sk_buff *skb, const struct xt_action_param *par)
{
	const struct nf_nat_ipv4_multi_range_compat *mr = par->targinfo;
	struct nf_nat_range range;
	enum ip_conntrack_info ctinfo;
	struct nf_conn *ct;

	ct = nf_ct_get(skb, &ctinfo);
	NF_CT_ASSERT(ct != NULL &&
		     (ctinfo == IP_CT_NEW || ctinfo == IP_CT_RELATED ||
		      ctinfo == IP_CT_RELATED_REPLY));

	xt_nat_convert_range(&range, &mr->range[0]);
	return nf_nat_setup_info(ct, &range, NF_NAT_MANIP_SRC);
}

//revision	= 1,
static unsigned int
xt_snat_target_v1(struct sk_buff *skb, const struct xt_action_param *par)
{
	const struct nf_nat_range *range = par->targinfo;
	enum ip_conntrack_info ctinfo;
	struct nf_conn *ct;

	ct = nf_ct_get(skb, &ctinfo);
	NF_CT_ASSERT(ct != NULL &&
		     (ctinfo == IP_CT_NEW || ctinfo == IP_CT_RELATED ||
		      ctinfo == IP_CT_RELATED_REPLY));

	return nf_nat_setup_info(ct, range, NF_NAT_MANIP_SRC);
}

2.2 DNAT 功能

1.调用nf_ct_get,获取传入数据包关联的nf_conn变量

2.此处进行DNAT只是设置连接跟踪项中的reply方向的nf_conntrack_tuple变量,因此:

  •    对于主连接,仅设置连接跟踪项的状态为NEW的DNAT操作,因为对于状态不为NEW的连接跟踪项,其reply方向的nf_conntrack_tuple结构的变量的目的地址和端口号已经修改过了,不需要再次修改了;
  •    对于期望连接来说,当期望连接刚建立时,其状态仅为IP_CT_RELATED,才进行DNAT操作。

3.调用nf_nat_setup_info,根据targinfo中的地址范围与端口值修改连接跟踪项的reply方向的nf_conntrack_tuple变量中的值。

执行这个target只是修改了数据包对应的连接跟踪项的reply方向的tuple变量,并没有修改数据包的ip地址,而修改数据包的ip地址是nat模块的hook函数中执行的(在执行了target操作后才会执行)。

//revision	= 0
static unsigned int
xt_dnat_target_v0(struct sk_buff *skb, const struct xt_action_param *par)
{
	const struct nf_nat_ipv4_multi_range_compat *mr = par->targinfo;
	struct nf_nat_range range;
	enum ip_conntrack_info ctinfo;
	struct nf_conn *ct;

	ct = nf_ct_get(skb, &ctinfo);
	NF_CT_ASSERT(ct != NULL &&
		     (ctinfo == IP_CT_NEW || ctinfo == IP_CT_RELATED));

	xt_nat_convert_range(&range, &mr->range[0]);
	return nf_nat_setup_info(ct, &range, NF_NAT_MANIP_DST);
}

//revision	= 1
static unsigned int
xt_dnat_target_v1(struct sk_buff *skb, const struct xt_action_param *par)
{
	const struct nf_nat_range *range = par->targinfo;
	enum ip_conntrack_info ctinfo;
	struct nf_conn *ct;

	ct = nf_ct_get(skb, &ctinfo);
	NF_CT_ASSERT(ct != NULL &&
		     (ctinfo == IP_CT_NEW || ctinfo == IP_CT_RELATED));

	return nf_nat_setup_info(ct, range, NF_NAT_MANIP_DST);
}

3 实例分析

对于nat相关的函数,我们都分析完了,那我们就分别以SNAT与DNAT两种情况,来分析下数据包在网关中是如何实现地址转换的。

3.1 SNAT

这个就是典型的路由器工作机制,路由器的lan侧设备需要访问到互联网,而又只有路由器上的wan连接存在一个公网地址,此时lan侧pc发来的数据就需要进行SNAT转换。

3.1.1 环境说明

lan1 pc ip:192.168.1.123  

route wan ip为115.22.112.12       

需要访问的外网的地址为ip 14.17.88.99

网关通过iptables做了SNAT,命令如下:

iptables -t nat -A POSTROUTING -s 192.168.1.123/32 -o wan0 -j SNAT --to-source 115.22.112.12

3.1.2 数据SNAT转换分析

当第一个lan侧数据进入到路由器的wan接口时,在 PRE_ROUTING 创建一个nf_conn和两个nf_conntrack_tuple(origin 与reply)。其中origin tuple.src=192.168.1.123 origin tuple.dst=14.17.88.99; reply tuple.src=14.17.88.99 reply tuple.dst=192.168.1.3

当查找路由成功,要转发该数据包时,进入到 POST_ROUTING 链时,进入到NAT的hook函数时,查看到有SNAT的规则,经过SNAT后,会将tuple里的值修改如下:其中origin tuple.src=192.168.1.123 origin tuple.dst=14.17.88.99; reply tuple.src=14.17.88.99 reply tuple.dst=115.22.112.12。

当服务器14.17.88.99回复了一个数据包后(src=14.17.88.99 dst=115.22.112.12),进入到wan侧接口的PRE_ROUTING链时,则在调用其nat相关的hook函数后,会调用函数ip_nat_packet获取到origin tuple值,然后再根据origin tuple,计算出反方向的tuple,即为new_tuple.src = 14.17.88.99 new_tuple.dst = 192.168.1.123,然后就会根据这个新的 tuple 修改其目的ip地址,修改后的数据包的目的地址即为192.168.1.123 。然后再查找路由,将数据发送到正常的lan口。这就是 nat 的 De-SNAT(反向SNAT)

3.2 DNAT

即路由器的lan侧设备中,有一个设备要作为server使用,这时候就需要使用dnat了。

3.2.1 环境说明

lan1 pc ip:192.168.1.183  

route wan ip为115.22.123.12(外网看到的server的ip地址)      

外网的地址为ip 14.17.88.22

iptables -t nat -A PREROUTING -i wan0 -j DNAT --to-destination 192.168.1.183

3.2.2 数据的DNAT分析

当外网client发送一个到server的请求数据。(其src ip 14.17.88.22 dst 115.22.123.12)

当数据到达路由器的wan0口,进入到PRE_ROUTING时,会先建立一个nf_conn结构,和两个nf_conntrack_tuple(origin 与reply)其中origin tuple.src=14.17.88.22 origin tuple.dst=115.22.123.12reply tuple.src=115.22.123.12 reply tuple.dst=14.17.88.22;然后又会进入到 PRE_ROUTING 的hook点的nat hook中,然后调用nat hook,查找nat表的DNAT规则,刚好找到了我们上面创建的规则,接着就会修改reply tuple。将reply tuple.src=115.22.123.12 reply tuple.dst=14.17.88.22修改为reply tuple.src=192.168.1.183 reply tuple.dst=14.17.88.22,然后再根据修改后的reply tuple,取反获取到新的tuple,即new_tuple.src=14.17.88.22,new_tuple.dst=192.168.1.183。然后就会根据这个tuple值将数据包的目的地址修改为192.168.1.183,接着查找路由,将数据包发送给lan侧server。

当lan侧发送一个回应的报文时(数据包的src为192.168.1.183 dst为14.17.88.22),然后当数据进入wan0的PRE_ROUTING链时,由于查找到的nf_conn没有SNAT标志,则会继续查找路由,然后forward这个数据包;当数据包到达POST_ROUTING时,根据nf_conn的flag置位为DNAT,且为reply方向,就会查找origin tuple,然后根据origin tuple的值,取反得到新的tuple:new_tuple.src=115.22.123.12, new_tuple.dst=14.17.88.22,然后根据这个新的tuple,修改数据包的src地址,修改后的数据包的地址为src=115.22.123.12,dst=14.17.88.22,这就是nat的De-DNAT功能。

4 nat表配置演练(iptables )

4.1 配置转发设备

配置一个转发设备或者说是重定,将tcp的客户端和服务器彼此隐藏起来,互相看不见对方。

=========================tcp_client 10.228.90.11==================================
[root@localhost tcp]# ./tcp_client 10.228.90.11 10.228.90.4 22222
bind succ and connect start
connect succ and send start
recv=data ack
recv=data ack
^C
[root@localhost tcp]#

=========================10.228.90.4转发配置======================================
//1、将客户端发过来的包转发到真实服务器(10.228.90.12:11111)地址上,隐藏服务器信息
[root@Node_B ~]# iptables -t nat -A PREROUTING -d 10.228.90.4 -p tcp --dport 22222 -j DNAT --to-destination 10.228.90.12:11111

//2、本地向服务器传输的报文,替换源地址信息,即替换真实客户端的地址,影藏客户端信息
[root@Node_B ~]# iptables -t nat -A POSTROUTING -d 10.228.90.12 -p tcp --dport 11111 -j SNAT --to-source 10.228.90.4

//3、转发功能使能
[root@Node_B ~]# echo 1 > /proc/sys/net/ipv4/ip_forward

=========================tcp_server 10.228.90.12==================================
[root@localhost wq]# ./tcp_server 10.228.90.12 11111
wait for clients connect----------
client IP :10.228.90.4 8003//服务器看到客户端就是转发设备
msg_no=1,msg_version=1,msg_len=1,msg_bit1=0,msg_bit2=1,msg_bit3=1,local_id=100,remote_id=111
msg_no=1,msg_version=1,msg_len=1,msg_bit1=0,msg_bit2=1,msg_bit3=1,local_id=100,remote_id=111
recv: Connection reset by peer
[root@localhost wq]#

注意:当转发设备的本地也存在一个客户端向服务器建链,如果此时和待转发客户端使用相同端口时,即tuple产生冲突,此时本地的转发设备会为后启动客户端重新分配一个端口,

[root@localhost ~]# netstat -anp | grep 11111
tcp        0      0 10.228.90.12:11111          0.0.0.0:*                   LISTEN      21567/./tcp_server
//转发设备先启动本地的客户端
tcp        0      0 10.228.90.12:11111          10.228.90.4:8003            ESTABLISHED 21567/./tcp_server //
//转发设备重定向的客户端后启动
tcp     2000      0 10.228.90.12:11111          10.228.90.4:1024            ESTABLISHED -
[root@localhost ~]#

说明:转发设备先重定向客户端,本地再启动客户端,效果也是一样的,此时本地绑定的8003端口被改写成1024

4.2 本地loopback重定向

本地提供一个对外的 ip 地址 ip_addr1(100.100.100.100:30000),实际监听服务是在监听地址为本地 looback 地址 ,配置信息如下:

[root@localhost ~]# echo 1 > /proc/sys/net/ipv4/conf/eth3/route_localnet
[root@localhost ~]# iptables -t nat -A PREROUTING -p tcp -d 100.100.100.100 --dport 30000 -j DNAT --to-destination 127.0.0.1:30000
[root@localhost ~]#

地址转发并不默认,关键不要忘了网口的 route_localnet 配置(非常非常非常重要),实验如下:

客户端:

[root@localhost ~]# telnet 100.100.100.100 30000
Trying 100.100.100.100...
telnet: connect to address 100.100.100.100: Connection timed out //缺少route_local
[root@localhost ~]# telnet 100.100.100.100 30000
Trying 100.100.100.100...
telnet: connect to address 100.100.100.100: Connection refused //缺少nat转换
[root@localhost ~]# telnet 100.100.100.100 30000
Trying 100.100.100.100...
Connected to 100.100.100.100.//连接成功
Escape character is '^]'.

^C^Z
Connection closed by foreign host.
[root@localhost ~]#

服务器:

[root@localhost ~]# echo 0 > /proc/sys/net/ipv4/conf/eth3/route_localnet
[root@localhost ~]# cat /proc/sys/net/ipv4/conf/eth3/route_localnet
0
[root@localhost ~]# echo 1 > /proc/sys/net/ipv4/conf/eth3/route_localnet
[root@localhost ~]# iptables -t nat -D PREROUTING -p tcp -d 100.100.100.100 --dport 30000 -j DNAT --to-destination 127.0.0.1:30000
[root@localhost ~]# iptables -t nat -A PREROUTING -p tcp -d 100.100.100.100 --dport 30000 -j DNAT --to-destination 127.0.0.1:30000
[root@localhost ~]#

===================================
[root@localhost wq]# ./tcp_server 127.0.0.1 30000
wait for clients connect----------
client IP :100.100.100.101 34490
msg_no=13,msg_version=10,msg_len=48,msg_bit1=2,msg_bit2=3,msg_bit3=1,local_id=774910001,remote_id=774910001
msg_no=255,msg_version=244,msg_len=255,msg_bit1=1,msg_bit2=7,msg_bit3=3,local_id=774909958,remote_id=774910001
msg_no=255,msg_version=237,msg_len=255,msg_bit1=1,msg_bit2=7,msg_bit3=3,local_id=774909958,remote_id=774910001
msg_no=13,msg_version=10,msg_len=255,msg_bit1=1,msg_bit2=7,msg_bit3=3,local_id=774909958,remote_id=774910001
^C
[root@localhost wq]#

===================================

[root@localhost wq]# ifconfig
eth3: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 100.100.100.100  netmask 255.255.255.0  broadcast 100.100.100.255
        inet6 2019::2200:1  prefixlen 112  scopeid 0x0<global>
        inet6 fe80::f816:3eff:fee1:f82c  prefixlen 64  scopeid 0x20<link>
        ether fa:16:3e:e1:f8:2c  txqueuelen 1000  (Ethernet)
        RX packets 1333  bytes 135792 (132.6 KiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 2090  bytes 341764 (333.7 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

eth3:10: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 10.10.10.10  netmask 255.255.255.0  broadcast 10.10.10.255
        ether fa:16:3e:e1:f8:2c  txqueuelen 1000  (Ethernet)

4.2.1 loopback 接口(火星报文)

根据维基百科上针对Loopback得到相关loopback 接口的定义:在同一台机器上执行时的网络应用程序可以进行通信。它完全在操作系统的网络软件中实现,并且不将任何数据包传递给任何网络接口控制器。计算机程序发送到环回IP地址的任何流量都将被简单地立即传回网络软件堆栈,就像从另一个设备接收到的一样。

另外,源地址或目标地址设置为环回地址的任何IP 数据报都不得出现在计算机系统之外(通俗就是说网络上不允许出现源或目的为127.0.0.1的包),也不得由任何路由设备进行路由。必须删除在网络接口上收到具有环回目的地址的报文。这样的分组有时被称为火星分组。与其他虚假数据包一样,它们可能是恶意的,可以通过应用bogon过滤来避免任何可能导致的问题。

看过定义之后才知道原来这个包文是直接被删除了。根据这个帖子所说,可以修改内核的参数,将eth0(即本例中的外网卡)设置为不删除环回目的地址的报文。

默认是禁止的。内核参数如下:

route_localnet - BOOLEAN
    Do not consider loopback addresses as martian source or destination
    while routing. This enables the use of 127/8 for local routing purposes.
    default FALSE

备注:大致意思是不要将来自或发送到 环回接口上的数据包视为火星报文(这里称这个包为火星报文),既不丢弃它,启用用于127.0.0.1的本地路由。

使用以下命令(针对eth0)进行修改该内核参数的值为TRUE:

# sysctl -w net.ipv4.conf.eth0.route_localnet=1
net.ipv4.conf.eth0.route_localnet = 1

永久保存该设置

# echo "net.ipv4.conf.eth0.route_localnet=1" >> /etc/sysctl.conf
# sysctl -p

4.2.2 route_localnet 路由逻辑 ip_route_input_slow()

低版本内核并没有 route_localnet 网口配置选项,如下代码为 linux 3.10.0

static int ip_route_input_slow(struct sk_buff *skb, __be32 daddr, __be32 saddr,
			       u8 tos, struct net_device *dev)
{
	struct fib_result res;
	struct in_device *in_dev = __in_dev_get_rcu(dev);
	struct flowi4	fl4;
	unsigned int	flags = 0;
	u32		itag = 0;
	struct rtable	*rth;
	int		err = -EINVAL;
	struct net    *net = dev_net(dev);
	bool do_cache;

	/* IP on this device is disabled. */

	if (!in_dev)
		goto out;

	/* Check for the most weird martians, which can be not detected
	   by fib_lookup.
	 */

	if (ipv4_is_multicast(saddr) || ipv4_is_lbcast(saddr))
		goto martian_source;

	res.fi = NULL;
	if (ipv4_is_lbcast(daddr) || (saddr == 0 && daddr == 0))
		goto brd_input;

	/* Accept zero addresses only to limited broadcast;
	 * I even do not know to fix it or not. Waiting for complains :-)
	 */
	if (ipv4_is_zeronet(saddr))
		goto martian_source;

	if (ipv4_is_zeronet(daddr))
		goto martian_destination;

	/* Following code try to avoid calling IN_DEV_NET_ROUTE_LOCALNET(),
	 * and call it once if daddr or/and saddr are loopback addresses
	 */
	if (ipv4_is_loopback(daddr)) {
		if (!IN_DEV_NET_ROUTE_LOCALNET(in_dev, net))//route_localnet未使能直接丢弃
			goto martian_destination;
	} else if (ipv4_is_loopback(saddr)) {
		if (!IN_DEV_NET_ROUTE_LOCALNET(in_dev, net))
			goto martian_source;
	}

	...
}

 

以上参见:《为什么不能将客户端的连接请求跳转或转发到本机lo回环接口上?》

4.3 本地地址重定向

本地提供一个对外的 ip 地址 ip_addr1(100.100.100.100:30000),实际监听服务是在监听地址为本地地址 ip_addr2(10.10.10.10:30001),配置信息如下:

[root@localhost ~]# iptables -t nat -A PREROUTING -p tcp -d 100.100.100.100 --dport 30000 -j DNAT --to-destination 10.10.10.10:30001

客户端:

[root@localhost ~]# telnet 100.100.100.100 30000
Trying 100.100.100.100...
telnet: connect to address 100.100.100.100: Connection refused//缺少nat规则
[root@localhost ~]# telnet 100.100.100.100 30000
Trying 100.100.100.100...
Connected to 100.100.100.100.
Escape character is '^]'.

^C

服务器:

[root@localhost wq]# ./tcp_server 10.10.10.10 30001
wait for clients connect----------
client IP :100.100.100.101 34494
msg_no=13,msg_version=10,msg_len=48,msg_bit1=2,msg_bit2=3,msg_bit3=1,local_id=774910001,remote_id=774910001
msg_no=255,msg_version=244,msg_len=255,msg_bit1=1,msg_bit2=7,msg_bit3=3,local_id=774909958,remote_id=774910001
^Z
[1]+  已停止               ./tcp_server 10.10.10.10 30001
[root@localhost wq]#

 

 

 

 

 

 

 

 

 

 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值