一个复杂的nf_conntrack实例全景解析

本文通过一个详细的例子展示了NAT与Linux内核模块nf_conntrack之间的互动过程,包括如何处理连接跟踪、NAT转换及应用层信息的适配等关键技术点。

本文关注两点,一点是细节,另外一点是概览:

  1. 细节:一个完整的关于nf_conntrack和NAT互动的例子
  2. 概览:关于人云亦云的讽刺

近期搜集了一些关于iptables,NAT相关的问题,其中最令人觉得麻烦的还是nf_conntrack相关的东西,比如它和NAT的关系,它和state match的关系,它的Helper机制怎么使用等等。

  因此决定写一篇随笔来一个情景分析,也是方便自己终有一天遗忘了细节知识后复查。

拓扑说明

文全文将围绕Netfilter主题的一片小的领地nf_conntrack来讨论,类似于一个情景分析,在给出全解之前,我先给出一个拓扑:

这里写图片描述

大意就是A试图与B的21端口建立一个类似FTP控制通道连接,然后B反连A的23端口,创建一个类似FTP数据通道连接,中间经过一个NAT Box的节点,旨在实现地址隔离,将1.1.1.1/2.2.2.2元组转换为172.16.1.1/192.168.2.2元组,就这么简单。

  指出一点,以上的拓扑中展示的连接细节跟FTP很像,但却不是FTP,为了指出其不同之处,我来说一下B得以反连A的23端口的细节

  首先,B并不知道要反连哪个地址的哪个端口,这一切都是A在适当的时候告诉B的

  其次,A告诉B的方式很简单,就是把信息封装在应用层数据中,比如把请反连1.1.1.1的23端口这句话作为buffer写入到应用层缓存里。

  因此,由于A的地址1.1.1.1已经被NAT Box改成了172.16.1.1,那么应用层buffer里的IP地址信息也应该做相应的改变,如果关联这一切,这就是本文的要旨,虽然说很简单(至少不会太难),但是对于nf_conntrack机制而言,这个案例却可以说它覆盖了其方方面面,可谓集大成者于一例,不可不学。

实例全景图解

本节中,我将给出几幅图例,按照实际的情景了来当对应的数据包到达NAT Box的时候,Box的conntrack机制到底在做什么以及怎么做。

当来自A的TCP建链包首次从NAT Box网口1到达时

这是首次到达的SYN包,显然在此之前,NAT Box上没有关于该连接的任何记录:
这里写图片描述

当来自B的SYN/ACK返回包从NAT Box网口2到达时

这个情景的重点在于conntrack状态的更新:

这里写图片描述

当来自A的带有反连命令的数据包从NAT Box网口1到达时

这里的重点是应用层数据的解析,即Helper开始发挥作用:

这里写图片描述

当来自B的反连A的SYN包从NAT Box网口2到达时

这是Helper使能的核心:

这里写图片描述

……

后续的部分我想没有必要列举了

代码提纲

了上面的图并且看明白了,我相信你对conntrack就基本了解了,然而对于一个初学者,或者说仅仅想在简历上写上”精读过XXX代码“的人而言,没有什么比弄懂代码更有成就感了。本节我给出Linux 4.14内核版本关于conntrack实现的提纲式概览,对照着上一节的图示,我想应该能把细节全部搞明白。

Netfilter HOOK函数(只包含转发)

static const struct nf_hook_ops ipv4_conntrack_ops[] = {
    { // PREROUTING在RAW之后首先进入
        .hook       = ipv4_conntrack_in,
        .pf     = NFPROTO_IPV4,
        .hooknum    = NF_INET_PRE_ROUTING,
        .priority   = NF_IP_PRI_CONNTRACK,
    },
    { // POSTROUTING在confirm之前
        .hook       = ipv4_helper,
        .pf     = NFPROTO_IPV4,
        .hooknum    = NF_INET_POST_ROUTING,
        .priority   = NF_IP_PRI_CONNTRACK_HELPER,
    },
    {
        .hook       = ipv4_confirm,
        .pf     = NFPROTO_IPV4,
        .hooknum    = NF_INET_POST_ROUTING,
        .priority   = NF_IP_PRI_CONNTRACK_CONFIRM,
    },
};

然后我们分别看下。

nf_conntrack_in的细节

