linux内核netfilter之ip_conntrack模块的作用举例--ftp为例

很多协议的控制信息在应用层数据中被包含,这些信息直接影响到了链路的建立,比如ftp协议就是这样,ftp分为port模式和pass模式,port模式中,起初client连接server的21端口,然后当需要传输data的时候,client发送一个控制包给server,包中包含client端开启的端口和自己的ip地址,server收到之后用自己的20端口去连接client控制包中建议的ip和端口,在这种情况下,如果client在nat后面使用私网地址,那么server将无法连接client,因此nat网关必须要处理这种情况,处理方式就是修改client发给server的控制包(如果加密将不可能修改,还好ftp是不加密的);在pass模式下,client连接server的21端口后,如果要传输data,client还要连接server的另一个随机端口,该端口是由server发送的控制包传给client的,如果client或者server端所在的防火墙禁止了任意非熟知端口,那么数据将被防火墙拦截;不管是port模式还是pass模式,防火墙都要处理“第二个”数据连接通路的放行问题,在linux中是通过RELATED状态来放行的,正如前文所述,只需配置一条--state RELATED -j ACCEPT规则即可,但是具体这个规则如何实现,linux的连接追踪模块又是怎样处理ftp的nat问题的,本文详述之。
     首先从ip_conntrack的HOOK函数说起:
unsigned int ip_conntrack_in(...)
{
    ...
    proto = ip_ct_find_proto((*pskb)->nh.iph->protocol); //从数据包中取出协议号
    ...//resolve_normal_ct会试图在已建立的连接中寻找刚进入的包属于的连接,如果找不到则新建立一个状态为NEW的连接,同时还要初始化该连接相关的数据,比如helper
    ct = resolve_normal_ct(*pskb, proto,&set_reply,hooknum,&ctinfo);//此中调用的init_conntrack函数是一个做了很多事的函数
    ...
    if (ret != NF_DROP && ct->helper) {  //如果有helper则调用其help函数
        ret = ct->helper->help(*pskb, ct, ctinfo);
        ...
    }
    ...
}
init_conntrack中有如下逻辑:
...//从链表中查找该连接,如果找到说明这是一个“预测”的连接
expected = LIST_FIND(&ip_conntrack_expect_list, expect_cmp,
                 struct ip_conntrack_expect *, tuple);
...
if (expected) {
    __set_bit(IPS_EXPECTED_BIT, &conntrack->status); //预测的连接到了,设置一个标志,在resolve_normal_ct得到已有连接的情况下会判断如果有了这个标志,则设置IP_CT_RELATED状态,该状态可用于filter的判断
    expected->sibling = conntrack; //预测的连接已经到来并且初始化了。expected->sibling在预测的时候是NULL,因为那时仅仅是预测,连接还没有真的到来,后面可以看到,ip_conntrak预测之后,ip_nat会使用预测结果,然后调用helper的help修改应用层的和连接相关的控制数据,比如ip地址和端口信息,在遍历一个已有连接的所有预测到的连接从而决定是否调用ip_nat的helper时,如果一个预测即一个ip_conntrack_expect的sibling字段非NULL,ip_nat将跳过此预测结果,因为它已经是真实的连接了,说明已经在它还是预测的连接的时候就已经被help过了。
    ...
}
...
     每一个ip_conntrack都可以拥有多个helper,用于帮助处理连接相关的信息,比如ftp协议穿越防火墙就需要处理nat和副连接(data连接)问题,因此就有必要用一个helper模块来处理这一类情况,处理ftp nat的helper和处理副连接的helper其实不是一类helper,前者是ip_nat_ftp结构体,后者是ip_conntrack_ftp结构体,虽然不同,但是它们的处理逻辑和注册逻辑都是一样的,因此到后面说ftp nat的时候再统一说明。下面是ip_conntrack_ftp注册的help函数的实现逻辑
