WEBKT

彻底解决 conntrack 表满:利用 eBPF Iterator 实现 TCP 半开连接的精准强制回收

11 0 0 0

在处理高并发网络应用或面临 SYN Flood 攻击时,Linux 内核的 nf_conntrack 表满是一个经典痛点。通常,大家会习惯性地调大 net.netfilter.nf_conntrack_max,或者缩短 nf_conntrack_tcp_timeout_syn_recv 的时间。

但这些方法都是“全局性”的,缺乏灵活性。如果你希望在不影响长连接的前提下,针对性地清理掉那些耗尽资源的 TCP 半开连接(SYN_RECV 状态),eBPF 提供了一种更为优雅且高效的手段:eBPF Iterators

为什么传统的 GC 不够快?

内核原生的 nf_conntrack GC(垃圾回收)逻辑分为两种:

  1. 被动回收:在分配新条目时,如果表满,则尝试回收过期条目。
  2. 周期性回收:内核线程定期扫描部分表项。

当瞬时 SYN 攻击流量巨大时,内核扫描的速度往往跟不上填充的速度。此时,我们需要一个“外部推力”来强制清理特定状态的连接。

eBPF 解决方案:bpf_iter_netfilter

从 Linux 5.12 左右开始,内核引入了针对 Netfilter 的 eBPF 迭代器(Iterators)。它允许用户态程序触发一个 eBPF 程序,遍历内核中所有的 conntrack 条目,并根据自定义逻辑执行操作。

实现思路

我们的目标是:编写一个 eBPF 程序,遍历 nf_conntrack 表,识别出处于 TCP_CONNTRACK_SYN_RECV 状态且持续时间超过阈值的条目,并将其标记为过期或直接删除。

核心代码逻辑(C 语言)

以下是基于 bpf_iter__nf_conntrack 的核心逻辑伪代码:

#include <vmlinux.h>
#include <bpf/bpf_helpers.h>

SEC("iter/nf_conntrack")
int gc_half_open_conns(struct bpf_iter__nf_conntrack *ctx)
{
    struct nf_conn *ct = ctx->ct;
    if (!ct) return 0;

    // 1. 获取 TCP 状态
    // 注意:nf_conn 内部存储了不同协议的状态信息
    u8 proto = ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple.dst.protonum;
    if (proto != IPPROTO_TCP) return 0;

    // 2. 识别 SYN_RECV 状态 (通常对应内核常量 TCP_CONNTRACK_SYN_RECV)
    // 这里需要根据内核版本通过偏移获取具体的 tcp 状态
    u8 tcp_state = ct->proto.tcp.state;

    if (tcp_state == 5) { // 5 对应 TCP_CONNTRACK_SYN_RECV
        // 3. 计算持续时间或检查特定条件
        // 如果该连接已经持续超过 10 秒仍未完成握手
        u64 timeout = ct->timeout;
        u64 now = bpf_jiffies64();

        if (now > timeout) {
            // 4. 强制将其超时时间设为 0,触发内核自然回收
            // 或者在支持的内核版本上使用 bpf_ct_release (需结合具体 helper)
            ct->timeout = 0; 
            
            // 也可以通过 bpf_printk 记录日志用于审计
            bpf_printk("GC Conntrack: Syn-Recv expired, sport: %d", 
                        ct->tuplehash[0].tuple.src.u3.all[0]);
        }
    }

    return 0;
}

如何触发回收?

编写好上述 eBPF 程序并加载到内核后,它并不会自动运行。你需要通过读取关联的文件描述符来触发迭代:

  1. 创建 link:使用 bpf_link_create 绑定该程序。
  2. 获取 iter FD:通过 bpf_iter_create 创建迭代器实例。
  3. 触发读取:在用户态调用 read(iter_fd, ...)。此时内核会遍历整个 conntrack 表并执行你的 eBPF 逻辑。

这种方式的优势在于:按需触发。你可以写一个简单的用户态守护进程,监控 conntrack_count,当水位线超过 90% 时,读取一次迭代器执行强制 GC。

进阶优化:结合 XDP 丢弃无效包

强制回收只是“事后处理”。为了更彻底地解决问题,建议配合 XDP (Express Data Path)

  • XDP 过滤:在流量进入协议栈之前,利用 eBPF 查看当前 nf_conntrack 的负载。
  • SYN Cookie 卸载:如果表即将溢出,直接在 XDP 层对非信任 IP 强制开启 SYN Cookie 校验,完全绕过 conntrack 表的创建,直到三路握手真正完成。

注意事项

  1. 内核版本:bpf_iter 功能需要较新的内核支持(建议 5.15+)。
  2. BTF 支持:确保你的内核编译了 CONFIG_DEBUG_INFO_BTF=y,否则 eBPF 无法识别 struct nf_conn
  3. 写权限:直接在 eBPF 中修改 ct->timeout 需要程序具有相应的特权,并且在某些严格限制的内核环境中可能需要通过特定的 Helper 函数实现。

总结

nf_conntrack 达到瓶颈时,传统的 sysctl 优化往往捉襟见肘。利用 eBPF Iterator,我们可以实现针对特定协议状态(如 SYN_RECV)、特定源 IP 段甚至特定流量特征的 动态连接回收机制。这不仅提升了系统的鲁棒性,也为网络安全防护提供了更底层的控制能力。

内核探测员 eBPFLinux内核网络优化

评论点评