nf_conntrack_in()
{
    l4proto = __nf_ct_l4proto_find(pf, protonum);
    // 这个error调用对于ICMP的RELATED关联比较重要
    ret = l4proto->error(net, tmpl, skb, dataoff, pf, hooknum);
    if (ret <= 0) {
        goto out;
    }
    // 在既有的全局conntrack链表中查找,查不成功则创建
    h = __nf_conntrack_find_get;
    if (!h) {
        h = init_conntrack;
    }
}

init_conntrack()
{
    // 分配结构体内存空间
    ct = __nf_conntrack_allo

    // 确认expect表中有项
    if (net->ct.expect_count) {
        spin_lock(&nf_conntrack_expect_lock);
        // 先查找看自己是不是一个既有的连接所期待的连接
        exp = nf_ct_find_expectation(net, zone, tuple);
        if (exp) {
            // 将其状态设置为RELARED
            __set_bit(IPS_EXPECTED_BIT, &ct->status);
            ct->master = exp->master;
            // 继承其Master的mark
            ct->mark = exp->master->mark;
        }
    }
    if (!exp){
        // 如果不属于任何期待的连接,那它就是一个潜在的Master,因此查一下看有没有和它关联的Helper,以备将来帮助它发现它自己所期待的连接。
        __nf_ct_try_assign_helper
    }
    // 不管怎样,将其加入unconfirmed链表
    nf_ct_add_to_unconfirmed_list(ct);

    if (exp) {
        // 这个很重要,如果Master经历了NAT的洗礼,那么会将当前的这个Slave的tuple也依照Master进行相应的更改,以帮助其在NAT Hook中顺利进行NAT
        if (exp->expectfn)
            exp->expectfn(ct, exp);
    }
}

ipv4_helper的细节

ipv4_helper
{
    if (!ct || ctinfo == IP_CT_RELATED_REPLY)
        return NF_ACCEPT;
    help = nfct_help(ct);
    helper = rcu_dereference(help->helper);
    if (!helper)
        return NF_ACCEPT;

    return helper->help(skb, skb_network_offset(skb) + ip_hdrlen(skb),
                ct, ctinfo);

}

以上框架性的东西,无需解释,如果想知道help回调里到底做了什么,请参考FTP的help回调,一般人都是懂FTP协议的,所以理解起来超级典型,超级简便,比那些SIP之类的强多了,敢问除了搞视频,语音相关的,哪位能精通SIP,可是FTP可是大学标准要学习的玩意儿,要是不会,那是要考试挂科的。大体上,FTP的help回调逻辑如下:

help()
{
    1.解析数据包内容,看能不能发现注册的正则模式,如果不能,则返回
    2.如果发现了,则:
    exp = nf_ct_expect_alloc(ct);
    if (其Mater发生了NAT) {
        修正新发现的exp的tuple
        修正数据包中关于exp内容的IP地址,端口相关的内存
        如果数据包修改前和修改后的长度不同,则标记此数据包需要调整整个TCP数据段的序列号信息,并且重新计算校验和
        ...
    }
}

也怪复杂的,然而配合上节的图示以及抓包,也是很容易理解的,毕竟这是唯一的解释,即便你不看代码,只要你能对FTP和NAT有了很好的把握,你自己也能设计出这个逻辑,如果你设计出的不是这个逻辑,那说明你错了。

ipv4_confirm的细节

ipv4_confirm()
{
    if (test_bit(IPS_SEQ_ADJUST_BIT, &ct->status) &&
        !nf_is_loopback_packet(skb)) {
        // 如果在helper里面标记了数据包需要重新调整序号后,那么就在此处调吧
        if (!nf_ct_seq_adjust(skb, ct, ctinfo, ip_hdrlen(skb))) {
            return NF_DROP;
        }
    }
out:
    return {
        if (!nf_ct_is_confirmed(ct))
            ret = __nf_conntrack_confirm(skb);
    }
}

关于Helper的注册

nf_conntrack有一个nf_conntrack_helper_register接口用于注册一个Helper,在调用该接口前,需要初始化一个nf_conntrack_helper结构体,nf_conntrack同样提供了很方便的接口来帮你进行数据结构的初始化,即nf_ct_helper_init。

  以标准FTP为例,其nf_conntrack_helper结构体会包含端口信息,比如默认就是TCP端口21,在该Helper注册成功后,它将成为匹配链的一员,每当调用init_conntrack初始化一个conntrack表项的时候,均会用当前数据包的端口信息去匹配Helper匹配链,试图绑定一个Helper,以便在合适的时候,帮助其解析或者适配应用层的信息。