static int help(...)
{
    ...//操作skb,取出我们需要的一切信息
    skb_copy_bits(skb, dataoff, ftp_buffer, skb->len - dataoff);
    ...
    array[0] = (ntohl(ct->tuplehash[dir].tuple.src.ip) >> 24) & 0xFF;
    array[1] = (ntohl(ct->tuplehash[dir].tuple.src.ip) >> 16) & 0xFF;
    array[2] = (ntohl(ct->tuplehash[dir].tuple.src.ip) >> 8) & 0xFF;
    array[3] = ntohl(ct->tuplehash[dir].tuple.src.ip) & 0xFF;
    //以上的这个array就是server需要连接的ip地址
    for (i = 0; i < ARRAY_SIZE(search); i++) {
        if (search[i].dir != dir) continue;
        found = find_pattern(...);//在ftp_buffer中寻找search字符,如果找到了,则说明本次数据包需要help,其中有个参数是个数组,数组的每一个元素都是一个匹配键,此谓search,是一个ftp_search结构体类型的数组
        if (found) break;
    }
    ...//如果找不到则返回,说明本次到来的数据不需要help
    exp = ip_conntrack_expect_alloc();
    ... //初始化一个ip_conntrack_expect,可以用于描述一个将要建立的连接
    exp->expectfn = NULL;
    ip_conntrack_expect_related(exp, ct); //准备添加一个RELATED的连接,如果用户在iptables规则中配置RELATED连接可以通过,那么ftp的port模式数据连接就可以畅行无阻了。iptables的RELATED连接就是在这里被“预料”到的,然后加入进已有的连接。
    ret = NF_ACCEPT;
 out:
    UNLOCK_BH(&ip_ftp_lock);
    return ret;
}
最终会在ip_conntrack_expect_insert函数中将“预料”到的连接加入与此“预料”的连接相关联的已有连接的链表中,同时还将这个预料到的连接加入一个系统全局的链表中,并且如果已有的连接需要限制“预料”连接的建立连接时间,则需要启动一个定时器,定时器超时连接还不到的话,就会删除该预料的连接。这个related连接会被netfilter的state模块使用,比如你使用--state NEW/ESTABLISHED/...的话,在state模块中的match回调函数中,系统会取出该数据包属于的连接,然后取出该连接的state,将之与参数的state比较,然后返回进入target抉择。
     以上是数据在ip_conntrack模块中的流程,出了ip_conntrack就该进入ip_nat了,还是从其HOOK说起:
