linux内核netfilter之ip_conntrack模块的作用--抽象总结

来源:互联网 发布:php炫酷源码 编辑:程序博客网 时间:2024/05/12 04:58

nat规则将数据流的地址信息进行转换,转换了之后需要将转换后的地址信息写入ip_conntrack结构体中,经过nat之后目的地址无非两个方向,一个是本机(redirect target),一个是其它机器(网关上的nat一般都这样),于是netfilter需要对这两个方向的转换记录提供支持。
     netfilter的ip_conntrack提供了下列两个HOOK,ip_conntrack_out_ops用于nat到其它机器的支持,ip_conntrack_local_in_ops提供nat到本机的支持:
static struct nf_hook_ops ip_conntrack_out_ops = {
    .hook        = ip_refrag, //这个名字起得有些怪异,不过也很合理,因为ip_conntrack由于可能需要操作4层以上协议头或数据载荷,因此必须在PREROUTING这个挂载点上对ip数据包进行defrag操作。
    .owner        = THIS_MODULE,
    .pf        = PF_INET,
    .hooknum    = NF_IP_POST_ROUTING,
    .priority    = NF_IP_PRI_LAST,
};
static struct nf_hook_ops ip_conntrack_local_in_ops = {
    .hook        = ip_confirm,  //这个名字很好,不过也没有覆盖其全部功能
    .owner        = THIS_MODULE,
    .pf        = PF_INET,
    .hooknum    = NF_IP_LOCAL_IN,
    .priority    = NF_IP_PRI_LAST-1,
};
不管是ip_refrag还是ip_confirm,最终都是要调用__ip_conntrack_confirm的,从__ip_conntrack_confirm这个函数的逻辑可以一眼看出ip_conntrack的结构:
int __ip_conntrack_confirm(struct nf_ct_info *nfct)
{
    ...
    hash = hash_conntrack(&ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple);
    repl_hash = hash_conntrack(&ct->tuplehash[IP_CT_DIR_REPLY].tuple);
    ...
    if (!LIST_FIND(...IP_CT_DIR_ORIGINAL...) //如果原始流和nat后(也可以不做nat,此时两个tuple是一样的)的流信息都没有加入hash
        && !LIST_FIND(...IP_CT_DIR_REPLY...)) {
        list_prepend(&ip_conntrack_hash[hash],
                 &ct->tuplehash[IP_CT_DIR_ORIGINAL]);
        list_prepend(&ip_conntrack_hash[repl_hash],
                 &ct->tuplehash[IP_CT_DIR_REPLY]);
        ct->timeout.expires += jiffies;
        ...
        set_bit(IPS_CONFIRMED_BIT, &ct->status); //流已经顺利地经过了本机
        ...
    }
    ...
}
最后,通过ip_conntrack为引子来用一种简单的方式描述一下ip_conntrack在整个netfilter中的作用以及其实现逻辑:
struct ip_conntrack
{
    ...
    struct ip_conntrack_tuple_hash tuplehash[IP_CT_DIR_MAX]; //一共2个方向
    ...
    struct list_head sibling_list; //related的连接
    ...
    struct ip_conntrack_expect *master; //和sibling_list的意义相反
    ...
    struct {
        struct ip_nat_info info;
        union ip_conntrack_nat_help help;
    } nat;
};
repl是replace的意思,是这样的,因为orig是不会被修改的,所以repl虽然字面是reply,实则replace,不管是SNAT还是DNAT,修改的都是数据包往目的地方向的链路信息,对于SNAT来说,数据流进入网关,然后被snat,此时orig的tuple是不会变的,变化的是reply的tuple,因为数据回应回来以后,目的地址就是转换后的源地址,而源地址就是当前数据流的目的地址,所以repl作为reply和replace来讲都是一样的。
     事实是,tuple只是为了匹配流的,在数据包刚进入时,为了将数据包和一个流相联系,tuple就有用了,ip_conntrack_tuple中包含足够的信息用于匹配一个流,包括ip地址和端口号:
struct ip_conntrack_tuple
{
    struct ip_conntrack_manip src;  //和下面的dst类似
    struct {
        u_int32_t ip; //ip地址
        union {
        ...//应用层协议
        } u;
        u_int16_t protonum; //传输层协议
    } dst;
}; //该结构包含两个ip-port对
     下面用一种抽象的方式将ip_contrack以及nat的流程化繁为简而为一种简单的形式,从情景角度分析数据包流经nat网关时内核中相关模块的行为,数据流以(x-y)表示,并且忽略端口信息,忽略状态信息。其实状态信息很重要,netfilter的其它模块可以使用ip_contrack设置的状态从而做出特殊的决策,同时状态信息还可以标识一个流目前的行为以及目前其需要被给与的行为,忽略之是为了事情更简单,引出一种分析代码的方式,凡遗漏之事务容日后视心力与心情加之。
     数据包(a-b)进入R,发生了snat,地址信息成为了(m-b),虽然发生了nat,(a-b)和(m-b)应该是属于同一个数据流CK的,ip_conntrack需要作记录,以便将两个流绑定在一起,数据从a到b的方向在R处成了由m到b的方向,属于一个方向,都是源到目的,发生了snat后,数据就可以出去了,既然数据出去R了,我们也就不关心它了,我们关心的是从b发出的回应a数据到达R后如何将之绑定到流CK,数据回来后由于发生过snat流标示显然是(b-m),于是ip_conntrack需要将(b-m)也绑定到CK,此时我们可以定义出CK了:
