linux 内核 netfilter 网络过滤模块 (3)-NAT

本文对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之前,所以不用担心分片包的问题。

 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值