注意事项

们总说conntrack效率低下甚至使用了conntrack之后会急剧影响Linux Box的网络处理性能,然而内核社区却一直没有取消conntrack甚至都没有发出过类似的声音,这说明前面说的这帮人是多么不可理喻,他们某时仅仅会求教于别人彻底去除conntrack的方法,然而当我反问“你能列出可以替代state match的规则吗?如果可以,那就可以去掉conntrack”,他们却根本不知道我在说什么,当然,我所提到的肯定跟state match相关,不过话又说回来,如果他们知道了state match的本质,他们肯定也就知道如何彻底去除conntrack的方法,所以说这不能怪他们。还是那句话,不与无知者辩论,沉默是金。

  不管怎么说,虽然我并不认为conntrack存在问题,但是还是要说下使用conntrack时要注意的几个问题,我归纳了三个:

避开不必要的自旋锁

  1. init_conntrack调用时expect查表自旋锁
    高版本内核已经已经优化为有条件查询了,具体要不要查询取决于当前expect表中有没有可用项。
  2. init_conntrack调用时的unconfirmed list自旋锁
    高版本内核已经被优化成percpu,不用担心。
  3. helper调用时发现应用层数据需要help时的expect插入自旋锁
    比如本文所示的类FTP实例,当Helper逻辑发现了应用层存在一个预期的连接时,会插入一个项到expect表中,这个时候会使用一个自旋锁来保护插入动作。
      这里的问题在于,即便你的系统中只有一个流创建了哪怕一个expect表项,所有的流在进入conntrack逻辑时均将会去查询expect表以确定自己是不是一个从属于一个Master的Slave,这意味着所有的流都要去抢这个全局的expect自旋锁,这件事是悲哀的。如何优化掉它呢?请自行思考。
  4. confirm时的unconfirmed自旋锁
    在一个流被confirm时,它首先要从unconfirmed链表中被删除,如前所述,这个锁已经优化为percpu的了,因此不必担心。
  5. confirm时的全局自旋锁
    在一个流被confrm时,它的ORIG tuple和REPLY tuple(如果发生了NAT,REPLY tuple会被更改)会被同时插入到一个全局的hash链表中,此时需要一个自旋锁保护,这是避不开的,好在这个锁对于一个连接而言只需要锁两次,一次是confirm时,一次是被删除时,这显然意味着我们不必过分担心。
      然而,问题在于,如果存在大量的短链接或者同时有大量的连接(比如TCP Timewait)被销毁,这笔开销将是高昂且不可避免的。更有甚至,如果存在DDoS攻击,很多不请自来的数据包将会confirm海量的数据流,自旋锁将会锁死整个江湖…关于这个问题,请参见:
    SYNPROXY抵御DDoS攻击的原理和优化http://blog.youkuaiyun.com/dog250/article/details/77920696

注意CPU和内存操作

仅说几句,不会逗留。

  如果一个变态的熟知协议在应用层牵扯了大量的IP层信息,且该协议在Linux系统中注册了Helper,那么当NAT发生的时候,Helper将花费高昂的成本对应用层的IP地址以及端口信息进行不得已的适配性修改(底层的IP信息被修改要求上层做对应适配),这种内存操作是不可避免的。那么你会怎么选择呢?不注册Helper任其默默失败以保持性能,还是说来者不拒式地提供服务?

  除此之外,抛开内存我们想象一下CPU资源的一种浪费方式。如果应用层保存了大量的基于IP地址的校验码,是不是意味着在NAT发生了之后要将其重新计算一遍呢?

注意net.nf_conntrack_max的大小

这个就不啰嗦了,如果你的机器内存足够,不要吝啬,多多的用于网络协议栈是没有坏处的,特别是当你的机器用于转发设备而不是服务器时,这意味着你没有数据库的开销,没有应用服务器的开销,你的内存设置甚至都不会用于TCP/UDP(我一再强调TCP/UDP属于端到端的技术范畴,而根本就不属于网络技术范畴),因为数据根本就到不了四层处理…那你的内存何用呢?

…..

