在上一节中,我们介绍了iptables和netfilter的基本关系,在这里我们会进一步介绍现有的NAT以及Linux中大多已实现的MASQUERADE实现原理。

1. NAT类型介绍

  常用的NAT类型主要有Full Cone NAT(全锥型), Restricted NAT(限制型锥型), Port Restricted NAT(端口限制型锥型), Symmetric NAT(对称型)四种。

  四种类型的主要区别在于对外界访问内部IP的控制力度。为方便解释,我们使用如下的用语来说明四种NAT类型的不同之处。

用语定义

  1. 内部Tuple:指内部主机的私有地址和端口号所构成的二元组,即内部主机所发送报文的源地址、端口所构成的二元组
  2. 外部Tuple:指内部Tuple经过NAT的源地址/端口转换之后,所获得的外部地址、端口所构成的二元组,即外部主机收到经NAT转换之后的报文时,它所看到的该报文的源地址(通常是NAT设备的地址)和源端口
  3. 目标Tuple:指外部主机的地址、端口所构成的二元组,即内部主机所发送报文的目标地址、端口所构成的二元组

详细释义

  1. Full Cone NAT:所有来自同一 个内部Tuple X的请求均被NAT转换至同一个外部Tuple Y,而不管这些请求是不是属于同一个应用或者是多个应用的。除此之外,当X-Y的转换关系建立之后,任意外部主机均可随时将Y中的地址和端口作为目标地址 和目标端口,向内部主机发送UDP报文,由于对外部请求的来源无任何限制,因此这种方式虽然足够简单,但却不那么安全

  2. Restricted Cone NAT: 它是Full Cone的受限版本:所有来自同一个内部Tuple X的请求均被NAT转换至同一个外部Tuple Y,这与Full Cone相同,但不同的是,只有当内部主机曾经发送过报文给外部主机(假设其IP地址为Z)后,外部主机才能以Y中的信息作为目标地址和目标端口,向内部 主机发送UDP请求报文,这意味着,NAT设备只向内转发(目标地址/端口转换)那些来自于当前已知的外部主机的UDP报文,从而保障了外部请求来源的安 全性

  3. Port Restricted Cone NAT:它是Restricted Cone NAT的进一步受限版。只有当内部主机曾经发送过报文给外部主机(假设其IP地址为Z且端口为P)之后,外部主机才能以Y中的信息作为目标地址和目标端 口,向内部主机发送UDP报文,同时,其请求报文的源端口必须为P,这一要求进一步强化了对外部报文请求来源的限制,从而较Restrictd Cone更具安全性

  4. Symmetric NAT:这是一种比所有Cone NAT都要更为灵活的转换方式:在Cone NAT中,内部主机的内部Tuple与外部Tuple的转换映射关系是独立于内部主机所发出的UDP报文中的目标地址及端口的,即与目标Tuple无关; 在Symmetric NAT中,目标Tuple则成为了NAT设备建立转换关系的一个重要考量:只有来自于同一个内部Tuple 、且针对同一目标Tuple的请求才被NAT转换至同一个外部Tuple,否则的话,NAT将为之分配一个新的外部Tuple;打个比方,当内部主机以相 同的内部Tuple对2个不同的目标Tuple发送UDP报文时,此时NAT将会为内部主机分配两个不同的外部Tuple,并且建立起两个不同的内、外部 Tuple转换关系。与此同时,只有接收到了内部主机所发送的数据包的外部主机才能向内部主机返回UDP报文,这里对外部返回报文来源的限制是与Port Restricted Cone一致的。不难看出,如果说Full Cone是要求最宽松NAT UDP转换方式,那么,Symmetric NAT则是要求最严格的NAT方式,其不仅体现在转换关系的建立上,而且还体现在对外部报文来源的限制方面。

2. Masquerade 用户空间的实现

  在Netfilter中,不同NAT的实现在内核中进行。而通常情况下,Linux系统自带的NAT类型多为端口限制型锥型NAT或者对称型。对此,我们参照着内核和用户层的源码进行说明。这里之所以说为端口限制型锥型NAT或对称型NAT是因为系统自带的MASQUERADE部分模块根据配置命令行的不同可以实现上述两种不同类型的NAT。由上一篇,大家应该都知道了iptables和Netfilter的关系,这里我们先看看在用户层,Iptables命令中关于MASQUERADE的实现。

  源码名libipt_MASQUERADE.c,位置的话不同版本的linux系统可能不大一样。该部分主要是实现对配置masquerade命令的解析以及注册,注册之后即可激活内核空间中的masquerade相应钩子函数实现masquerade功能。关于钩子函数和内核中masquerade的实现,在下一讲中将会简单介绍。

  首先看一下该源码的大体框架。

  由图可见,该部分主要由help, opts, init, parse_ports, parse, final_check, print, save, masq, _init组成。按
各个函数、结构的名称大致可以猜测到该函数做的是什么功能,之后实现其他几种NAT均需要模仿该模块完成。

  首先需要介绍的是masq。源码如下

