概念
NAT(Network Address Translation,网络地址转换),是一种网络通信协议,用于将私有网络中的IP地址转换成公网IP地址,以访问公网资源,实现内网与外网之间的通信;NAT同时也是一种网络安全处理策略。
背景
NAT根据地址转换操作的不同可细分为多种类型,如SNAT、DNAT、全锥型NAT等等,Linux内核中是如何进行NAT处理的,这里以IPv4 NAT处理为例进行学习讨论。
NAT处理
之前在学习Linux内核连接跟踪时提到了Netfilter框架;Netfilter主要功能包括:
包过滤:Netfilter可以根据预定义的规则过滤网络数据包,允许或拒绝它们的传递。
NAT:允许修改数据包的源地址、目的地址和端口信息等。
连接跟踪:追踪网络连接状态,识别相关数据包,并根据连接状态应用相应的规则。
数据包修改:可以对数据报的各个字段进行修改。
由此可见,Linux 内核NAT处理依赖于Netfilter框架;NAT处理即在内核协议栈特定HOOK点(钩子)挂载NAT处理函数,以ipv4为例,和NAT有关的HOOK操作定义如下:
static const struct nf_hook_ops nf_nat_ipv4_ops[] = {
{
.hook = ipt_do_table,
.pf = NFPROTO_IPV4,
.hooknum = NF_INET_PRE_ROUTING,
.priority = NF_IP_PRI_NAT_DST,
},
{
.hook = ipt_do_table,
.pf = NFPROTO_IPV4,
.hooknum = NF_INET_POST_ROUTING,
.priority = NF_IP_PRI_NAT_SRC,
},
{
.hook = ipt_do_table,
.pf = NFPROTO_IPV4,
.hooknum = NF_INET_LOCAL_OUT,
.priority = NF_IP_PRI_NAT_DST,
},
{
.hook = ipt_do_table,
.pf = NFPROTO_IPV4,
.hooknum = NF_INET_LOCAL_IN,
.priority = NF_IP_PRI_NAT_SRC,
},
};
从上述代码可以看出,依据HOOK点的位置,NAT处理分为对转发报文的处理和本机报文的处理;从HOOK点处理函数的优先级,NAT处理分为DNAT处理和SNAT处理,接下来分别讨论。
在介绍NAT处理前,首先回顾下报文在内核协议处理的过程及几个关键HOOK点位置,如下图所示:
转发报文
和转发报文或者说非本机报文NAT处理相关的HOOK操作定义为:
{
.hook = ipt_do_table,
.pf = NFPROTO_IPV4,
.hooknum = NF_INET_PRE_ROUTING,
.priority = NF_IP_PRI_NAT_DST,
},
{
.hook = ipt_do_table,
.pf = NFPROTO_IPV4,
.hooknum = NF_INET_POST_ROUTING,
.priority = NF_IP_PRI_NAT_SRC,
},
报文进入内核协议栈,到达NF_INET_PRE_ROUTING,路由决策前对报文进行DNAT转换,DNAT 处理后路由决策依据转换后的目的IP查找并确定报文的下一跳出口。
在NF_INET_POST_ROUTING报文外发前,对报文进行SNAT处理,可以将内网IP统一转换为一个公网IP进行外部资源访问,同时也是一种对内网IP的保护。
本机报文
本机报文分为到本机的报文和本机外出的报文,本机报文也可以进行SNAT或DNAT处理,具体相关定义如下:
{
.hook = ipt_do_table,
.pf = NFPROTO_IPV4,
.hooknum = NF_INET_LOCAL_OUT,
.priority = NF_IP_PRI_NAT_DST,
},
{
.hook = ipt_do_table,
.pf = NFPROTO_IPV4,
.hooknum = NF_INET_LOCAL_IN,
.priority = NF_IP_PRI_NAT_SRC,
},
当本机报文上送到上层应用程序前,在HOOK点NF_INET_LOCAL_IN处进行SNAT转换,该情况发生在通过反向代理访问外网资源的情况,客户端发送请求报文目的IP为代理服务器的IP,请求报文到达代理服务器,将报文目的IP转换为对真正服务IP地址的访问,服务器回复报文,SIP为服务器IP,目的IP为客户端IP,报文到达代理服务器(防火墙可充当代理服务器),将报文源IP转为代理服务器IP,从而保证正反向的一致性;
本机外出报文在HOOK点NF_INET_LOCAL_OUT处作DNAT转换,需要注意的时,本机外出报文到达该HOOK点时还未确定邻居信息(出接口),如果此时防火墙充当代理服务器的报文,DNAT处理的目的是,将对代理服务器的访问转换位到真正服务器的访问。
NAT处理
在理解NAT处理逻辑前,首先须对iptables规则四表五链的组织有个认知;
四表:
raw表:配置连接跟踪的规则,影响连接跟踪的状态。
mangle表:修改数据包内容,提供一些额外的修改项,如Qos,TTL等
nat表:网络地址转换,允许修改报文源、目的IP地址,源、目的端口信息等
filter表:用于过滤网络数据包,主要用于控制网络数据报文的通过与阻止。
五链:
五链与Netfilter框架中,报文处理的HOOK点相对应,具体如下:
PREROUTING链:数据包路由处理之前经过该链
INPUT链:目的IP为本机的报文应用此链中的规则
FORWARD链:需要转发的报文应用此链中的规则
OUTPUT链:本地输出报文应用此链中的规则
POSTROUTING链:经过路由后的报文应用此链中的规则
不同的表有优先级处理顺序,不同的链根据处理位置不同也有先后,同一表中可以存在多条链,iptables 四表五链总体组织如下:
一条nat iptable规则配置如下:
了解iptables 规则在内核的组织方式,是理解nat处理以及其他规则处理流程的前提,否则根本看不明白,一头雾水,iptables规则的维护涉及到几个数据结构,下面分别讨论:
数据结构
表结构xt_table
struct xt_table {
struct list_head list;
/* 内核不仅有iptables,还有ip6tables, arptables, ebtables,对应不同的协议,每种协议都有多个表(raw,magle...),
一个协议的多个表通过list维护 */
/* What hooks you will enter on */
unsigned int valid_hooks; /* 该表使用的hook点,也对应了该表中有几条链 */
/* Man behind the curtain... */
struct xt_table_info *private; /* 记录表本身的信息,size, 规则数,每条链的入口地址等 */
/* hook ops that register the table with the netfilter core */
struct nf_hook_ops *ops;
/* Set this to THIS_MODULE if you are a module, otherwise NULL */
struct module *me;
u_int8_t af; /* address/protocol family */
int priority; /* hook order */ /* 优先级顺序 */
/* A unique name... */
const char name[XT_TABLE_MAXNAMELEN]; /*表名, 如:nat, filter等 */
};
规则ipt_entry
struct ipt_entry {
struct ipt_ip ip; /* 记录ip头相关信息 */
/* Mark with fields that we care about. */
unsigned int nfcache;
/* Size of ipt_entry + matches */
__u16 target_offset; /* target 相对于ipt_entry起始地址的偏移 */
/* Size of ipt_entry + matches + target */
__u16 next_offset; /* 下一条规则相对于当前规则起始位置的偏移 */
/* Back pointer */
unsigned int comefrom;
/* Packet and byte counters. */
struct xt_counters counters; /* 经过该规则报文个数或字节的统计 */
/* The matches (if any), then the target. */
unsigned char elems[]; /* 存储match信息,target信息,match在前,target在后,数组长度0表示长度可变 */
};
匹配xt_entry_match
struct xt_entry_match {
union {
struct {
__u16 match_size; /* match 信息大小 */
/* Used by userspace */
char name[XT_EXTENSION_MAXNAMELEN];
__u8 revision;
} user;
struct {
__u16 match_size; /* match 信息大小 */
/* Used inside the kernel */
struct xt_match *match;
} kernel;
/* Total length */
__u16 match_size; /* 整个match结构体大小 */
} u;
unsigned char data[]; /* 存储match的具体内容 */
};
目标动作target
struct xt_entry_target {
union {
struct {
__u16 target_size; /* target 的大小 */
/* Used by userspace */
char name[XT_EXTENSION_MAXNAMELEN];
__u8 revision;
} user;
struct {
__u16 target_size; /* target 大小 */
/* Used inside the kernel */
struct xt_target *target;
} kernel;
/* Total length */
__u16 target_size;/* target总大小 */
} u;
unsigned char data[0]; /* target具体信息 */
};
处理流程
从nat操作的定义看,NAT的核心处理函数为ipt_do_table;具体定义如下:
unsigned int
ipt_do_table(void *priv,
struct sk_buff *skb,
const struct nf_hook_state *state)
{
const struct xt_table *table = priv; /* 确认了某个表 */
unsigned int hook = state->hook; /* 获取hook点 */
static const char nulldevname[IFNAMSIZ] __attribute__((aligned(sizeof(long))));
const struct iphdr *ip;
/* Initializing verdict to NF_DROP keeps gcc happy. */
unsigned int verdict = NF_DROP;
const char *indev, *outdev;
const void *table_base;
struct ipt_entry *e, **jumpstack;
unsigned int stackidx, cpu;
const struct xt_table_info *private;
struct xt_action_param acpar;
unsigned int addend;
/* Initialization */
stackidx = 0;
ip = ip_hdr(skb); /* 获取的ip头 */
indev = state->in ? state->in->name : nulldevname; /* 获取报文入接口名 */
outdev = state->out ? state->out->name : nulldevname; /* 获取报文出接口名 */
/* We handle fragments by dealing with the first fragment as
* if it was a normal packet. All other fragments are treated
* normally, except that they will NEVER match rules that ask
* things we don't know, ie. tcp syn flag or ports). If the
* rule is also a fragment-specific rule, non-fragments won't
* match it. */
acpar.fragoff = ntohs(ip->frag_off) & IP_OFFSET;
acpar.thoff = ip_hdrlen(skb);
acpar.hotdrop = false;
acpar.state = state;
WARN_ON(!(table->valid_hooks & (1 << hook)));
local_bh_disable();
addend = xt_write_recseq_begin();
private = READ_ONCE(table->private); /* Address dependency. */
cpu = smp_processor_id(); /* 当前cpu id */
table_base = private->entries; /* 表的起始地址,表第一条规则的起始地址 */
jumpstack = (struct ipt_entry **)private->jumpstack[cpu];
/* Switch to alternate jumpstack if we're being invoked via TEE.
* TEE issues XT_CONTINUE verdict on original skb so we must not
* clobber the jumpstack.
*
* For recursion via REJECT or SYNPROXY the stack will be clobbered
* but it is no problem since absolute verdict is issued by these.
*/
if (static_key_false(&xt_tee_enabled))
jumpstack += private->stacksize * __this_cpu_read(nf_skb_duplicated);
e = get_entry(table_base, private->hook_entry[hook]); /* 获取某条(hook确定)链里的第一条规则指针 */
do {
const struct xt_entry_target *t;
const struct xt_entry_match *ematch;
struct xt_counters *counter;
WARN_ON(!e);
if (!ip_packet_match(ip, indev, outdev,
&e->ip, acpar.fragoff)) { /* e->ip 记录表了匹配信息, 用报文IP 信息去匹配 */
no_match:
e = ipt_next_entry(e); /* 获取该链下一条规则 */
continue;
}
xt_ematch_foreach(ematch, e) {
acpar.match = ematch->u.kernel.match;
acpar.matchinfo = ematch->data;
if (!acpar.match->match(skb, &acpar))
goto no_match;
}
counter = xt_get_this_cpu_counter(&e->counters); /* 更新规则的统计信息 */
ADD_COUNTER(*counter, skb->len, 1);
t = ipt_get_target_c(e); /* 获取规则链的target信息 */
WARN_ON(!t->u.kernel.target);
#if IS_ENABLED(CONFIG_NETFILTER_XT_TARGET_TRACE)
/* The packet is traced: log it */
if (unlikely(skb->nf_trace))
trace_packet(state->net, skb, hook, state->in,
state->out, table->name, private, e);
#endif
/* Standard target? */
if (!t->u.kernel.target->target) {
int v;
v = ((struct xt_standard_target *)t)->verdict;
if (v < 0) {
/* Pop from stack? */
if (v != XT_RETURN) {
verdict = (unsigned int)(-v) - 1;
break;
}
if (stackidx == 0) {
e = get_entry(table_base,
private->underflow[hook]);
} else {
e = jumpstack[--stackidx];
e = ipt_next_entry(e);
}
continue;
}
if (table_base + v != ipt_next_entry(e) &&
!(e->ip.flags & IPT_F_GOTO)) {
if (unlikely(stackidx >= private->stacksize)) {
verdict = NF_DROP;
break;
}
jumpstack[stackidx++] = e;
}
e = get_entry(table_base, v);
continue;
}
acpar.target = t->u.kernel.target;
acpar.targinfo = t->data;
verdict = t->u.kernel.target->target(skb, &acpar); /* 依据具体action 处理报文 */
if (verdict == XT_CONTINUE) {
/* Target might have changed stuff. */
ip = ip_hdr(skb);
e = ipt_next_entry(e);
} else {
/* Verdict */
break;
}
} while (!acpar.hotdrop);
xt_write_recseq_end(addend);
local_bh_enable();
if (acpar.hotdrop)
return NF_DROP;
else return verdict;
}
从上述处理过程来看,NAT的处理过程即找到iptables 具体table表的(nat表的)起始地址,然后根据hook点找到对应链的起始地址,之后遍历链表中所有的规则,逐一匹配,一旦匹配依据target处理函数处理并给出处理结果(NF_DROP/NF_ACCEPT/NF_STOLEN..)。