时间和精力关系,我只能列出以上几点了,如果有人连上面这些都说不上,那还有什么资格说conntrack不好呢?当然,请忽略并原谅那些人云亦云的人。
___________________________
该track的就使用conntrack,不该track的就NOTRACK,这难道不是正确的解题思路吗?如果你在抱怨为什么iptables提供了NOTRACK却没有提供TRACK,那干嘛不去试试ipset呢?其实我自己在很多年前也抱怨过,也人云亦云过,但随着我对Netfilter各个机制各个模块不断深入的理解,我发现即便有问题,不还是可以解决的嘛,如果在根本一无所知的情况下去抵触一个东西,当然不可能有任何解决问题的办法,除了抱怨。


后记

天喝8杯水原来是卖水的广告语,悟性不好的人再努力也不会成为佼佼者,四大美女原来凭的不是长相而是品德,喝酒伤身原来应该把“喝”改成“酗”,原来南方城市和农村过年不吃饺子,粽子和月饼一开始就是包肉的,在这个真假难辨的年代,我知道的唯一确定的事情就是吸烟有害健康

  我不是流言终结者,但我也不会人云亦云,听说某人很牛逼,那必然至少在我面前耍两下才能承认他牛逼,不然都是别人或者他自己吹的。曾经偶然偶尔的一次功绩让一个人成为了神,那他就是永远的神,这显然是屁理论,不信你去问问被人捅了几十刀的Gaius Julius Caesar大神,但人们有找到自己崇拜的人的需求,所以才造就了很多的假大神。

  所以当我听到或者看到一群连conntrack是什么都不懂的人在瞎BB什么conntrack影响性能之类的Pi话,我就感到非常气愤…最终还是选择了奥迪,我倒要看看大部分出自键盘侠的喷子们所说的烧机油是不是真的!

