linux中nat的若干细节–基于2.6.8和2.6.17内核分析

在netfilter的nat模块中有一个alloc_null_binding函数,该函数在local_in这个hook点上会被调用,在nat没有初始化的时候也会被调用,在这两种情况会被调用,netfilter规定可在postrouting和local_in执行snat,但是local_in的时候tuplehash[IP_CT_DIR_REPLY]的目的地址是本机,转换源地址没有任何意义,因此只是分配一个null的映射,第二种情况中会在没有找到nat规则的情况下调用,因为如果找到了nat规则,则会在ipt_snat_target或者ipt_dnat_target中调用ip_nat_setup_info,后者会初始化nat,所谓的分配一个null的映射就是将信息原始连接复制一份,在2.6.8中nat的info中的num_manips会保持0,在后续的内核版本中,IPS_SRC_NAT和IPS_DST_NAT均不被置位,内核在nat模块中判断一个连接是否进行nat的依据就是上述的num_manips或者IPS_xxx的位信息,null类型的映射就是为了便于nat模块进行统一处理。
     另外,只有一个数据流的第一个数据包进入主机,既连接流建立的时候才会初始化nat信息,后续的流中数据包只是简单取出nat信息,不会再去查nat规则表,因此不必担心数据流的回复包由于匹配上nat的规则被nat而连接包却没有。
     nat只在数据流的第一个包到来时也就是建立流时起作用,道理如下:resolve_normal_ct为in_conntrack钩子中的流处理函数,其中:
{
h = ip_conntrack_find_get(&tuple, NULL);
//如果没有匹配到现有流则在此处新建一个流,调用init_conntrack
if (DIRECTION(h) == IP_CT_DIR_REPLY) { //如果是同流的反方向包到来了,那么就更新状态,并且在外层调用函数中不管之前IPS_SEEN_REPLY_BIT被设置与否都要再次设置IPS_SEEN_REPLY_BIT位
    *ctinfo = IP_CT_ESTABLISHED + IP_CT_IS_REPLY;
    *set_reply = 1;
} else { //如果是正方向的包,那么不再设置IPS_SEEN_REPLY_BIT位,而是维持其原状
    if (test_bit(IPS_SEEN_REPLY_BIT, &h->ctrack->status)) { //如果原来IPS_SEEN_REPLY_BIT位被设置,则连接肯定是已建立状态
        *ctinfo = IP_CT_ESTABLISHED;
    } else if (test_bit(IPS_EXPECTED_BIT, &h->ctrack->status)) { //关联流
        *ctinfo = IP_CT_RELATED;
    }
    ...//IP_CT_NEW的情况
    *set_reply = 0;
}
skb->nfct = &h->ctrack->infos[*ctinfo]; //skb的nfct字段设置成一个数组元素的地址,该数组的每一个元素代表一个包类型,该数组在后续的比如nat模块中取流状态的时候要使用。
return h->ctrack;
}
最关键的就是resolve_normal_ct的最后那一个赋值操作,这个赋值在后续的比如nat中会被使用,后续的nat模块的HOOK函数的开始需要匹配流,调用__ip_conntrack_get:
static inline struct ip_conntrack * __ip_conntrack_get(struct nf_ct_info *nfct, enum ip_conntrack_info *ctinfo)
{
    struct ip_conntrack *ct = (struct ip_conntrack *)nfct->master;
    *ctinfo = nfct - ct->infos; //resolve_normal_ct的最后赋值的数组元素地址减去数组头的地址得到一个索引,而该索引就是枚举类型ip_conntrack_info
    return ct; //返回一个流结构,上述的强制转换在linux内核使用的很普遍,关键在于如何安排结构体的内容
}
enum ip_conntrack_info
{
    IP_CT_ESTABLISHED,
    IP_CT_RELATED,
    IP_CT_NEW,
    IP_CT_IS_REPLY,
    IP_CT_NUMBER = IP_CT_IS_REPLY * 2 - 1 //这个就是所有流的类型
};
在init_conntrack会有如下调用:
for (i=0; i < IP_CT_NUMBER; i++)
    conntrack->infos[i].master = &conntrack->ct_general;
