扼要地介绍Linux内核中netfilter,iptable,连接跟踪,NAT功能。这个分析基于内核版本
请不要奢望通读本文档就能融会贯通这四个功能实现,因为连作者也没有达到那个程度~ 这只是我给自己复习代码时做些路标之用。网上列出netfilter代码的文档已经很多,所以,我只以文字说明为主。
行文难免会有错,请不吝赐教。
一、netfilter。 Netfilter本身并不复杂,它只是在Linux协议栈上的功能点上一种hook注入机制。举个例子,当Linux内核检测到接收到的数据包是到达本机的,就会调用内核函数ip_local_deliver(),这个函数不会直接处理相应的事务,而是主动给Netfilter一次执行hook的机会:
int ip_local_deliver(struct sk_buff *skb)
{
/* 这里省略若干代码 */
return NF_HOOK(PF_INET, NF_INET_LOCAL_IN, skb, skb->dev, NULL, ip_local_deliver_finish);
}
Chain |
函数名 |
注 |
LOCAL_IN |
Ip_local_deliver() |
|
LOCAL_OUT |
IP_VS_XMIT() __ip_local_out() |
__ip_local_out()内会进一步调用dst_output() |
PRE_ROUTING |
Xfrm4_transport_finish() ip_rcv() |
|
POST_ROUTING |
Ip_output() Ip_mc_output() |
dst_output()可能调用它们。 |
FORWARD |
Ip_forward() |
dst_input()可能调用它。 |
Netfilter hook priority |
Hooks |
Chains |
FIRST |
ip_sabotage_in() |
PRE_ROUTING |
CONNTRACK_DEFRAG |
ipv4_conntrack_defrag() |
LOCAL_OUT PRE_ROUTING |
RAW |
ipt_do_table() wrappers |
LOCAL_OUT PRE_ROUTING |
SELINUX_FIRST |
selinux_ipv4_forward() |
FORWARD |
selinux_ipv4_local() |
LOCAL_OUT | |
CONNTRACK |
ipv4_conntrack_in() |
PRE_ROUTING |
ipv4_conntrack_local() |
LOCAL_OUT | |
MANGLE |
ipt_do_tables() wrappers |
All chains |
NAT_DST |
nf_nat_in() |
PRE_ROUTING |
nf_nat_local_fn() |
LOCAL_OUT | |
FILTER |
ipt_do_table() wrappers |
LOCAL_IN LOCAL_OUT FORWARD |
SECURITY |
ipt_do_table() wrappers |
LOCAL_IN LOCAL_OUT FORWARD |
NAT_SRC |
nf_nat_out() |
POST_ROUTING |
nf_nat_fn() |
LOCAL_IN | |
SELINUX_LAST |
selinux_ipv4_postroute() |
POST_ROUTING |
CONNTRACK_CONFIRM |
ipv4_confirm() |
LOCAL_IN POST_ROUTING |
LAST |
无 |
无 |
Iptable通过ip_tables_init()初始化,它调用nf_register_sockopt()为iptables注册一个socket option,这个option用于读或写iptable的配置:Linux的防火墙规则、NAT转换映射最终都是通过这个接口通知内核的。注意,这里只有读和写两种操作,没有改操作。因此,任何写配置的操作都会之前的所有旧配置都替换掉。
通过这个socket option写iptable配置,最终都会调用内核函数do_replace()。这个函数的大致过程是:
<!--[if !supportLists]-->1、 <!--[endif]-->调用translate_table()函数,将用ipt_replace结构描述的输入数据转换为用xt_table_info结构表示。在转换过程中,会要必要的数据完整性检查,同时还会加载所需的内核模块,例如相应iptable table模块,match模块,target模块,nat协议模块等等。
<!--[if !supportLists]-->2、 <!--[endif]-->调用__do_replace()进行实际替换内核的数据结构。
translate_table()涉及到的数据结构众多,可以参考唐文侠士的大作“Linux netfilter机制分析”。这里,我只会该文做些补充。
table |
Valid chain |
Filter |
LOCAL_IN LOCAL_OUT FORWARD |
NAT |
PRE_ROUTING POST_ROUTING LOCAL_OUT |
Mangle |
All chains |
Security |
LOCAL_IN LOCAL_OUT FORWARD |
“匹配要求”和“目标处理”保存于ipt_entry的elems成员内,这又是一个结构数组。这个数组以ipt_match序列开始,之后是ipt_target序列。Ipt_target序列以字节ipt_entry->target_offset开始。
<!--[if !supportLists]-->
<!--[if !supportLists]-->2、 <!--[endif]-->调用resolve_normal_ct(),这是连接跟踪的核心函数;
<!--[if !supportLists]-->3、 <!--[endif]-->调用l4proto->packet(),根据L4协议的设计更新输入skb连接跟踪状态,这个状态信息保存于一个nf_conn数据结构中,一般其变量名为ct。
<!--[if !supportLists]-->4、 <!--[endif]-->若发现是一个REPLY方向的数据包,设置ct->status |= IPS_SEEN_REPLY_BIT,标记这个连接上已经发现了REPLY数据。
<!--[if !supportLists]-->
<!--[if !supportLists]-->2、 <!--[endif]-->在net->ct.hash表中查找tuple,如果没有找到,就调用init_conntrack()返回一个“新的查找结果”;net对应的是一个名字空间的概念,用于实现类似于Solaris中的domain的功能。Net->ct.hash记录了所有已经被跟踪了的连接的信息;
<!--[if !supportLists]-->3、 <!--[endif]-->将查找结果转换为nf_conn结构形式,这个结构是记录连接跟踪状态的主要结构,结果变量名为ct;
<!--[if !supportLists]-->4、 <!--[endif]-->Ctinfo变量记录了当前连接的状态。如果ct在REPLY方向上,ct_info = ESTAB+IS_REPLY,否则:
如果本连接上已经出现了REPLY数据,就
ctinfo = ESTAB
如果本连接是一个期待连接(expected connection),则
Ctinfo = RELATED
否则
Ctinfo = NEW
<!--[if !supportLists]-->5、 <!--[endif]-->用ct和ctinfo更新输入skb。
这里需要一点解释:
2、举一个期待连接的例子。FTP的数据连接和控制连接是两个相关的L4连接。其中数据连接后于控制连接建立。在处理控制连接时,内核可以预见数据连接会在什么端口上建立,这些信息就记录在内核中了。之后真正建立数据连接时,内核会先查找之前记录的信息,如果验证本连接的确是一个期待连接,那么就修改本连接状态为RELATED。类似的处理还见于TFTP、ICMP等。
3、粉色文字所描述的代码是相互互联的。
<!--[if !supportLists]-->1、 <!--[endif]-->调用l3proto和l4proto->invert_tuple()获得REPLY数据包的tuple信息;
<!--[if !supportLists]-->2、 <!--[endif]-->调用l4proto->new();
<!--[if !supportLists]-->3、 <!--[endif]-->在之前的期待连接信息中查找本连接的信息,如果找到说明这是一个我们期待之中的连接,设置相应的标志位;
<!--[if !supportLists]-->4、 <!--[endif]-->初始化需要的conntrack extension;
<!--[if !supportLists]-->5、 <!--[endif]-->将新分配的nf_conn添加到net->ct.unconfirmed哈希表;
<!--[if !supportLists]-->6、 <!--[endif]-->如果可能,调用exp->expectfn();
<!--[if !supportLists]-->
<!--[if !supportLists]-->2、 <!--[endif]-->注意,新增加的nf_conn没有直接增加到net->ct.hash中。因为CONNTRACK之后的包过滤hook可能会扔掉这个数据包,这个ct会在CONNTRACK_CONFIRM的hook内移动到net->ct.hash中。CONNTRACK_CONFIRM的hook实现比较简单,本文不再多言,直接看代码就行了。
在NAT_DST/NAT_SRC上的hooks,最后都会调用nf_nat_fn()函数,这是NAT功能的入口。
<!--[if !supportLists]-->
<!--[if !supportLists]-->2、 <!--[endif]-->若当前ctinfo为RELATED或者RELATED+IS_REPLY,且当前协议为ICMP,就调用nf_nat_icmp_reply_translation(),对ICMP包做特殊NAT处理,本函数返回;
<!--[if !supportLists]-->3、 <!--[endif]-->若当前ctinfo为RELATED或者RELATED+IS_REPLY或者NEW,判断该数据包是否已经作过NAT预处理了,如果没有就调用nf_nat_rule_find()查找nat表作地址修改前的准备工作。但是如果当前chain为LOCAL_IN,就只分配一个alloc_null_binding(),即构造一个不做任何地址映射的NAT配置;
<!--[if !supportLists]-->4、 <!--[endif]-->剩下一种情况是ctinfo为ESTAB,此时不作特别的NAT预处理;
<!--[if !supportLists]-->5、 <!--[endif]-->调用nf_nat_packet()实际修改数据包。
<!--[if !supportLists]-->1、 <!--[endif]-->关于alloc_null_binding(),将nf_nat_rage.min_ip和max_ip设置为与原IP地址相同的IP地址,即不需转换,然后调用nf_nat_setup_info()。
<!--[if !supportLists]-->2、 <!--[endif]-->Nf_nat_rule_find()的核心功能是通过ipt_do_table()完成,额外再处理一些边界条件。而nat表上的两个重要target:SNAT和DNAT的函数最终都会调用nf_nat_setup_info()进实际的NAT预处理操作;
<!--[if !supportLists]-->
<!--[if !supportLists]-->2、 <!--[endif]-->因为以上的结果还有可能是没有NAT转换过的地址,所以这里再用上面的结果调用get_unique_tuple(),获取一个真正可用的NAT转换后地址;
<!--[if !supportLists]-->3、 <!--[endif]-->若新得到的地址信息与前不同,则:
<!--[if !supportLists]-->a) <!--[endif]-->求这个新地址信息“反转”,即转换后的REPLY方向信息;
<!--[if !supportLists]-->b) <!--[endif]-->使用上面的“反转”结果初始化ct->tuplehash[REPLY]。
<!--[if !supportLists]-->4、 <!--[endif]-->将ct->tuplehash[ORIG]加入到net->ipv4.nat_bysource哈希表中。
<!--[if !supportLists]-->
<!--[if !supportLists]-->2、 <!--[endif]-->调用find_best_ips_proto(),通过hash“揉”出可用的NAT转换信息,一个新tuple;
<!--[if !supportLists]-->3、 <!--[endif]-->使用nat proto相关的函数,以确定这个新tuple满足它们的要求,如果有必要nat proto也可修改之。
nf_nat_packet()的代码很少,但核心逻辑有些绕,可以结合以下表格理解它:
NAT类型 |
LAN->WAN |
WAN->LAN |
注解 |
SNAT |
根据reply tuple改SIP |
根据orig tuple改DIP |
一般由LAN侧发起 |
DNAT |
根据orig tuple改SIP |
根据reply tuple改DIP |
一般由WAN侧发起 |