Linux
分类: LINUX
2012-11-05 20:11:02
请不要奢望通读本文档就能融会贯通这四个功能实现,因为连作者也没有达到那个程度~ 这只是我给自己复习代码时做些路标之用。网上列出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);
}
Netfilter在IPv4协议栈上的Hook点如下:
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()可能调用它。 |
不同的Hook间是有优先级区别的,高优先级的Hook会先调用,这不是个可有可无的特性。例如,连接跟踪代码要求输入IPv4分组的所有分片都得到齐了才行,再例如,NAT代码靠一个连接是否已经confirm了判断这个数据包是不是做进一步处理。
Netfilter 在IPv4协议栈上的默认hooks有(其中FIRST的优先级最高,按从高到底排序):
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。
Iptable通过ip_tables_init()初始化,它调用nf_register_sockopt()为iptables注册一个socket option,这个option用于读或写iptable的配置:Linux的防火墙规则、NAT转换映射最终都是通过这个接口通知内核的。注意,这里只有读和写两种操作,没有改操作。因此,任何写配置的操作都会之前的所有旧配置都替换掉。
通过这个socket option写iptable配置,最终都会调用内核函数do_replace()。这个函数的大致过程是:
1、调用translate_table()函数,将用ipt_replace结构描述的输入数据转换为用xt_table_info结构表示。在转换过程中,会要必要的数据完整性检查,同时还会加载所需的内核模块,例如相应iptable table模块,match模块,target模块,nat协议模块等等。
2、调用__do_replace()进行实际替换内核的数据结构。
translate_table()涉及到的数据结构众多,可以参考唐文侠士的大作“Linux netfilter机制分析”。这里,我只会该文做些补充。translate_table()处理时做得一个值得留意的检查是每个规则的有效chain,由此我们可以得到不同table的有效chain:
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_replace,和xt_table_info的entries成员保存的是一个ipt_entry数组,而ipt_entry则到iptable规则本身,包括包模式(ip成员),匹配要求(ipt_match结构),目标处理等信息(ipt_target结构):
“包模式”保存于ipt_entry的ip成员内;
“匹配要求”和“目标处理”保存于ipt_entry的elems成员内,这又是一个结构数组。这个数组以ipt_match序列开始,之后是ipt_target序列。Ipt_target序列以字节ipt_entry->target_offset开始。
Ipt_replace和xt_table_info的成员hook_entry[NF_INET_NUMHOOKS]保存的是一系列entries的偏移。例如,hook_entry[NF_INET_LOCAL_IN]保存着LOCAL_IN链上需要处理的第一个iptable规则的偏移。Iptables的核心函数ipt_do_table()会从这个偏移上找到的iptable规则开始处理。请注意,默认hooks表中有许多hook其实只是ipt_do_table()的包装函数,它们使用不同的iptable table调用它。
Ipt_replace和xt_table_info的成员underflow[NF_INET_NUMHOOKS]保存的是也一系列entries的偏移。有些iptable target可能返回IPT_RETURN,这表明这要求内核返回到上一个处理的规则上,这个回溯关系事实上是一条“链栈”。而每个chain都可以有这样一个链栈,underflow[]记录的就是这个栈的栈底偏移。
Iptable的内核实现内有一个经典的空间换时间的例子。
结合以上介绍,再读ipt_do_table()函数应该就不再那么困难了。
三、连接跟踪。
在默认hooks表内,CONNTRACK优先级上的hook最终都会调用nf_conntrack_in()。
这个函数的核心逻辑如下:
1、调用l4proto->error(),对输入包作L4协议的合法性基本检查。因为conntrack的hook点可能在协议栈的输入路径上,此时L4协议事先还没有机会检查。
2、调用resolve_normal_ct(),这是连接跟踪的核心函数;
3、调用l4proto->packet(),根据L4协议的设计更新输入skb连接跟踪状态,这个状态信息保存于一个nf_conn数据结构中,一般其变量名为ct。
4、若发现是一个REPLY方向的数据包,设置ct->status |= IPS_SEEN_REPLY_BIT,标记这个连接上已经发现了REPLY数据。
Resolve_normal_ct()主要逻辑如下:
1、调用l3proto和l4proto->get_tuple(),获得数据包的连接信息,主要是L3地址,L4端口等;
2、在net->ct.hash表中查找tuple,如果没有找到,就调用init_conntrack()返回一个“新的查找结果”;net对应的是一个名字空间的概念,用于实现类似于Solaris中的domain的功能。Net->ct.hash记录了所有已经被跟踪了的连接的信息;
3、将查找结果转换为nf_conn结构形式,这个结构是记录连接跟踪状态的主要结构,结果变量名为ct;
4、ctinfo变量记录了当前连接的状态。如果ct在REPLY方向上,ct_info = ESTAB+IS_REPLY,否则:
如果本连接上已经出现了REPLY数据,就
ctinfo = ESTAB
如果本连接是一个期待连接(expected connection),则
Ctinfo = RELATED
否则
Ctinfo = NEW
5、用ct和ctinfo更新输入skb。
这里需要一点解释:
1、连接跟踪中的ESTAB状态,不等同于TCP连接中的对应术语;
2、举一个期待连接的例子。FTP的数据连接和控制连接是两个相关的L4连接。其中数据连接后于控制连接建立。在处理控制连接时,内核可以预见数据连接会在什么端口上建立,这些信息就记录在内核中了。之后真正建立数据连接时,内核会先查找之前记录的信息,如果验证本连接的确是一个期待连接,那么就修改本连接状态为RELATED。类似的处理还见于TFTP、ICMP等。
3、粉色文字所描述的代码是相互互联的。
再来看看init_conntrack():
1、调用l3proto和l4proto->invert_tuple()获得REPLY数据包的tuple信息;
2、调用l4proto->new();
3、在之前的期待连接信息中查找本连接的信息,如果找到说明这是一个我们期待之中的连接,设置相应的标志位;
4、初始化需要的conntrack extension;
5、将新分配的nf_conn添加到net->ct.unconfirmed哈希表;
6、如果可能,调用exp->expectfn();
这里也需要一些解释:
1、关于conntrack externsion。有些数据结构不是所有nf_conn结构都需要的,比如期待连接信息,NAT信息等;如果为每个nf_conn都留出保存这些信息的位置是非常浪费空间,为此,内核设计conntrack extension机制。只在需要时,才分配需要的空间,目前只有三种extension。
2、注意,新增加的nf_conn没有直接增加到net->ct.hash中。因为CONNTRACK之后的包过滤hook可能会扔掉这个数据包,这个ct会在CONNTRACK_CONFIRM的hook内移动到net->ct.hash中。CONNTRACK_CONFIRM的hook实现比较简单,本文不再多言,直接看代码就行了。
四,NAT
NAT实现需要保存转换前后的信息,这些信息保存于连接跟踪状态表中,也即nf_conn结构中,其中ORIG方向为原始地址信息,REPLY方向被修改为转换后地址信息。
在NAT_DST/NAT_SRC上的hooks,最后都会调用nf_nat_fn()函数,这是NAT功能的入口。
Nf_nat_fn()的核心逻辑如下:
1、检查当前skb,是否被本函数处理过,如果没有,就检查当前数据包的conn是否已经confirm过。如果已经confirm了,说明这个连接在NAT模块加载之前就已经存在了,此时NAT不对之再作进一步,直接放行;
2、若当前ctinfo为RELATED或者RELATED+IS_REPLY,且当前协议为ICMP,就调用nf_nat_icmp_reply_translation(),对ICMP包做特殊NAT处理,本函数返回;
3、若当前ctinfo为RELATED或者RELATED+IS_REPLY或者NEW,判断该数据包是否已经作过NAT预处理了,如果没有就调用nf_nat_rule_find()查找nat表作地址修改前的准备工作。但是如果当前chain为LOCAL_IN,就只分配一个alloc_null_binding(),即构造一个不做任何地址映射的NAT配置;
4、剩下一种情况是ctinfo为ESTAB,此时不作特别的NAT预处理;
5、调用nf_nat_packet()实际修改数据包。
一些解释:
1、关于alloc_null_binding(),将nf_nat_rage.min_ip和max_ip设置为与原IP地址相同的IP地址,即不需转换,然后调用nf_nat_setup_info()。
2、nf_nat_rule_find()的核心功能是通过ipt_do_table()完成,额外再处理一些边界条件。而nat表上的两个重要target:SNAT和DNAT的函数最终都会调用nf_nat_setup_info()进实际的NAT预处理操作;
nf_nat_setup_info()的核心逻辑:
1、首先将ct->tuplehash[REPLY]反转一下。因为REPLY方向的ct信息可能保存了NAT转换之后的地址信息,这样其实就是在得到可能的NAT转换结果;
2、因为以上的结果还有可能是没有NAT转换过的地址,所以这里再用上面的结果调用get_unique_tuple(),获取一个真正可用的NAT转换后地址;
3、若新得到的地址信息与前不同,则:
a)求这个新地址信息“反转”,即转换后的REPLY方向信息;
b)使用上面的“反转”结果初始化ct->tuplehash[REPLY]。
4、将ct->tuplehash[ORIG]加入到net->ipv4.nat_bysource哈希表中。
Get_unique_tuple()核心逻辑:
1、如果该地址信息已经是SNAT过的,且该地址信息就是为本数据包服务的就直接返回之,没有必要再继续处理了。这个判定过程是通过find_appropriate_src()完成的,在这个函数内部会先查找刚才提到的net->ipv4.nat_bysource哈希表,然后判断是否这个地址信息是否就是“自己人”;
2、 调用find_best_ips_proto(),通过hash“揉”出可用的NAT转换信息,一个新tuple;
3、使用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侧发起 |