上述的for循环奠定了后续取流状态的一切数据结构基础,ip_conntrack结构体和infos数组的布局也使得__ip_conntrack_get的强制转换成为可能。
     最后看看IPS_SEEN_REPLY_BIT这个bit有什么用。很多人都知道在/proc/net/下面有一个ip_conntrack文件,该文件在加载了ip_conntrack.ko之后会自动生成(涉及到procfs的知识),文件中每条记录的前面会有ESTABLISHED等状态的字段,这些字段是内核基于一些标志判断得到的,比如IPS_SEEN_REPLY_BIT就是其中之一,一个流中的数据包有两个方向,以发起流的数据包(建立流的数据包)的方向为正方向,如果只有该方向的数据包过来,是不设置IPS_SEEN_REPLY_BIT标志的,可能原先就有这个标志,此时不管它,只有在反方向的数据包通过的时候才会设置IPS_SEEN_REPLY_BIT,这样内核会得到一个信息:通信是闭合的,也就是通信数据包的数量是偶数个。因为有些数据包可能有去无回,有请求无回应,只要有一个回应包,即反方向的数据包通过,则设置IPS_SEEN_REPLY_BIT,该标志的设置会延长流的寿命,毕竟机器内存及处理能力是有限的,大量的流状态保持会消耗很多时间和空间,因此必须为每一个流设置一个超时时间,超时时间过了还没有数据包过往则会删除一个流,为其他的流腾地方。以udp为例,IPS_SEEN_REPLY_BIT的设置会延长其超时时间:
static int udp_packet(struct ip_conntrack *conntrack,
              const struct sk_buff *skb,
              enum ip_conntrack_info conntrackinfo)
{
    if (test_bit(IPS_SEEN_REPLY_BIT, &conntrack->status)) {
        ip_ct_refresh(conntrack, ip_ct_udp_timeout_stream);
        set_bit(IPS_ASSURED_BIT, &conntrack->status);
    } else
        ip_ct_refresh(conntrack, ip_ct_udp_timeout);
    return NF_ACCEPT;
}
细节不再细说,udp_packet也是一个回调函数,是一个协议回调函数,因为对于不同的协议要有不同的策略,比如tcp流是基于连接的,传输层会做好保持链接的一切工作,那就不需ip_conntrack做更多了,它的超时时间要比udp长得多,而且其状态也比udp多一些,所以需要回调函数将机制和策略分离,packet回调函数在ip_conntrack的HOOK函数中被调用。本来想到此打住,不再细说关于IPS_ASSURED_BIT的更多细节,涉及到early_drop等很多函数,也涉及到很多hash操作和链表组织以及没完没了的优化。可是...
init_conntrack(...)
{
    ...
    static unsigned int drop_next; //静态局部变量
    ...
    hash = hash_conntrack(tuple); //如果漫了
    if (atomic_read(&ip_conntrack_count) >= ip_conntrack_max) {
        unsigned int next = (drop_next++)%ip_conntrack_htable_size;
        if (!early_drop(&ip_conntrack_hash[next]) //先从递增的一个类似随机的hash链表中尝试丢弃
            && !early_drop(&ip_conntrack_hash[hash])) { //再尝试从和本包hash到同一个冲突链的链表中排挤,副作用是冲突链大致守恒,进入一个的话就要挤出一个
            ... //谁也不能被挤出,那么就不允许当前包建立流,对于需要nat的连接,结果就是无法联通
            return ERR_PTR(-ENOMEM);
        }
    }
...
}
static int early_drop(struct list_head *chain)
{
    /* Traverse backwards: gives us oldest, which is roughly LRU */
    struct ip_conntrack_tuple_hash *h;
    int dropped = 0;
    h = LIST_FIND_B(chain, unreplied, struct ip_conntrack_tuple_hash *);
    if (h)从链表中找到一个能丢弃的,其原则就是找到一个没有设置IPS_ASSURED_BIT的流,这个在unreplied比较函数中体现
        atomic_inc(&h->ctrack->ct_general.use);
    if (!h)
        return dropped;
    if (del_timer(&h->ctrack->timeout)) {
        death_by_timeout((unsigned long)h->ctrack);
        dropped = 1;
    }
    ip_conntrack_put(h->ctrack);
    return dropped;
}
对于tcp而言,情况大致类似,只是更复杂一些而已。

原文链接: https://blog.csdn.net/dog250/article/details/5719696

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍;

也有高质量的技术群,里面有嵌入式、搜广推等BAT大佬

    linux中nat的若干细节--基于2.6.8和2.6.17内核分析

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/409724

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年4月26日 上午11:35
下一篇 2023年4月26日 上午11:35

相关推荐