应使用 tracepoint tcp:tcp_retransmit_skb 而非 socket_filter 捕获 TCP 重传,因其在内核发送路径触发;需通过 CO-RE 安全读取 sock 结构体四元组,并在用户态用 perf_buffer 二次过滤。

用 bpf_program__attach_socket_filter 捕获 TCP 重传不现实
直接在 socket 上 attach 过滤程序,无法拿到 TCP 重传事件。eBPF 的 socket_filter 类型只作用于入站数据包(recv path),且仅能看到已交付给 socket 的数据帧,而重传是内核协议栈在 tcp_transmit_skb 或 tcp_retransmit_skb 中触发的发送行为,不在 socket filter 覆盖范围。
真正能捕获重传的入口是 tracepoint:tcp:tcp_retransmit_skb。它在每次调用 tcp_retransmit_skb 时触发,参数包含 sk(socket 结构指针)、skb(待重传 skb)和重传原因(如 tcp_rtx_queue_head、tcp_send_loss_probe 等)。
实操建议:
- 必须使用
tracepoint类型程序,而非socket_filter或sk_skb - 目标内核需 ≥ 5.10(该 tracepoint 自 5.2 引入,但稳定字段布局从 5.10 开始)
- 需通过
bpf_probe_read_kernel安全读取struct sock *中的字段(如sk_saddr、sk_daddr、sk_num、sk_dport),不能直接解引用 - 若要过滤“特定 Socket”,应在 eBPF 程序中比对四元组(saddr, daddr, sport, dport),而非靠用户态预 attach
如何在 eBPF 中安全提取重传 socket 的四元组
tracepoint tcp:tcp_retransmit_skb 的上下文结构体(struct trace_event_raw_tcp_retransmit_skb)里只有 sk 指针,没有现成的端口/地址字段。必须手动从 struct sock * 中读取,且字段偏移随内核版本变化——不能硬编码。
立即学习“C++免费学习笔记(深入)”;
推荐做法是用 libbpf 的 CO-RE(Compile Once – Run Everywhere)机制,配合 bpf_core_read:
struct sock *sk = ctx->sk; __u32 saddr = 0, daddr = 0; __u16 sport = 0, dport = 0; bpf_core_read(&saddr, sizeof(saddr), &sk->__sk_common.skc_rcv_saddr); bpf_core_read(&daddr, sizeof(daddr), &sk->__sk_common.skc_daddr); bpf_core_read(&sport, sizeof(sport), &sk->__sk_common.skc_num); bpf_core_read(&dport, sizeof(dport), &sk->__sk_common.skc_dport);
注意:
-
skc_num是本地端口(host byte order),skc_dport是网络字节序,需用bpf_ntohs转换 - IPv6 场景需额外判断
sk->__sk_common.skc_family == AF_INET6,并读取skc_v6_saddr/skc_v6_daddr - 未完成三次握手的 socket(SYN_RECV)可能部分字段为 0,需加空指针检查(
if (!sk) return 0;)
用户态如何按四元组过滤并避免高频事件淹没
eBPF 程序本身可以做粗粒度过滤(比如只放行目标 dport == 8080),但精细控制(如只关心某个 client IP + port 组合)建议放在用户态:把重传事件通过 perf_buffer 推出,由用户程序做二次匹配。
关键点:
- 不要在 eBPF 中做字符串比较或复杂哈希——性能差且易超指令数限制
- perf event 格式必须紧凑:建议只传
timestamp_ns、saddr、daddr、sport、dport、reason(来自 tracepoint 的reason字段)共 6 个字段 - 设置 perf buffer ring size ≥ 4M,否则高重传率下丢事件(
libbpf_print_fn可捕获PERF_EVENT_LOST) - 用户态用
perf_buffer__poll()非阻塞轮询,避免因单次处理慢导致 backlog
常见失败现象与绕过方式
运行时报错 invalid indirect read from stack 或 unknown btf id:大概率是未启用 BTF 或 CO-RE 编译失败。
现象与对策:
- 加载失败提示
tracepoint 'tcp:tcp_retransmit_skb' not found→ 检查/sys/kernel/debug/tracing/events/tcp/tcp_retransmit_skb/enable是否可写,确认内核编译开启了CONFIG_TRACEPOINTS=y和CONFIG_TCP_CONG_ADVANCED=y - 读出的端口全为 0 → 忘记用
bpf_core_read,或字段名在当前内核 BTF 中不存在(可用pahole -C sock /usr/lib/debug/lib/modules/*/vmlinux验证) - 事件数量远少于
netstat -s | grep retrans统计值 → tracepoint 只捕获重传 skb 的首次触发,不包括 fast recovery 中的重复 ACK 触发的重传;如需完整覆盖,需同时监听tcp:tcp_retransmit_skb和tcp:tcp_retransmit(后者更轻量,但字段更少)
CO-RE 不是银弹:如果目标机器没装 vmlinux.h 或 BTF 信息被 strip,bpf_core_read 会退化为常量偏移读取,此时必须确保构建环境和运行环境内核版本一致,或手动维护字段偏移映射表。


















