基于 TC 的网络数据包捕获
目的
本页面记录 ecapture 中基于流量控制(TC)分类器的数据包捕获机制。TC 分类器在数据链路层(第 2 层)运行,拦截进入(ingress)或离开(egress)网络接口的数据包。此功能对于 OpenSSL/BoringSSL 模块的 PCAP 模式至关重要,该模式重构完整的网络数据包并将其与应用层 TLS 密钥关联。
有关 TLS 密钥提取和 keylog 生成的信息,请参阅主密钥提取。有关 PCAP 输出格式和 Wireshark 集成的详细信息,请参阅PCAP 集成。有关 OpenSSL 模块的整体架构,请参阅OpenSSL 模块。
架构概述
TC 数据包捕获系统由三个协作组件组成:
- TC 分类器 - 附加到网络接口的 eBPF 程序,检查每个数据包
- 内核探针 - 对
tcp_sendmsg和udp_sendmsg的 kprobe,将数据包与进程关联 - 网络映射 - 一个 LRU 哈希映射,存储网络连接与进程元数据(PID、UID、comm)之间的映射
系统流程图
来源: kern/tc.h:1-384, user/module/probe_openssl.go:137-148
eBPF 数据结构
连接标识:net_id_t
net_id_t 结构使用 IPv4 或 IPv6 的五元组唯一标识网络连接:
| 字段 | 类型 | 描述 |
|---|---|---|
protocol | u32 | 协议号 (IPPROTO_TCP=6, IPPROTO_UDP=17, IPPROTO_ICMP=1, IPPROTO_ICMPV6=58) |
src_port | u32 | 源端口号 |
dst_port | u32 | 目标端口号 |
src_ip4 | u32 | 源 IPv4 地址(用于 IPv4 连接) |
dst_ip4 | u32 | 目标 IPv4 地址(用于 IPv4 连接) |
src_ip6 | u32[4] | 源 IPv6 地址(用于 IPv6 连接) |
dst_ip6 | u32[4] | 目标 IPv6 地址(用于 IPv6 连接) |
来源: kern/tc.h:39-47
进程上下文:net_ctx_t
net_ctx_t 结构存储与连接关联的进程元数据:
| 字段 | 类型 | 描述 |
|---|---|---|
pid | u32 | 进程 ID |
uid | u32 | 用户 ID |
comm | char[TASK_COMM_LEN] | 进程命令名称(16 字节) |
来源: kern/tc.h:49-54
数据包事件:skb_data_event_t
skb_data_event_t 结构表示发送到用户空间的捕获数据包事件:
| 字段 | 类型 | 描述 |
|---|---|---|
ts | uint64_t | 时间戳(自启动以来的纳秒数) |
pid | u32 | 进程 ID(来自 network_map 查找) |
comm | char[TASK_COMM_LEN] | 进程命令名称 |
len | u32 | 数据包总长度 |
ifindex | u32 | 网络接口索引 |
来源: kern/tc.h:30-37
eBPF 映射
network_map
network_map 是将网络数据包与进程元数据关联的核心数据结构。
struct {
__uint(type, BPF_MAP_TYPE_LRU_HASH);
__type(key, struct net_id_t);
__type(value, struct net_ctx_t);
__uint(max_entries, 10240);
} network_map SEC(".maps");类型: BPF_MAP_TYPE_LRU_HASH - 在满时自动驱逐最近最少使用的条目
容量: 10,240 个连接
生命周期:
- 由
tcp_sendmsg和udp_sendmsgkprobe 在进程发起连接时填充 - 由 TC 分类器查询以将数据包与进程关联
- 条目通过 LRU 驱逐自动过期
来源: kern/tc.h:72-77
skb_events
skb_events 映射是用于将数据包数据发送到用户空间的 perf 事件数组:
struct {
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
__uint(key_size, sizeof(u32));
__uint(value_size, sizeof(u32));
__uint(max_entries, 10240);
} skb_events SEC(".maps");来源: kern/tc.h:57-62
skb_data_buffer_heap
一个每 CPU 数组映射,用于临时存储事件结构以避免栈限制:
struct {
__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
__type(key, u32);
__type(value, struct skb_data_event_t);
__uint(max_entries, 1);
} skb_data_buffer_heap SEC(".maps");来源: kern/tc.h:64-69
内核探针钩子
tcp_sendmsg Kprobe
tcp_sendmsg kprobe 拦截 TCP socket 发送操作以填充 network_map:
关键操作:
- 从第一个参数提取
struct sock *(kern/tc.h:290) - 读取 socket 族字段以确定 IPv4 或 IPv6 (kern/tc.h:297)
- 从
sock->__sk_common提取本地/远程 IP 地址和端口 (kern/tc.h:299-323) - 构建
net_id_t,协议设置为IPPROTO_TCP(kern/tc.h:306-322) - 使用当前进程的 PID、UID 和 comm 填充
net_ctx_t(kern/tc.h:325-328) - 更新
network_map的连接元组 → 进程映射 (kern/tc.h:331)
udp_sendmsg Kprobe
udp_sendmsg kprobe 与 tcp_sendmsg 的操作相同,但用于 UDP 连接,在 net_id_t 结构中设置 protocol = IPPROTO_UDP。
TC 分类器实现
附加点
定义了两个 TC 分类器程序,附加到出口和入口挂钩点:
SEC("classifier")
int egress_cls_func(struct __sk_buff *skb) {
return capture_packets(skb, false);
}
SEC("classifier")
int ingress_cls_func(struct __sk_buff *skb) {
return capture_packets(skb, true);
}出口(Egress): 离开主机的数据包(出站流量)
入口(Ingress): 进入主机的数据包(入站流量)
两个分类器都调用共同的 capture_packets 函数,返回 TC_ACT_OK 以允许正常数据包处理。
capture_packets 函数
capture_packets 函数执行核心数据包处理逻辑:
数据包过滤与验证
长度验证
在处理之前,函数验证数据包至少包含以太网头和 IP 头:
if (data_start + sizeof(struct ethhdr) + sizeof(struct iphdr) > data_end) {
return TC_ACT_OK;
}PCAP 过滤器集成
当通过 --pcapfilter 标志指定 pcap 过滤器时,filter_pcap_l2 函数应用编译的过滤器表达式:
if (!filter_pcap_l2(skb, data_start, data_end))
return TC_ACT_OK;该函数标记为 __noinline 以允许用户空间加载器进行指令修补。指令修补器将 libpcap 过滤器表达式(例如 tcp port 443)转换为注入此函数的 eBPF 指令。
来源: kern/tc.h:121-132, kern/tc.h:147-149, user/module/probe_openssl.go:302-307
协议解析
IPv4 数据包处理
对于 IPv4 数据包(eth->h_proto == ETH_P_IP):
- 在偏移量
sizeof(struct ethhdr)处解析struct iphdr(kern/tc.h:205) - 过滤非 TCP/UDP/ICMP 协议 (kern/tc.h:207-209)
- 提取源/目标 IP 地址:
iph->saddr,iph->daddr(kern/tc.h:211-213) - 重新验证缓冲区以访问 L4 头 (kern/tc.h:214-217)
- 解析 TCP/UDP 头(两者都使用前 4 字节作为端口) (kern/tc.h:220)
- 提取源/目标端口:
bpf_ntohs(hdr->source),bpf_ntohs(hdr->dest)(kern/tc.h:221-222)
IPv6 数据包处理
对于 IPv6 数据包(eth->h_proto == ETH_P_IPV6):
- 在偏移量
sizeof(struct ethhdr)处解析struct ipv6hdr(kern/tc.h:162) - 通过
iph->nexthdr过滤非 TCP/UDP/ICMPv6 协议 (kern/tc.h:163-165) - 提取 128 位源/目标地址:
iph->saddr,iph->daddr(kern/tc.h:168-169) - 与 IPv4 相同地处理 L4 头 (kern/tc.h:171-186)
网络映射查找
在构建 net_id_t 连接元组后,函数执行双向查找:
net_ctx = bpf_map_lookup_elem(&network_map, &conn_id);
if (net_ctx == NULL) {
// 交换 src/dst 并尝试反向
u32 tmp_ip = conn_id.src_ip4;
conn_id.src_ip4 = conn_id.dst_ip4;
conn_id.dst_ip4 = tmp_ip;
u32 tmp_port = conn_id.src_port;
conn_id.src_port = conn_id.dst_port;
conn_id.dst_port = tmp_port;
net_ctx = bpf_map_lookup_elem(&network_map, &conn_id);
}这种双向查找处理同一连接的出口和入口数据包。当进程通过 tcp_sendmsg 发起连接时,network_map 被填充出口元组。对于响应数据包(入口方向),源和目标被交换,因此代码尝试两个方向。
来源: kern/tc.h:224-234 (IPv4), kern/tc.h:187-197 (IPv6)
进程过滤
在内核 >= 5.2 上,可以设置全局变量 target_pid 和 target_uid 以按进程过滤数据包:
if (net_ctx != NULL) {
// pid uid 过滤
if (filter_rejects(net_ctx->pid, net_ctx->uid)) {
return TC_ACT_OK;
}
}filter_rejects 函数检查是否应该根据 PID/UID 过滤数据包。这些全局变量通过用户空间模块初始化中的常量编辑器设置。
来源: kern/tc.h:240-244, kern/ecapture.h:93-105, kern/common.h:68-70
事件提交
捕获的数据包元数据和原始数据包数据通过 bpf_perf_event_output 发送到用户空间:
event.ts = bpf_ktime_get_ns();
event.len = skb->len;
event.ifindex = skb->ifindex;
u64 flags = BPF_F_CURRENT_CPU;
flags |= (u64)skb->len << 32;
size_t pkt_size = TC_PACKET_MIN_SIZE;
bpf_perf_event_output(skb, &skb_events, flags, &event, pkt_size);flags 参数在高 32 位中编码 CPU 号和总数据包长度。实际数据包数据由内核的 perf 事件子系统从 skb 结构中读取。只有最小的元数据(TC_PACKET_MIN_SIZE = 36 字节)从事件结构复制。
来源: kern/tc.h:251-266, kern/tc.h:20
用户空间集成
模块配置
OpenSSL 模块在配置为 PCAP 模式时启用 TC 数据包捕获:
case config.TlsCaptureModelPcap, config.TlsCaptureModelPcapng:
pcapFile := m.conf.(*config.OpensslConfig).PcapFile
m.eBPFProgramType = TlsCaptureModelTypePcap
var fileInfo string
fileInfo, err = filepath.Abs(pcapFile)
if err != nil {
return err
}
m.tcPacketsChan = make(chan *TcPacket, 2048)
m.tcPackets = make([]*TcPacket, 0, 256)
m.pcapngFilename = fileInfo来源: user/module/probe_openssl.go:137-148
管理器设置
setupManagersPcap 函数配置 eBPF 管理器以进行 TC 和 kprobe 附加:
func (m *MOpenSSLProbe) setupManagersPcap() error {
var err error
m.bpfManager = &manager.Manager{
Probes: []*manager.Probe{
// SSL 函数 uprobes...
{
Section: "kprobe/tcp_sendmsg",
EbpfFuncName: "kprobe_tcp_sendmsg",
AttachToFuncName: "tcp_sendmsg",
},
{
Section: "kprobe/udp_sendmsg",
EbpfFuncName: "kprobe_udp_sendmsg",
AttachToFuncName: "udp_sendmsg",
},
},
}
// ... TC 分类器设置
}关键组件:
- Kprobes: 附加到
tcp_sendmsg和udp_sendmsg内核函数 - TC 分类器: 附加到网络接口(在 TCObjects 中单独配置)
- 常量编辑器: 设置
target_pid和target_uid全局变量
来源: user/module/probe_openssl.go:280-350
PCAP 过滤器编译
当指定 --pcapfilter 时,指令修补器编译过滤器表达式:
pcapFilter := m.conf.(*config.OpensslConfig).PcapFilter
if m.eBPFProgramType == TlsCaptureModelTypePcap && pcapFilter != "" {
ebpfFuncs := []string{tcFuncNameIngress, tcFuncNameEgress}
m.bpfManager.InstructionPatchers = prepareInsnPatchers(m.bpfManager,
ebpfFuncs, pcapFilter)
}prepareInsnPatchers 函数:
- 解析 pcap 过滤器表达式(例如
tcp port 443 and host 1.2.3.4) - 使用 libpcap 将其编译为 BPF 指令
- 在
ingress_cls_func和egress_cls_func中修补filter_pcap_ebpf_l2函数
来源: user/module/probe_openssl.go:302-307
事件处理
TC 数据包事件从 perf 事件数组读取并解码为 TcSkbEvent 结构:
case *event.TcSkbEvent:
err := m.dumpTcSkb(ev)
if err != nil {
m.logger.Error().Err(err).Msg("save packet error.")
}dumpTcSkb 函数:
- 接收带有数据包元数据和原始字节的
TcSkbEvent - 在
m.tcPackets切片中缓冲数据包 - 定期刷新到带有进程元数据的 PCAPNG 文件
来源: user/module/probe_openssl.go:746-750
支持的协议
第 3 层(网络层)
| 协议 | 常量 | 值 | 支持 |
|---|---|---|---|
| IPv4 | ETH_P_IP | 0x0800 | 完全支持 |
| IPv6 | ETH_P_IPV6 | 0x86DD | 完全支持 |
第 4 层(传输层)
| 协议 | 常量 | 值 | Kprobe 钩子 | TC 支持 |
|---|---|---|---|---|
| TCP | IPPROTO_TCP | 6 | tcp_sendmsg | 完全支持 |
| UDP | IPPROTO_UDP | 17 | udp_sendmsg | 完全支持 |
| ICMP | IPPROTO_ICMP | 1 | 无 | 受限* |
| ICMPv6 | IPPROTO_ICMPV6 | 58 | 无 | 受限* |
*ICMP/ICMPv6 数据包由 TC 分类器捕获,但无法与进程关联(无 kprobe 钩子),因此事件中 pid=0。
来源: kern/tc.h:163, kern/tc.h:207, kern/tc.h:285-383
内核版本要求
全局变量(target_pid, target_uid)
通过 target_pid 和 target_uid 进行进程过滤需要内核 >= 5.2 以支持 .rodata 部分:
#ifndef KERNEL_LESS_5_2
const volatile u64 target_pid = 0;
const volatile u64 target_uid = 0;
#endif在内核 < 5.2 上,这些过滤器被禁用,捕获所有匹配 pcap 过滤器的数据包。
来源: kern/common.h:64-71, user/module/imodule.go:145-148
BTF 和 CO-RE
TC eBPF 程序支持 CO-RE 和非 CO-RE 模式:
- CO-RE 模式: 使用
vmlinux.h和内核 BTF 进行自动结构重定位 - 非 CO-RE 模式: 使用特定内核版本的内核头文件
模式根据 BTF 可用性自动选择,或通过 -b/--btf 标志选择。
限制与注意事项
连接跟踪间隙
network_map 仅在进程调用 tcp_sendmsg 或 udp_sendmsg 时填充。这会为以下情况创建间隙:
- 入站发起的连接: 服务器进程接受连接可能直到发送数据时才出现在映射中
- 预先存在的连接: ecapture 启动前建立的连接不被跟踪
- 内核发起的流量: 内核直接发送的没有进程上下文的数据包
对于这些情况,TC 分类器捕获的数据包 pid=0。
LRU 映射驱逐
network_map 的最大大小为 10,240 个条目。在高连接流失下,旧条目可能在其数据包被捕获之前被驱逐。LRU(最近最少使用)驱逐策略优先考虑活动连接。
来源: kern/tc.h:76
数据包大小
支持的最大数据包大小为 SKB_MAX_DATA_SIZE = 2048 字节。较大的数据包被截断。实际数据包长度保留在 skb_data_event_t.len 中,但只捕获 2048 字节。
来源: kern/common.h:61
性能影响
TC 分类器在遍历网络接口的每个数据包上执行,包括与监控进程无关的流量。性能影响与总网络吞吐量成比例,而不仅仅是监控的应用流量。使用 pcap 过滤器来减少开销。
与 OpenSSL 模块的集成
TC 数据包捕获与 TLS 密钥提取集成以实现完整的明文恢复:
组合输出包括:
- TC 捕获的带有进程元数据(PID、comm)的网络数据包
- 从 SSL 库钩子提取的 TLS 主密钥
- 嵌入密钥的 PCAPNG DSB 块,用于在 Wireshark 中自动解密
有关 DSB 生成的详细信息,请参阅PCAP 集成。
来源: user/module/probe_openssl.go:558-565
使用示例
基本 PCAP 捕获
将所有 TLS 流量捕获到 pcapng 文件:
ecapture tls --pcapng=/tmp/traffic.pcapng过滤捕获
仅捕获特定进程的 HTTPS 流量(端口 443):
ecapture tls --pcapng=/tmp/https.pcapng --pcapfilter="tcp port 443" --pid=12345IPv6 捕获
捕获 IPv6 TLS 流量:
ecapture tls --pcapng=/tmp/ipv6.pcapng --pcapfilter="ip6"使用 Wireshark 分析
生成的 PCAPNG 文件可以直接在 Wireshark 中打开,Wireshark 将自动使用嵌入的 DSB 密钥解密 TLS 流量:
wireshark /tmp/traffic.pcapng