能否用BCC/eBPF追踪TCP数据包而非TCP连接?
可以用BCC/eBPF追踪TCP数据包(无需XDP)
完全可行,不需要依赖XDP技术。eBPF可以通过挂载到TCP协议栈的关键处理节点,直接触及单个TCP数据包的发送/接收流程,而非仅追踪连接生命周期事件。以下是具体实现方式:
核心实现思路
避开XDP的话,优先选择kprobe/tracepoint(内核函数钩子)或TC(Traffic Control)clsact qdisc这两类挂载点,它们都能在TCP数据包的处理路径上插入eBPF程序,提取数据包信息。
具体实现方案
方案1:基于Kprobe/Tracepoint的用户态追踪
Kprobe可以挂钩内核中处理TCP数据包的核心函数,比如tcp_transmit_skb(发送数据包)、tcp_v4_rcv/tcp_v6_rcv(接收数据包);如果内核版本支持,更推荐用稳定的tracepoint(如tcp:tcp_send_skb、tcp:tcp_recv_skb),避免内核版本变动导致的兼容性问题。
下面是一个用BCC框架实现的示例,追踪所有发送的TCP数据包:
eBPF程序(C部分)
#include <uapi/linux/ptrace.h> #include <net/sock.h> #include <bcc/proto.h> #include <linux/tcp.h> // 定义传递到用户态的事件结构体 struct tcp_packet_event { u32 pid; u32 src_ip; u32 dst_ip; u16 src_port; u16 dst_port; u32 packet_len; u8 tcp_flags; }; // 用于向用户态传递事件的perf buffer BPF_PERF_OUTPUT(packet_events); // Kprobe挂钩tcp_transmit_skb函数,捕获发送的TCP数据包 int kprobe__tcp_transmit_skb(struct pt_regs *ctx, struct sock *sk, struct sk_buff *skb) { struct tcp_packet_event event = {}; struct tcp_sock *tcp_sk_ptr = tcp_sk(sk); struct tcphdr *tcp_hdr = (struct tcphdr *)skb_transport_header(skb); // 提取进程PID event.pid = bpf_get_current_pid_tgid() >> 32; // 提取IP地址和端口(IPv4示例,IPv6需调整) event.src_ip = sk->__sk_common.skc_rcv_saddr; event.dst_ip = sk->__sk_common.skc_daddr; event.src_port = ntohs(sk->__sk_common.skc_num); event.dst_port = ntohs(sk->__sk_common.skc_dport); // 提取数据包长度和TCP flags event.packet_len = skb->len; event.tcp_flags = tcp_hdr->flags; // 将事件发送到用户态 packet_events.perf_submit(ctx, &event, sizeof(event)); return 0; }
用户态加载程序(Python部分)
from bcc import BPF import struct from socket import inet_ntoa # 加载上面的eBPF C代码 bpf_program = BPF(text=""" // 这里粘贴上面的C代码 """) # 定义事件处理函数,格式化输出数据包信息 def handle_packet_event(cpu, data, size): event = bpf_program["packet_events"].event(data) src_ip = inet_ntoa(struct.pack("<I", event.src_ip)) dst_ip = inet_ntoa(struct.pack("<I", event.dst_ip)) tcp_flags = f"0x{event.tcp_flags:02x}" print(f"PID: {event.pid:5d} | Src: {src_ip}:{event.src_port:5d} | Dst: {dst_ip}:{event.dst_port:5d} | Len: {event.packet_len:4d} | Flags: {tcp_flags}") # 关联perf buffer和处理函数 bpf_program["packet_events"].open_perf_buffer(handle_packet_event) # 循环监听事件 print("Tracking TCP packets (send direction)...") print("PID | Src | Dst | Len | Flags") print("-------------------------------------------------------") while True: try: bpf_program.perf_buffer_poll() except KeyboardInterrupt: exit()
运行与验证
- 保存代码为
tcp_packet_trace.py - 以root权限运行:
sudo python3 tcp_packet_trace.py - 此时系统中所有TCP发送数据包的信息会实时输出,包括所属进程、源/目的地址端口、长度和TCP标志位。
方案2:基于TC BPF的内核态追踪
如果需要更底层的数据包捕获(不依赖进程上下文),可以用TC clsact qdisc挂载eBPF程序,在网卡的ingress/egress路径上捕获TCP数据包。
步骤1:配置TC clsact qdisc
# 为目标网卡添加clsact队列规则(以eth0为例) sudo tc qdisc add dev eth0 clsact
步骤2:编写TC eBPF程序(C)
#include <uapi/linux/bpf.h> #include <uapi/linux/if_ether.h> #include <uapi/linux/ip.h> #include <uapi/linux/tcp.h> // 在ingress路径捕获TCP数据包 SEC("ingress") int tc_tcp_ingress(struct __sk_buff *skb) { // 解析以太网头部 struct ethhdr *eth_hdr = bpf_hdr_pointer(skb); if (eth_hdr->h_proto != htons(ETH_P_IP)) { return TC_ACT_OK; // 跳过非IP数据包 } // 解析IP头部 struct iphdr *ip_hdr = (struct iphdr *)(eth_hdr + 1); if (ip_hdr->protocol != IPPROTO_TCP) { return TC_ACT_OK; // 跳过非TCP数据包 } // 解析TCP头部 struct tcphdr *tcp_hdr = (struct tcphdr *)(ip_hdr + 1); u32 src_ip = ip_hdr->saddr; u32 dst_ip = ip_hdr->daddr; u16 src_port = ntohs(tcp_hdr->source); u16 dst_port = ntohs(tcp_hdr->dest); // 打印数据包信息到内核trace buffer bpf_printk("Ingress TCP: %x:%d -> %x:%d, len: %d", src_ip, src_port, dst_ip, dst_port, skb->len); return TC_ACT_OK; // 放行数据包 } // 可选:在egress路径捕获 SEC("egress") int tc_tcp_egress(struct __sk_buff *skb) { // 逻辑同ingress,略 return TC_ACT_OK; } char _license[] SEC("license") = "GPL";
步骤3:编译与挂载
# 用clang编译eBPF程序 clang -target bpf -O2 -c tc_tcp_trace.c -o tc_tcp_trace.o # 将程序挂载到网卡ingress hook sudo tc filter add dev eth0 ingress bpf da obj tc_tcp_trace.o sec ingress # 查看输出(内核trace buffer) sudo cat /sys/kernel/debug/tracing/trace_pipe
清理资源
sudo tc filter del dev eth0 ingress sudo tc qdisc del dev eth0 clsact
注意事项
- Kprobe依赖内核函数签名,不同内核版本可能存在参数差异,建议优先使用tracepoint(如
tcp:tcp_send_skb)以提升兼容性。 - 所有操作需要root权限,因为eBPF程序需要访问内核态资源。
- 如果需要过滤特定数据包(如特定端口、IP),可以在eBPF程序中添加条件判断,减少不必要的事件输出。
内容的提问来源于stack exchange,提问作者Afonso Pinto
相关产品推荐
相关产品推荐