static unsigned int ip_nat_fn(...)
{
    ...
    ct = ip_conntrack_get(*pskb, &ctinfo); //得到连接,如果没有得到则返回NULL
    ...  //如果没有得到既有连接则返回ACCEPT(注意有ICMP重定向的特殊情况),由后续的链来抉择,不管怎样nat总在conntrack之后起作用,因此只要有连接,conntrack就会将之加入hash
    switch (ctinfo) {
    ...
    case IP_CT_NEW:  //如果是一个连接的第一个包,那么就要初始化一系列结构体,包括两个方向的nat转换表,ftp等等需要help的协议的相关结构体等等
        info = &ct->nat.info;
        WRITE_LOCK(&ip_nat_lock);
        if (!(info->initialized & (1 << maniptype))) {
            ...
            ret = ip_nat_rule_find(pskb, hooknum, in, out, ct, info);
    ...
    return do_bindings(ct, ctinfo, info, hooknum, pskb);
}
unsigned int do_bindings(...)
{
    ...
    int proto = (*pskb)->nh.iph->protocol;

    ...//实施地址/端口转换,省略。就是在两个方向的转换表中根据方向和地址/端口信息来修改数据包的协议头
    helper = info->helper;  //info在ip_nat_setup_info也就是初始化连接的时候就会被建立,这里只是取出来
    if (helper) {
        ...//一个主连接可以有多个与之RELATED的副连接,因此下面就遍历这些副连接
        list_for_each_prev(cur_item, &ct->sibling_list) {
            ...//如果已经是established的连接了,则说明下面将要做的工作已经作过了,就不再做了。
            if (exp_for_packet(exp, *pskb)) {  //包合理则调用help函数,在help函数中处理特殊的nat转换,比如ftp的port模式相关的nat转换
                ret = helper->help(ct, exp, info, ctinfo, hooknum, pskb);
        ...
}
对于ip_nat_ftp帮助模块而言,其help函数的执行逻辑如下:
static unsigned int help(...)
{
    ...
    ct_ftp_info = &exp->help.exp_ftp_info;
    ...
        ftp_data_fixup(ct_ftp_info, ct, pskb, ctinfo, exp);
    ...
}
最终ftp_data_fixup调用了mangle[ct_ftp_info->ftptype](...)函数,显然最后的函数完成了对数据包的修改,对数据包进行修改就是为了ftp服务器可以成功连接到客户端。由于客户端很多时候在具有nat功能的防火墙后,并且都是用私网地址,而在ftp的port模式下,如果客户端将一个私有地址建议给了ftp服务器用于连接,服务器是连接不到的,这个建议的ip地址在ftp的数据包中,因此必须修改数据包,将建议的地址和端口修改为nat后的地址和端口,同时再铺设一条nat,用于服务器连接客户端时将请求真正转到内网的客户端。ip_nat_mangle_tcp_packet是ip_nat_helper.c中的一个很重要的函数,就是它完成了对应用层数据包的修改。
     ip_conntrack和ip_nat处理ftp总的过程就是,ip_conntrack模块得到了连接的信息,然后根据连接信息可以得到一个helper,需要说明,一个连接完全可以没有helper,而且大多数的都没有helper,是否需要helper是根据连接的类型决定的,一般触及应用层控制数据的修改时才会使用helper,比如ftp的控制命令是在应用层数据中被传输的,连接的类型是可以从数据包以及协议头中得到的,因此ip_conntrack模块需要数据包不能分段,也就是说需要完整的ip数据包。得到helper之后开始调用其help函数,然后判断当前数据包是否需要help,比如判断是否是ftp的特殊命令,该命令可以建立一条新的连接,如果是这样的话,那么help函数则“预测”到一条即将建立的连接并将之和当前连接关联,然后ip_conntrack基本就没有什么做的了,数据包继续在netfilter中流动,进入nat,同样的,nat也如ip_conntrack判断是否需要help,如果是则调用helper的help函数,判断是否需要help的依据一般就是是否在ip_conntrack模块中“预测”到了即将建立的连接,如果预测到了,那么就调用nat的helper的help函数,并且将预测到的连接参数传入,在ip_nat_ftp的help函数中根据预测连接的信息对应用层控制数据进行修改。
     两类helper的注册都是在模块初始化的时候进行的,而helper与连接或者nat的绑定则是在连接初始化的时候进行的。ip_nat_fn是nat的HOOK,其中对于IP_CT_NEW包来讲需要调用call_expect,而后者最终调用下面的函数实现ftp相关的ip_nat_helper结构体的指定,该结构体在模块初始化时被注册:
unsigned int ip_nat_setup_info(...)
{
    ...
    info->helper = LIST_FIND(&helpers, helper_cmp, struct ip_nat_helper *, &reply);
    //寻找ip_nat_ftp的helper,在ip_nat_ftp的init中,会调用ip_conntrack_helper_register将ftp相关的信息注册进内核,这些信息包含在ip_nat_helper结构体中,其中有很多静态数据是用于匹配helper的,比如新建一个连接,当数据越过conntrack而进入nat时会调用ip_nat_setup_info,在该函数中,如上述调用LIST_FIND,其实就是使用当前的addr,port等信息和注册的helper逐个进行比较,一旦有命中的则将此helper取出留作后用,ip_nat_helper中最重要的就是help函数了。
    ...
}
ip_nat_ftp模块的初始化函数如下:
static int __init init(void)
{
    ...
    for (i = 0; (i < MAX_PORTS) && ports[i]; i++) {
        ftp[i].tuple.src.u.tcp.port = htons(ports[i]);
        ftp[i].tuple.dst.protonum = IPPROTO_TCP;
        ftp[i].mask.src.u.tcp.port = 0xFFFF;
        ftp[i].mask.dst.protonum = 0xFFFF;
        ftp[i].max_expected = 1;
        ftp[i].timeout = 0;
        ftp[i].flags = IP_CT_HELPER_F_REUSE_EXPECT;
        ftp[i].me = ip_conntrack_ftp;
        ftp[i].help = help;
        ...
        ret = ip_conntrack_helper_register(&ftp[i]);
        ...
    }
    return 0;
}
类似的ip_conntrack的helper也是在模块初始化时注册,在连接初始化时被指定特定的连接的,道理和nat是一样的。
     总之,helper模块一般是对需要在应用层数据中传输控制数据的协议进行帮助的,因为OS实现的协议栈并不包含应用层,但是有的时候必须对应用层控制数据进行修改,这时就不得不需要一个额外的帮助模块了,注意,一般helper修改的都是控制数据,而不是业务数据,所谓控制数据就是和业务无关的,仅仅影响到连接本身的数据。

你可能感兴趣的:(linux内核netfilter之ip_conntrack模块的作用举例--ftp为例)