struct Contrack {
    Two-Direct[2];
}CK = {
    Two-Direct[0] = (a-b);
    Two-Direct[1] = (b-m);
};
#define IP_CT_DIR_ORIGINAL 0
#define IP_CT_DIR_REPLY 1
和上面的ip_conntrack对比一下看看少了什么?既然上面的例子是在R处发生了snat,那么nat的指导信息显然也应该在CK中,加上后就圆满了。nat信息实际上是一个数组,并且是两个方向上的,比如下列规则:
-A POSTROUTING -d 172.16.0.0/255.255.0.0 -o eth0 -j MASQUERADE
实际上有两条nat规则,一个是到达172.16.0.0网络的数据源地址转化为eth0的地址,另一条是从172.16.0.0回应的数据包的地址还要转化回来,否则数据就有去无回了,所以针对一个源地址(-s)或者目的地址(-d),一共需要有(两个方向*一条流最大挂载点)条nat规则,看一下一个流最大可以有几个挂载点,如果数据只是过路,那么最大也就两个,既作snat,又做dnat,可是如果去本机出入的流,那么很有可能会有三个挂载点,因此nat规则一共需要3对也就是6条,用数组表示就是nat_info[6],于是CK成了:
CK = {
    Two-Direct[0] = (a-b);
    Two-Direct[1] = (b-m);
    NAT-Info[] = {,,,,,};
};
最终NAT-Info中的元素是什么呢?肯定是地址信息了,最简单的方式就是每个元素就是一个ip-port对,然后通过数组下标来索引nat的类型,比如定义:
#define SRC_NAT 0
#define OPPOSITE_SRC 1
#define DST_NAT 2
#define OPPOSITE_DST 3
...
有了上述定义后来初始化nat信息数组:
NAT-Info[] = {{src_ip_to,port1},{src_ip_from,port2},
          {dst_ip_to,port3},{dst_ip_from,port4},
           ...}; //对应于上述CK的定义,这里的src_ip_to就是m,而src_ip_from就是a
src_ip_to是需要转换成的新源ip,src_ip_from是回应数据需要转换回的原始源ip,dst前缀的ip地址的含义类似,这张表初始化完了之后,对应数据流再有数据包来的时候就可以直接通过查这张表来进行地址转换了。由于来回两个方向的流都被映射进了CK结构体,因此不管哪个方向过来数据,(a-b)也好,(b-m)也好,都会对应到同一个CK,这在linux中是通过hash实现的,既然找到了CK,从CK中取出NAT-Info就可以得到如何转化地址的信息,很显然,不可能每次数据包到来时都要查nat表,而是在一个流的第一个包到达时就确定了NAT-Info,也就是一个流(CK)建立的时候,建立一个CK之后,查找nat表,如果有规则命中,那么根据nat表的规则来建立NAT-Info信息,同时还要更新Two-Direct[1]为新的转换后的流(注意是反向的),该流的第一个数据包流出机器或者流往用户层的时候将上述流信息记录到hash中,两个方向的都要记录,如果再有包来临,不管哪个方向的,请求包还是回应包,通过地址信息查询hash都可以找到CK,然后到了nat的时候,直接从CK将NAT-Info取出即可,取出后判断当前是哪个HOOK,根据当前的HHOK来使用NAT-Info中的信息实行地址转换。整个过程中,CK的作用就是追踪连接,它最大的共享就是一个流的第一个包来的时候建立CK,之后nat会使用这个初始CK查nat表,之后再来数据包CK以及其中的信息比如nat信息就可以直接取出来使用了,是conntrack模块取出,后续模块使用。
     不要被内核代码中复杂的细节所蒙蔽,其实每一段代码每一个机制的思想都是很简单的,正如ip_conntrack-nat的思想以及数据结构的设计和我上述的说明一样,如果能通过阅读代码将数据结构抽象成最简单的形式并且剖析出思想,那么阅读代码才算是有了收获,否则总有一天会迷失于茫茫字符海中而不可自拔,最终只见树木不见森林,搞得自己也不想再钻研了。理解了大致的流程之后,再次阅读代码的时候就要详细些了,以下是几个比较重要的函数:
ip_nat_setup_info:初始化ip_nat_info信息;
find_best_ips_proto_fast:初始化nat后的新的tuple;
ip_conntrack_alter_reply:配置由于nat而改变的反向tuple;
do_bindings:nat模块实施nat转换。

原创粉丝点击