1.NAT的原理
NAT会修改数据包的ip层的源或者目的ip地址。在实际应用中,NAT 主要用于实现私有网络访问公共网络的功能。
1.1 SNAT
源目的地址转换,即对ip数据包的源ip地址进行转换操作,典型的应用即是网关,网关的lan侧会下挂至少两台设备,而这两台设备的ip地址都是lan侧地址,而lan侧设备又要访问公网,这就需要SNAT大展身手了,通过将lan侧发送的ip数据包的源ip地址转换成公网地址即可以访问公网了。
1.2 DNAT
目的地址转换主要是将ip数据包的目的ip地址进行修改。典型的应用就是lan侧的设备需要作为服务器使用,比如网关的lan侧有一个设备用作ftp服务器,而作为服务器就需要一个公网地址。而对于服务器来说,只有客户端先向服务器发送请求后,服务器才会应答,这样的话,客户端发送的请求会首先到达网关,而此时的目的地址为公网地址,这就需要网关将目的ip地址修改成lan侧ftp的lan侧ip地址后才能发送给lan侧ftp服务器,这就需要使用DNAT操作,即DNAT的工作场景
本节我们分析nat表的创建及其hook函数分析,同样的 需要实例化xt_table与ipt_replace这两个结构体。创建filter表时同样需要这样做。
2.NAT模块的初始化
对于NAT模块的初始化,可以分为如下几个方面:
a)NAT表的初始化
b)NAT模块的SNAT与DNAT的target的注册
c)NAT的hook回调函数的初始化
d)四层协议中NAT模块相关的协议变量初始化
下面我们就从这几个方面进行分析
2. 1nat表的注册
首先是nat表的初始化,nat表的初始化与filter表的初始化大致相同,都是
下面是nat表实例化的这两个结构体
#define NAT_VALID_HOOKS ((1<<NF_IP_PRE_ROUTING) | (1<<NF_IP_POST_ROUTING) | (1<<NF_IP_LOCAL_OUT))
static struct
{
struct ipt_replace repl;
struct ipt_standard entries[3];
struct ipt_error term;
} nat_initial_table __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_IP_PRE_ROUTING] = 0,
[NF_IP_POST_ROUTING] = sizeof(struct ipt_standard),
[NF_IP_LOCAL_OUT] = sizeof(struct ipt_standard) * 2 },
.underflow = {
[NF_IP_PRE_ROUTING] = 0,
[NF_IP_POST_ROUTING] = sizeof(struct ipt_standard),
[NF_IP_LOCAL_OUT] = sizeof(struct ipt_standard) * 2 },
},
.entries = {
/* PRE_ROUTING */
{
.entry = {
.target_offset = sizeof(struct ipt_entry),
.next_offset = sizeof(struct ipt_standard),
},
.target = {
.target = {
.u = {
.target_size = IPT_ALIGN(sizeof(struct ipt_standard_target)),
},
},
.verdict = -NF_ACCEPT - 1,
},
},
/* POST_ROUTING */
{
.entry = {
.target_offset = sizeof(struct ipt_entry),
.next_offset = sizeof(struct ipt_standard),
},
.target = {
.target = {
.u = {
.target_size = IPT_ALIGN(sizeof(struct ipt_standard_target)),
},
},
.verdict = -NF_ACCEPT - 1,
},
},
/* LOCAL_OUT */
{
.entry = {
.target_offset = sizeof(struct ipt_entry),
.next_offset = sizeof(struct ipt_standard),
},
.target = {
.target = {
.u = {
.target_size = IPT_ALIGN(sizeof(struct ipt_standard_target)),
},
},
.verdict = -NF_ACCEPT - 1,
},
},
},
/* ERROR */
.term = {
.entry = {
.target_offset = sizeof(struct ipt_entry),
.next_offset = sizeof(struct ipt_error),
},
.target = {
.target = {
.u = {
.user = {
.target_size = IPT_ALIGN(sizeof(struct ipt_error_target)),
.name = IPT_ERROR_TARGET,
},
},
},
.errorname = "ERROR",
},
}
};
static struct xt_table nat_table = {
.name = "nat",
.valid_hooks = NAT_VALID_HOOKS,
.lock = RW_LOCK_UNLOCKED,
.me = THIS_MODULE,
.af = AF_INET,
};
对于这两个结构体,我们已经非常熟悉了,即创建了一个名字为nat的xt_table表,且只在NF_IP_PRE_ROUTING、NF_IP_POST_ROUTING、NF_IP_LOCAL_OUT这3个hook点进行nat操作。且为该表创建了NF_IP_PRE_ROUTING、NF_IP_POST_ROUTING、NF_IP_LOCAL_OUT等3个链,且为每一条链创建了一条target为accept的默认规则。
然后通过调用ipt_register_table即实现了nat表的初始化。
2.2 NAT模块的target注册
对于NAT模块来说,最主要的就是实现地址转换,而iptables中是通过-j实现地址转换的,这就说明NAT模块的注册的target模块一定很重要,那我们现在就分析一下这两个模块
2.2.1 SNAT target的注册
SNAT target的定义如下:
其中target的名称为SNAT,target函数ipt_snat_target,通过这个函数我们能够实现对连接跟踪项的SNAT操作。
static struct xt_target ipt_snat_reg = {
.name = "SNAT",
.target = ipt_snat_target,
.targetsize = sizeof(struct nf_nat_multi_range_compat),
.table = "nat",
.hooks = 1 << NF_IP_POST_ROUTING,
.checkentry = ipt_snat_checkentry,
.family = AF_INET,
};
通过模块名称,我们可以知道怎样通过iptables添加SNAT,如下:
iptables -t nat -A POSTROUTING -s 192.168.1.123 -j SNAT --to-source 25.29.1.23
而ipt_snat_target是实现SNAT的重要函数,我们看下这个函数的定义:
功能:实现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操作后才会执行)。
(疑问:为什么是期望连接时,状态为IP_CT_RELATED或者IP_CT_RELATED+IP_CT_IS_REPLY都认为是起始状态呢?
状态为IP_CT_RELATED时,认为是新创建的连接跟踪项,我是能理解的,但是IP_CT_RELATED+IP_CT_IS_REPLY也
做为新创建的连接跟踪项的依据,我没有搞懂? 而且我感觉不会出现状态为IP_CT_RELATED+IP_CT_IS_REPLY的
连接跟踪项
)
static unsigned int ipt_snat_target(struct sk_buff **pskb,
const struct net_device *in,
const struct net_device *out,
unsigned int hooknum,
const struct xt_target *target,
const void *targinfo)
{
struct nf_conn *ct;
enum ip_conntrack_info ctinfo;
const struct nf_nat_multi_range_compat *mr = targinfo;
NF_CT_ASSERT(hooknum == NF_IP_POST_ROUTING);
ct = nf_ct_get(*pskb, &ctinfo);
/* Connection must be valid and new. */
NF_CT_ASSERT(ct && (ctinfo == IP_CT_NEW || ctinfo == IP_CT_RELATED ||
ctinfo == IP_CT_RELATED + IP_CT_IS_REPLY));
NF_CT_ASSERT(out);
return nf_nat_setup_info(ct, &mr->range[0], hooknum);
}
这个函数主要是通过调用nf_nat_setup_info实现SNAT操作,而对于该函数的分析,我准备放到下一节里分析。
最后通过调用函数xt_register_target,将该target添加到xt[AF_INET].target链表中
2.2.2 DNAT target的注册
static struct xt_target ipt_dnat_reg = {
.name = "DNAT",
.target = ipt_dnat_target,
.targetsize = sizeof(struct nf_nat_multi_range_compat),
.table = "nat",
.hooks = (1 << NF_IP_PRE_ROUTING) | (1 << NF_IP_LOCAL_OUT),
.checkentry = ipt_dnat_checkentry,
.family = AF_INET,
};
该target的名称为DNAT,要使用该target,则在iptables命令中需要增加-j DNAT,下面是一个DNAT的命令:
iptables -t nat -A PREROUTING -i wan0 -j DNAT --to-destination 192.168.1.183
该target的处理函数为ipt_dnat_target,该函数的定义如下,与SNAT的处理函数的定义类似,最终都是通过调用函数nf_nat_setup_info实现DNAT转换。
static unsigned int ipt_dnat_target(struct sk_buff **pskb,
const struct net_device *in,
const struct net_device *out,
unsigned int hooknum,
const struct xt_target *target,
const void *targinfo)
{
struct nf_conn *ct;
enum ip_conntrack_info ctinfo;
const struct nf_nat_multi_range_compat *mr = targinfo;
NF_CT_ASSERT(hooknum == NF_IP_PRE_ROUTING ||
hooknum == NF_IP_LOCAL_OUT);
ct = nf_ct_get(*pskb, &ctinfo);
/* Connection must be valid and new. */
NF_CT_ASSERT(ct && (ctinfo == IP_CT_NEW || ctinfo == IP_CT_RELATED));
if (hooknum == NF_IP_LOCAL_OUT &&
mr->range[0].flags & IP_NAT_RANGE_MAP_IPS)
warn_if_extra_mangle((*pskb)->nh.iph->daddr,
mr->range[0].min_ip);
return nf_nat_setup_info(ct, &mr->range[0], hooknum);
}
在初始化函数里通过调用函数xt_register_target,将该target添加到xt[AF_INET].target链表中
2.3 NAT模块的hook函数的注册
Nat表主要对NF_IP_PRE_ROUTING、NF_IP_POST_ROUTING、NF_IP_LOCAL_OUT、NF_IP_LOCAL_IN这4条hook点感兴趣, 那nat的hook函数同样也是注册在这4个hook点上的。下面分析下nat表的hook函数注册情况。
通过ip_nat_in_ops的定义,我发现其在NF_IP_LOCAL_IN这个hook点上也定义了nf_hook_ops,但是我们在nat表的valid_hooks中并没有发现NF_IP_LOCAL_IN呢。
我们知道nat表上是没有NF_IP_LOCAL_IN链的,即在NF_IP_LOCAL_IN点根本不存在任何规则,那即使在NF_IP_LOCAL_IN hook点注册了hook函数,也不会匹配任何规则,那除非其hook函数并不是遍历nat表,而是进行其他的操作。
/* We must be after connection tracking and before packet filtering. */
/*注册在NF_IP_PRE_ROUTING链上,实现DNAT转换的功能(或者对于SNAT时,对reply的数据包实现De-SNAT功能)*/
static struct nf_hook_ops nf_nat_ops[] = {
/*注册在NF_IP_PRE_ROUTING链上,实现DNAT转换的功能(或者对于SNAT时,对reply的
数据包实现De-SNAT功能)*/
{
.hook = nf_nat_in,
.owner = THIS_MODULE,
.pf = PF_INET,
.hooknum = NF_IP_PRE_ROUTING,
.priority = NF_IP_PRI_NAT_DST,
},
/* After packet filtering, change source */
/*注册在NF_IP_POST_ROUTING实现SNAT转换的功能(或者对于开启DNAT时,在此处
会对数据包实现De-DNAT功能)*/
{
.hook = nf_nat_out,
.owner = THIS_MODULE,
.pf = PF_INET,
.hooknum = NF_IP_POST_ROUTING,
.priority = NF_IP_PRI_NAT_SRC,
},
/* After conntrack, adjust sequence number */
{
.hook = nf_nat_adjust,
.owner = THIS_MODULE,
.pf = PF_INET,
.hooknum = NF_IP_POST_ROUTING,
.priority = NF_IP_PRI_NAT_SEQ_ADJUST,
},
/* Before packet filtering, change destination */
/*
注册在NF_IP_LOCAL_OUT链,实现DNAT转换功能
*/
{
.hook = nf_nat_local_fn,
.owner = THIS_MODULE,
.pf = PF_INET,
.hooknum = NF_IP_LOCAL_OUT,
.priority = NF_IP_PRI_NAT_DST,
},
/* After packet filtering, change source */
/*
注册在NF_IP_LOCAL_IN链上,实现SNAT转换功能
*/
{
.hook = nf_nat_fn,
.owner = THIS_MODULE,
.pf = PF_INET,
.hooknum = NF_IP_LOCAL_IN,
.priority = NF_IP_PRI_NAT_SRC,
},
/* After conntrack, adjust sequence number */
{
.hook = nf_nat_adjust,
.owner = THIS_MODULE,
.pf = PF_INET,
.hooknum = NF_IP_LOCAL_IN,
.priority = NF_IP_PRI_NAT_SEQ_ADJUST,
},
};
接着通过调用nf_register_hooks,即完成nat模块的hook点的注册。
2.4 NAT模块四层协议相关的结构注册
在NAT模块里,四层协议相关的函数与变量,抽象出了结构体nf_nat_protocol,该结构体的定义如下,主要包括四层协议号、转换数据包四层协议相关的关键字的函数、根据输入tuple通过修改四层协议相关的关键字返回一个唯一的未被使用的tuple变量的函数等。
struct nf_nat_protocol
{
/* Protocol name */
const char *name;
/* Protocol number. */
/*四层协议号*/
unsigned int protonum;
/*是否为模块*/
struct module *me;
/* Translate a packet to the target according to manip type.
Return true if succeeded. */
/*对数据包进行四层协议相关的关键字的NAT转换*/
int (*manip_pkt)(struct sk_buff **pskb,
unsigned int iphdroff,
const struct nf_conntrack_tuple *tuple,
enum nf_nat_manip_type maniptype);
/* Is the manipable part of the tuple between min and max incl? */
/*
判断一个连接跟踪的四层协议相关的关键字的值是否在合理的范围内
*/
int (*in_range)(const struct nf_conntrack_tuple *tuple,
enum nf_nat_manip_type maniptype,
const union nf_conntrack_man_proto *min,
const union nf_conntrack_man_proto *max);
/* Alter the per-proto part of the tuple (depending on
maniptype), to give a unique tuple in the given range if
possible; return false if not. Per-protocol part of tuple
is initialized to the incoming packet. */
/*
根据传递的tuple变量与range值,通过随机获取四层协议相关的关键字
找到一个唯一的未被其他连接跟踪项使用的tuple变量。
*/
int (*unique_tuple)(struct nf_conntrack_tuple *tuple,
const struct nf_nat_range *range,
enum nf_nat_manip_type maniptype,
const struct nf_conn *ct);
/*netlink相关*/
int (*range_to_nfattr)(struct sk_buff *skb,
const struct nf_nat_range *range);
int (*nfattr_to_range)(struct nfattr *tb[],
struct nf_nat_range *range);
};
因为不同的协议,其 nf_nat_protocol变量可能不一样,因此对不同的协议需要定义一个nf_nat_protocol变量。NAT模块用nf_nat_protos[MAX_IP_NAT_PROTO]数组存储不同协议的nf_nat_protocol变量,可以通过函数nf_nat_protocol_register将一个新的nf_nat_protocol变量添加到nf_nat_protos[]数组中。Tcp、udp、icmp的nf_nat_protocol变量的定义以及注册函数如下:
struct nf_nat_protocol nf_nat_protocol_tcp = {
.name = "TCP",
.protonum = IPPROTO_TCP,
.me = THIS_MODULE,
.manip_pkt = tcp_manip_pkt,
.in_range = tcp_in_range,
.unique_tuple = tcp_unique_tuple,
#if defined(CONFIG_NF_CT_NETLINK) || defined(CONFIG_NF_CT_NETLINK_MODULE)
.range_to_nfattr = nf_nat_port_range_to_nfattr,
.nfattr_to_range = nf_nat_port_nfattr_to_range,
#endif
};
rcu_assign_pointer(nf_nat_protos[IPPROTO_TCP], &nf_nat_protocol_tcp);
rcu_assign_pointer(nf_nat_protos[IPPROTO_UDP], &nf_nat_protocol_udp);
rcu_assign_pointer(nf_nat_protos[IPPROTO_ICMP], &nf_nat_protocol_icmp);
以上就是四个方面的初始化,下面讲一下这个四个方面分别在哪些初始化函数里被调用。
A)nf_nat_init
该函数主要是进行nat相关的nf_nat_protocol变量的初始化,不同四层协议相关的nf_nat_protocol变量的实现可能不一样,nf_nat_protocol变量主要是实现四层协议相关的NAT转换等操作,即上述2.4是在这个函数里初始化的
B)nf_nat_standalone_init
该函数实现如下三个功能:
1.调用函数nf_conntrack_register_cache创建nat模块相关的nf_conn_help 的slab缓存
2.调用nf_nat_rule_init进行nat表与nat转换相关的target的初始化
3.调用nf_register_hooks进行NAT模块相关的hook函数的注册。
即2.3 是在该函数里初始化的,而2.1与2.2则是通过调用nf_nat_rule_init初始化的
C)nf_nat_rule_init
该函数主要实现如下几个功能:
1.注册nat表
2.调用xt_register_target注册SNAT的target,根据该target变量里的target函数,能够 实现SNAT
3.调用xt_register_target注册DNAT的target,根据该target变量里的target函数,能够 实现DNAT功能
即2.1 2.2是在这个函数里进行初始化的。
以上就是本节的主要内容,本节主要是介绍了NAT模块的初始化,下一节就开始分析NAT模块相关的hook函数,以及NAT转换的实现等功能。