<think>根据用户的问题,我们需要判断使用命令 `lsmod | grep -E 'ip_tables|iptable_filter|nf_conntrack'` 输出的内容能否证明模块配置成功。 分析思路: 1. `lsmod` 命令用于显示已加载到内核中的模块的状态。 2. 使用 `grep` 过滤出与 `ip_tables`、`iptable_filter` 和 `nf_conntrack` 相关的模块。 3. 如果这些模块在输出中显示,则说明它们已经被加载到内核中,即模块加载成功。 注意:模块名称的正确性非常重要。 - 正确的模块名称应该是: - `ip_tables`:这是iptables的核心模块。 - `iptable_filter`:这是iptables的过滤表模块。 - `nf_conntrack`:这是新的连接跟踪模块(在较新的内核中取代了旧的`ip_conntrack`)。 但是,用户之前尝试加载的模块中包括了一些旧的模块(如`ip_conntrack`),而在新的内核中,连接跟踪模块已经更名为`nf_conntrack`。此外,用户还尝试加载了`iptables_nat`(实际模块名可能是`iptable_nat`),但当前命令并没有检查这个模块。 所以,针对用户提供的命令: `lsmod | grep -E 'ip_tables|iptable_filter|nf_conntrack'` 如果输出中包含以下模块(或部分模块),则说明对应的模块已经加载: - `ip_tables` - `iptable_filter` - `nf_conntrack` 但是,需要注意的是: 1. 该命令只能检查这三个模块,其他模块(如`iptable_nat`, `nf_nat`等)不会被检查。 2. 如果输出中没有显示某个模块,则说明该模块没有加载。 因此,如果命令输出中出现了这些模块,那么可以证明这些模块配置成功(即已经加载)。但是,用户之前尝试加载的模块还包括了其他模块(例如`ip_nat_ftp`),这些模块可能依赖于上述模块,所以单独检查这三个模块不能代表所有模块都加载成功。 结论: 该命令的输出可以证明`ip_tables`、`iptable_filter`、`nf_conntrack`这三个模块是否加载成功,但不能证明用户之前尝试加载的所有模块都成功。 举例: 如果执行命令后输出如下: ip_tables 28672 1 iptable_filter iptable_filter 16384 0 nf_conntrack 147456 1 nf_conntrack_netlink 则说明这三个模块都已经加载。 如果缺少其中某个模块,则对应的行不会显示。 因此,用户可以通过这个命令来验证这三个模块,但若要验证其他模块,需要修改grep的模式。 另外,用户之前尝试加载的模块中有几个已经在新内核中被替代,例如: ip_conntrack -> nf_conntrack ip_nat_ftp 依赖于 nf_nat_ftp 和 nf_conntrack_ftp 所以,为了全面检查,建议用户根据实际加载的模块列表进行调整。 建议:如果要检查所有与iptables相关的模块,可以使用更宽泛的模式,例如: lsmod | grep -E 'ip_tables|iptable_|nf_|xt_' 但这样可能会包含很多其他模块,用户可以根据需要筛选。 总结回答: 是的,该命令的输出可以证明列出的三个模块(ip_tables, iptable_filter, nf_conntrack)是否加载成功。但用户之前尝试加载的模块不止这三个,因此需要根据实际需求调整要检查的模块名称。 注意:在较新的内核中,连接跟踪模块已经统一到netfilter框架(nf_开头),所以旧的ip_conntrack模块可能不存在,取而代之的是nf_conntrack。 引用用户给出的引用[1]中的输出: [root@k8sNode1 local]# lsmod |grep conntrack nf_conntrack_ipv4 20480 0 nf_defrag_ipv4 16384 1 nf_conntrack_ipv4 nf_conntrack 114688 1 nf_conntrack_ipv4 这里可以看到`nf_conntrack`模块已经加载,但是没有检查`ip_tables`和`iptable_filter`。 因此,用户需要根据自己加载的模块列表来调整grep的匹配模式。 生成相关问题: </think>是的,使用 `lsmod | grep -E 'ip_tables|iptable_filter|nf_conntrack'` 命令输出的内容可以证明模块配置成功。以下是判断依据和解释: ### 验证模块加载成功的标准 1. **模块名称出现在输出中**: - 如果命令输出包含 `ip_tables`、`iptable_filter` 和 `nf_conntrack` 等模块条目,表明这些模块已成功加载到内核 - 示例有效输出: ``` ip_tables 32768 1 iptable_filter iptable_filter 16384 0 nf_conntrack 167936 2 xt_conntrack,iptable_nat ``` 2. **输出内容需包含关键信息**: - **模块名称**:明确显示 `ip_tables`, `iptable_filter`, `nf_conntrack` 等 - **内存占用**:右侧数字表示模块占用的内存大小(非零值表示已加载) - **依赖关系**:显示被其他模块依赖(如 `iptable_filter` 依赖于 `ip_tables`[^3]) ### 与引用内容的对照验证 根据引用[1]和[3]的实际情况: ```bash # 引用[1]的输出示例 nf_conntrack_ipv4 20480 0 nf_defrag_ipv4 16384 1 nf_conntrack_ipv4 nf_conntrack 114688 1 nf_conntrack_ipv4 [^1] # 引用[3]的模块加载脚本 modprobe ip_tables modprobe iptable_filter modprobe nf_conntrack [^3] ``` - 当 `nf_conntrack` 出现在 `lsmod` 输出中,即证明连接追踪模块加载成功 - 若同时显示 `ip_tables` 和 `iptable_filter` 则表明过滤功能可用 ### 注意事项 1. **模块名称准确性**: - 确保使用正确名称(如 `iptable_filter` 而非 `iptables_filter`) - 新版内核中 `ip_conntrack` 已被 `nf_conntrack` 取代[^1][^3] 2. **依赖模块完整性**: - 基础模块 `ip_tables` 是必须的 - `iptable_filter` 需依赖 `ip_tables` - `nf_conntrack` 是 NAT/状态追踪的基础 3. **验证时机**: - 需在模块加载后执行(如重启后或运行 `modprobe` 命令后) - 临时加载的模块重启后会失效,需确认持久化配置 > 📌 **结论**:只要命令输出中明确显示目标模块名称及其内存占用值(非零),即可证明模块加载成功。这是 Linux 系统验证内核模块状态的**标准方法**[^1][^3]。 --- **相关问题** 1. 如果 `lsmod` 未显示模块,如何诊断 `modprobe` 加载失败的原因? 2. 如何确保内核模块在系统启动时自动加载? 3. `iptables` 和 `nftables` 的内核模块有什么区别? 4. 为什么新版内核用 `nf_conntrack` 取代了 `ip_conntrack`? 5. 如何查看某个内核模块的依赖关系(如 `iptable_nat` 依赖于哪些模块)?
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值