WEBKT

深入 Linux 内核:使用 bpftrace 实时追踪 Conntrack 状态迁移规律

6 0 0 0

在排查复杂的网络抖动、NAT 丢包或防火墙连接超时问题时,Linux 内核的 conntrack(连接跟踪)模块是绕不开的核心。虽然我们常用 conntrack -L 查看当前快照,或用 conntrack -E 监控实时事件,但在面对“连接为何突然从 ESTABLISHED 回退到 SYN_RECV”或“非预期的状态迁移规律”时,传统工具往往显得力不从心。

借助 bpftrace(基于 eBPF 的高级追踪语言),我们可以直接深入内核函数内部,捕获 conntrack 状态机每一次精确的跳变。

为什么选择 bpftrace?

传统的 conntrack -E 输出的是用户态封装后的事件,而 bpftrace 可以让你:

  1. 获取更细粒度的上下文:比如触发状态改变的具体内核调用栈。
  2. 自定义逻辑过滤:仅观察特定 IP 范围且发生了“特定状态转换”的连接。
  3. 极低的开销:eBPF 在内核态完成数据处理,无需像抓包那样频繁进行内核与用户态的数据拷贝。

核心原理:寻找 Hook 点

Conntrack 对 TCP 状态的处理主要集中在内核源码的 net/netfilter/nf_conntrack_proto_tcp.c 中。其中,tcp_packet 函数负责根据收到的报文更新连接状态。

我们的目标是挂载到这个函数(或其内部更新状态的逻辑),提取出连接的五元组信息以及 old_statenew_state

准备工作

确保你的系统支持 BTF(BPF Type Format),这样 bpftrace 可以直接读取内核结构体定义:

# 检查 BTF 是否开启
ls /sys/kernel/btf/vmlinux

编写 bpftrace 监控脚本

下面的脚本将实时捕获 TCP 连接在 conntrack 表中的状态迁移。

#!/usr/bin/bpftrace

#include <net/netfilter/nf_conntrack.h>
#include <linux/skbuff.h>
#include <net/netfilter/nf_conntrack_tuple.h>

BEGIN
{
    printf("开始监控 Conntrack 状态迁移... 按 Ctrl+C 停止\n");
    printf("%-20s %-25s %-15s -> %-15s\n", "TIME", "CONNECTION", "OLD_STATE", "NEW_STATE");

    // 定义 TCP 状态枚举映射(对应内核中的 tcp_conntrack 枚举)
    @tcp_states[0] = "NONE";
    @tcp_states[1] = "SYN_SENT";
    @tcp_states[2] = "SYN_RECV";
    @tcp_states[3] = "ESTABLISHED";
    @tcp_states[4] = "FIN_WAIT";
    @tcp_states[5] = "CLOSE_WAIT";
    @tcp_states[6] = "LAST_ACK";
    @tcp_states[7] = "TIME_WAIT";
    @tcp_states[8] = "CLOSE";
    @tcp_states[9] = "SYN_SENT2";
}

/* 
 * 挂载点说明:
 * 我们选择 nf_conntrack_event_report 附近的逻辑,
 * 或者直接追踪 tcp_packet 函数中状态赋值的时刻。
 * 这里以 kprobe 追踪 tcp_packet 内部状态变化为例。
 */
kprobe:tcp_packet
{
    $ct = (struct nf_conn *)arg1;
    $old_state = $ct->proto.tcp.state;
    
    // 我们记录下进入函数时的旧状态
    @temp_old_state[tid] = $old_state;
    @temp_ct[tid] = $ct;
}

kretprobe:tcp_packet
/@temp_ct[tid]/
{
    $ct = @temp_ct[tid];
    $old_state = @temp_old_state[tid];
    $new_state = $ct->proto.tcp.state;

    // 仅当状态发生改变时输出
    if ($old_state != $new_state) {
        $origin = $ct->tuplehash[0].tuple;
        $src = ntop($origin.src.u3.ip);
        $dst = ntop($origin.dst.u3.ip);
        $sport = bswap($origin.src.u.tcp.port);
        $dport = bswap($origin.dst.u.tcp.port);

        time("%H:%M:%S  ");
        printf("%s:%d -> %s:%d  %-15s -> %-15s\n", 
               $src, $sport, $dst, $dport, 
               @tcp_states[$old_state], @tcp_states[$new_state]);
    }

    delete(@temp_old_state[tid]);
    delete(@temp_ct[tid]);
}

END
{
    clear(@tcp_states);
    clear(@temp_old_state);
    clear(@temp_ct);
}

脚本解析

  1. 状态映射:内核中 state 存储的是整型枚举,通过 @tcp_states 字典将其转换为可读的字符串(如 ESTABLISHED)。
  2. kprobe & kretprobe 组合:由于我们需要对比“进入函数前”和“函数返回后”的状态,因此使用 tid(线程 ID)作为 Key,在 kprobe 中记录旧状态,在 kretprobe 中捕获更新后的新状态。
  3. 五元组提取:通过 struct nf_conn 结构体,访问 tuplehash[0].tuple 成员获取源/目的 IP 和端口。注意网络字节序转换使用 bswap
  4. 过滤逻辑if ($old_state != $new_state) 确保了我们只在迁移发生时才产生输出,极大减少了无效信息。

进阶:针对性监控

在生产环境中,全量监控可能导致大量的输出。你可以通过 bpftrace 的过滤器来锁定目标:

  • 按 IP 过滤
    if ($src == ntop(ip4("192.168.1.100"))) { ... }
    
  • 按特定迁移路径过滤
    如果你怀疑连接异常关闭,可以只看从 ESTABLISHEDCLOSE 的转换:
    if (@tcp_states[$old_state] == "ESTABLISHED" &amp;&amp; @tcp_states[$new_state] == "CLOSE") { ... }
    

注意事项

  1. 内核版本差异:不同版本的内核 struct nf_conn 的字段定义可能略有不同。如果报错,请使用 bpftrace -lv "struct nf_conn" 查看当前系统的结构体定义。
  2. 性能影响:虽然 eBPF 性能优秀,但在极高并发(每秒数十万次状态转换)的网关节点上,频繁的 printf 仍会消耗 CPU,建议在生产环境调试时配合过滤器使用。

总结

通过 bpftrace,我们不再是被动地接收 Netfilter 上报的事件,而是主动潜入内核观察连接状态机的每一个齿轮转动。这种实时、透明的可观测性,是构建高性能、高可用网络架构的必备利器。

内核探测员 bpftraceconntrack网络监控

评论点评