Linux协议栈-netfilter(3)-NAT
来源:互联网 发布:网络编辑是什么 编辑:程序博客网 时间:2024/05/22 01:33
本文对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。但LOCAL IN和LOCAL OUT上的hook点一般不做工作,因此我们只关注PRE ROUTING和POST ROUTING的hook点。
下文中有的地方将conntrack简写为ct。
nf_nat_fn()函数:
static unsigned intnf_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;elsereturn 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;}} elsepr_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之前,所以不用担心分片包的问题。
- Linux协议栈-netfilter(3)-NAT
- Linux协议栈-netfilter(1)-框架
- Linux协议栈-netfilter(2)-conntrack
- Linux协议栈-netfilter(4)-期望连接
- Linux协议栈-netfilter(5)-iptables
- Linux协议栈-netfilter(1)-框架
- 【Linux 驱动】Netfilter/iptables (八) Netfilter的NAT机制
- netfilter NAT
- netfilter中对多连接协议跟踪和NAT实现
- Linux协议栈优化之Netfilter分类conntrack
- Linux netfilter 学习笔记 之十一 ip层netfilter的NAT模块初始化以及NAT原理
- Linux netfilter 学习笔记 之十一 ip层netfilter的NAT模块初始化以及NAT原理
- 网络协议栈(10)netfilter
- Linux netfilter 学习笔记 之十二 ip层netfilter的NAT模块代码分析
- Linux netfilter 学习笔记 之十二 ip层netfilter的NAT模块代码分析
- netfilter的nat 分析
- Netfilter,iptable与NAT
- linux网络协议栈(五)网络层 (11)NAT穿越
- 百度地图的定位和poi搜索
- FFmpeg命令行工具系列三---参数详解
- Python核心编程 第七章 练习7–5
- android 4.0+版本 socket通信报错
- java反射(二)
- Linux协议栈-netfilter(3)-NAT
- 算法提高 最大乘积
- Ambarella处理器启动过程
- 编写一个函数,将一个数字字符串转换成这个字符串对应的数字(包括正浮点数、负浮点数)
- Excel Sheet Column Title
- MongoDB安装
- kmp
- Android—Android中 Your content must have a ListView whose id attribute is 'android.R.id.list'错误的解决办法
- Python之深浅拷贝