static struct iptables_target masq = {
	 NULL,
	.name= "MASQUERADE",
	.version= IPTABLES_VERSION,
	.size= IPT_ALIGN(sizeof(struct ip_nat_multi_range)),
	.userspacesize= IPT_ALIGN(sizeof(struct ip_nat_multi_range)),
	.help= &help,
	.init= &init,
	.parse= &parse,
	.final_check= &final_check,
	.print= &print,
	.save= &save,
	.extra_opts= opts
 
};

  该部分是整个MASQUERADE的注册,给定了该结构体的各个元素(名称、版本、函数实现等)。所有其他的iptables命令行参数均以此形式添加,注意这里的名字必须和内核中注册的钩子函数一致才可生效。关于为何这样写即可成功注册并与内核中的相应钩子函数配对,需要研究相关源码才可了解,在此无法简单概括,后面会单独写一篇进行讲解。

  help部分即命令行常用的帮助部分,在MASQUERADE中,实现如下。

/* Function which prints out usage message. */
static void
help(void)
{
 
	printf(
	"MASQUERADE v%s options:\n"
	" --to-ports <port>[-<port>]\n"
	"Port (range) to map to.\n"
	" --random\n"
	"Randomize source port.\n"
	"\n"
	,
	IPTABLES_VERSION);
 
}

  这里我们可以很清晰的了解iptables中MASQUERADE如何配置。之前所说的MASQUERADE可以配置两种不同类型的NAT也来源于此,提供的–random功能即为对称型NAT,其他两种形式命令行则为端口限制型锥形NAT的配置。其实这里的help是为使用该命令行的人提供的功能。当你添加了这个程序到Netiflter中,在命令行输入iptables -t nat -h即可自动出现该帮助菜单。

  opts是对用户输入做出控制的结构体,和上文中提供的功能对应,代码如下。

static struct option opts[] = {
 
	{
	 	"to-ports", 1, 0, '1'
	},
	{
	 	"random", 0, 0, '2'
	},
	{
	 	0 
	}
 
};

  parse函数是对用户输入命令行的解析,主要用于判断是否正确输入命令,并做了详细的异常处理机制(其实也就是打印提示错误),代码如下,其中已对关键位置做注释说明。

/* Function which parses command options; returns true if it
   ate an option */
static int
parse(int c, char **argv, int invert, unsigned int *flags,
      const struct ipt_entry *entry,
      struct ipt_entry_target **target)
{
 
	int portok;
	struct ip_nat_multi_range *mr
	= (struct ip_nat_multi_range *)(*target)->data;
	 
	 /* 协议检测 */
	if (entry->ip.proto == IPPROTO_TCP
	    || entry->ip.proto == IPPROTO_UDP
	    || entry->ip.proto == IPPROTO_ICMP)
		portok = 1;
		else
		portok = 0;
		
	 /* 输入1个参数则需要检查端口,输入2个即random类型不需要检测,直接给标记为赋值 */	 
	switch (c) {
	 
		case '1':
			if (!portok)
				exit_error(PARAMETER_PROBLEM,
			   "Need TCP or UDP with port specification");
			 
			if (check_inverse(optarg, &invert, NULL, 0))
				exit_error(PARAMETER_PROBLEM,
			   "Unexpected `!' after --to-ports");
			 
			parse_ports(optarg, mr);
			return 1;
		 
		case '2':
			mr->range[0].flags |=  IP_NAT_RANGE_PROTO_RANDOM;/*添加标记,该标记使得此类型转化为对称型NAT*/
			return 1;
			 
		default:
			return 0;
 
	}
 
}

  parse中调用的parse_port代码如下,同样在代码中注释说明。

/* Parses ports */
static void
parse_ports(const char *arg, struct ip_nat_multi_range *mr)
{
 
	const char *dash;
	int port;
	 
	 /*标记位赋值*/
	mr->range[0].flags |= IP_NAT_RANGE_PROTO_SPECIFIED;
	 
	port = atoi(arg);
	if (port <= 0 || port > 65535)
		exit_error(PARAMETER_PROBLEM, "Port `%s' not valid\n", arg);
	 
	 /*这里允许不输入范围,即限定仅使用某个端口,dash = 0*/
	dash = strchr(arg, '-');
	if (!dash) {	 
		mr->range[0].min.tcp.port
		= mr->range[0].max.tcp.port
		= htons(port);	 
	} else {
	 
		int maxport;
		 
		maxport = atoi(dash + 1);
		if (maxport == 0 || maxport > 65535)
			exit_error(PARAMETER_PROBLEM,
			   "Port `%s' not valid\n", dash+1);
		if (maxport < port)
		/* People are stupid.  Present reader excepted. */
			exit_error(PARAMETER_PROBLEM,
			   "Port range `%s' funky\n", arg);
			mr->range[0].min.tcp.port = htons(port);
			mr->range[0].max.tcp.port = htons(maxport);
	 
	}
 
}

  其余都是很容易弄懂的一些函数,比如print即为打印出当前配置,save为保存配置,在此不做过多的介绍,有问题的可以联系我。

  本篇结束。


欢迎关注本人公众号,公众号会更新一些不一样的内容,相信一定会有所收获。
在这里插入图片描述

Logo

更多推荐