netfilter 概要

来源:互联网 发布:新闻发布网站源码 编辑:程序博客网 时间:2024/05/21 06:49

扼要地介绍Linux内核中netfilter,iptable,连接跟踪,NAT功能。这个分析基于内核版本2.6.28。

       请不要奢望通读本文档就能融会贯通这四个功能实现,因为连作者也没有达到那个程度~ 这只是我给自己复习代码时做些路标之用。网上列出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);
}

       这里NF_HOOK宏就是netfilter的核心入口了。它的主要功能实现是nf_hook_slow(),这个函数的逻辑不算复杂,处理普通包的代码非常直观,只要留意一下NF_REPEAT/NF_QUEUE/NF_STOLEN的情况即可。

 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()。这个函数的大致过程是:

<!--[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机制分析”。这里,我只会该文做些补充。

        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()。

        这个函数的核心逻辑如下:

<!--[if !supportLists]--> 1、  <!--[endif]-->调用l4proto->error(),对输入包作L4协议的合法性基本检查。因为conntrack的hook点可能在协议栈的输入路径上,此时L4协议事先还没有机会检查。

<!--[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数据。

        Resolve_normal_ct()主要逻辑如下:

<!--[if !supportLists]--> 1、  <!--[endif]-->调用l3proto和l4proto->get_tuple(),获得数据包的连接信息,主要是L3地址,L4端口等;

<!--[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变量记录了当前连接的状态。如果ctREPLY方向上,ct_info = ESTAB+IS_REPLY,否则:

              如果本连接上已经出现了REPLY数据,就

                     ctinfo = ESTAB

              如果本连接是一个期待连接(expected connection),则

                     Ctinfo = RELATED

              否则

                     Ctinfo = NEW

<!--[if !supportLists]-->5、  <!--[endif]-->用ct和ctinfo更新输入skb。

 

这里需要一点解释:

        1、连接跟踪中的ESTAB状态,不等同于TCP连接中的对应术语;

       2、举一个期待连接的例子。FTP的数据连接和控制连接是两个相关的L4连接。其中数据连接后于控制连接建立。在处理控制连接时,内核可以预见数据连接会在什么端口上建立,这些信息就记录在内核中了。之后真正建立数据连接时,内核会先查找之前记录的信息,如果验证本连接的确是一个期待连接,那么就修改本连接状态为RELATED。类似的处理还见于TFTP、ICMP等。

       3、粉色文字所描述的代码是相互互联的。

        再来看看init_conntrack():

<!--[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]--> 1、  <!--[endif]-->关于conntrack externsion。有些数据结构不是所有nf_conn结构都需要的,比如期待连接信息,NAT信息等;如果为每个nf_conn都留出保存这些信息的位置是非常浪费空间,为此,内核设计conntrack extension机制。只在需要时,才分配需要的空间,目前只有三种extension。

<!--[if !supportLists]-->2、  <!--[endif]-->注意,新增加的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()的核心逻辑如下:

<!--[if !supportLists]--> 1、  <!--[endif]-->检查当前skb,是否被本函数处理过,如果没有,就检查当前数据包的conn是否已经confirm过。如果已经confirm了,说明这个连接在NAT模块加载之前就已经存在了,此时NAT不对之再作进一步,直接放行;

<!--[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预处理操作;

        Nf_nat_setup_info()的核心逻辑:

<!--[if !supportLists]--> 1、  <!--[endif]-->首先将ct->tuplehash[REPLY]反转一下。因为REPLY方向的ct信息可能保存了NAT转换之后的地址信息,这样其实就是在得到可能的NAT转换结果;

<!--[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哈希表中。

        Get_unique_tuple()核心逻辑:

<!--[if !supportLists]--> 1、  <!--[endif]-->如果该地址信息已经是SNAT过的,且该地址信息就是为本数据包服务的就直接返回之,没有必要再继续处理了。这个判定过程是通过find_appropriate_src()完成的,在这个函数内部会先查找刚才提到的net->ipv4.nat_bysource哈希表,然后判断是否这个地址信息是否就是“自己人”;

<!--[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侧发起

 

0 0