[ecapture]Connect Events获取

张开发
2026/4/17 8:39:13 15 分钟阅读

分享文章

[ecapture]Connect Events获取
说明使用caddygo程序 反向代理工具做测试示例分析ecaptureV1 conn_events下文中的用户态ecapture指的ecapture go用户态实现的代码(这里分析的是我们的小定制版)关键词ecaptureebpf1. 概述connect_events是用于捕获 TCP 连接建立和销毁事件的 eBPF map主要目的是关联 TLS 流量和网络连接信息。核心作用捕获连接的 pid, fd, sock, tuple (IP:Port) 信息为 TLS 事件提供连接上下文通过 pidfd 查找 tuple跟踪连接的生命周期建立和销毁2. Connect Events 整体架构图3. 主动连接 (connect) 时序图4. 被动连接 (accept) 时序图5. 连接销毁时序图6. 数据流向图7. 完整的调用链流程图8. bpftool日志更详细的日志9. 其他问题Caddy 返回的地址为什么有ffff::ffff:172.31.39.179::ffff:x.x.x.x是 IPv4-mapped IPv6 地址IPv4-mapped IPv6 address 的标准表示当应用程序监听或连接时使用了 dual-stack socket双栈 socket内核的 skc_family 就是 AF_INET6ipv4、ipv6统一但实际连接的是 IPv4 客户端。这时内核在 skc_v6_rcv_saddr / skc_v6_daddr 中存储的是 IPv4-mapped IPv6 地址格式为::ffff:x.x.x.xCaddyfile写成 :443{…} 不带网络类型前缀的地址Caddy 会默认使用tcp网络这通常会同时监听 IPv4 和 IPv6如果系统支持内核可以关闭IPv6可以判断saddr|daddr.Is4In6()进行修复10. Connect events源码解析10.1 数据结构10.1.1 eBPF 端C 结构体// ecapture/kern/openssl.h struct connect_event_t { unsigned __int128 saddr; // 源 IP 地址 (IPv4/IPv6) unsigned __int128 daddr; // 目标 IP 地址 char comm[TASK_COMM_LEN]; // 进程名 (16 bytes) u64 timestamp_ns; // 时间戳 u64 sock; // 内核 socket 指针 u32 pid; // 进程 ID u32 tid; // 线程 ID u32 fd; // 文件描述符 u16 family; // 地址族 (AF_INET/AF_INET6) u16 sport; // 源端口 u16 dport; // 目标端口 u8 is_destroy; // 是否为销毁事件 u8 pad[7]; // 对齐填充 } attribute((packed));10.1.2 用户态Go 结构体// ecapture用户侧/ebpf/event.go type connDataEvent struct { Saddr [16]byte // 源 IP Daddr [16]byte // 目标 IP Comm [16]byte // 进程名 TimestampNs uint64 // 时间戳 Sock uint64 // socket 指针 Pid uint32 // 进程 ID Tid uint32 // 线程 ID Fd uint32 // 文件描述符 Family uint16 // 地址族 Sport uint16 // 源端口 Dport uint16 // 目标端口 IsDestroy uint8 // 销毁标志 Pad [7]byte // 填充 } type ConnDataEvent struct { connDataEvent Tuple string // 格式化的连接信息 srcIP:srcPort-dstIP:dstPort }10.2 eBPF Hook 点10.2.1 连接建立 Hook主动连接 - connect10.2.1.1 Hook 1:__sys_connect(kprobe)SEC(kprobe/sys_connect) int probe_connect(struct pt_regs* ctx) { u64 pid_tgid bpf_get_current_pid_tgid(); struct tcp_fd_info fd_info {}; fd_info.fd PT_REGS_PARM1(ctx); // 获取 fd bpf_map_update_elem(tcp_fd_infos, pid_tgid, fd_info, BPF_ANY); return 0; }触发时机应用调用connect()系统调用时作用保存 fd 到临时 map参数int fd, struct sockaddr *addr, socklen_t len10.2.1.2 Hook 2:inet_stream_connect(kprobe)SEC(kprobe/inet_stream_connect) int probe_inet_stream_connect(struct pt_regs* ctx) { struct tcp_fd_info *fd_info find_fd_info(ctx); if (fd_info) { fd_info-sock (u64)(void *) PT_REGS_PARM1(ctx); // 获取 sock } return 0; }触发时机内核处理 TCP 连接时作用保存 socket 指针参数struct socket *sock, struct sockaddr *uaddr, int addr_len, int flags10.2.1.3 Hook 3:__sys_connect(kretprobe)SEC(kretprobe/sys_connect) int retprobe_connect(struct pt_regs* ctx) { struct tcp_fd_info *fd_info lookup_and_delete_fd_info(ctx); if (fd_info) { sock (typeof(sock)) fd_info-sock; bpf_probe_read_kernel(sk, sizeof(sk), sock-sk); if (sk) { return kretprobe_connect(ctx, fd_info-fd, sk, true); } } return 0; }触发时机connect()系统调用返回时作用提取完整连接信息并发送事件参数activetrue表示主动连接10.2.2 连接建立 Hook被动连接 - accept10.2.2.1 Hook 4:__sys_accept4(kprobe)SEC(kprobe/sys_connect) // 复用 probe_connect int probe_connect(struct pt_regs* ctx) { // 保存 fd }触发时机应用调用accept()时作用保存 fd10.2.2.2 Hook 5:inet_accept(kprobe)SEC(kprobe/inet_accept) int probe_inet_accept(struct pt_regs* ctx) { struct tcp_fd_info *fd_info find_fd_info(ctx); if (fd_info) { fd_info-sock (u64)(void *) PT_REGS_PARM2(ctx); // 获取 sock } return 0; }触发时机内核处理 accept 时作用保存 socket 指针10.2.2.3 Hook 6:__sys_accept4(kretprobe)SEC(kretprobe/__sys_accept4) int retprobe_accept4(struct pt_regs* ctx) { int fd PT_REGS_RC(ctx); // 获取返回的 fd if (fd 0) return 0; struct tcp_fd_info *fd_info lookup_and_delete_fd_info(ctx); if (fd_info) { sock (typeof(sock))(void *) fd_info-sock; bpf_probe_read_kernel(sk, sizeof(sk), sock-sk); if (sk) { return kretprobe_connect(ctx, fd, sk, false); // activefalse } } return 0; }触发时机accept()返回时作用提取连接信息并发送事件参数activefalse表示被动连接10.2.3 连接销毁 Hook10.2.3.1 Hook 7:tcp_v4_destroy_sock(kprobe)SEC(kprobe/tcp_v4_destroy_sock) int probe_tcp_v4_destroy_sock(struct pt_regs* ctx) { struct sock *sk (struct sock *)PT_REGS_PARM1(ctx); struct connect_event_t conn; __builtin_memset(conn, 0, sizeof(conn)); conn.sock (u64)sk; conn.is_destroy 1; // 标记为销毁事件 bpf_perf_event_output(ctx, connect_events, BPF_F_CURRENT_CPU, conn, sizeof(struct connect_event_t)); return BPF_OK; }触发时机IPv4 TCP socket 销毁时作用发送销毁事件只包含 sock 指针10.2.3.2 Hook 8:tcp_v6_destroy_sock(kprobe)SEC(kprobe/tcp_v6_destroy_sock) int probe_tcp_v6_destroy_sock(struct pt_regs* ctx) { // 同 tcp_v4_destroy_sock }触发时机IPv6 TCP socket 销毁时10.3 核心函数kretprobe_connectstatic __inline int kretprobe_connect(struct pt_regs *ctx, int fd, struct sock *sk, const bool active) { u64 current_pid_tgid bpf_get_current_pid_tgid(); u32 pid current_pid_tgid 32; u16 address_family 0; unsigned __int128 saddr, daddr; u32 ports; // 1. 读取地址族 bpf_probe_read_kernel(address_family, sizeof(address_family), sk-__sk_common.skc_family); // 2. 根据地址族读取 IP 地址 if (address_family AF_INET) { u64 addrs; bpf_probe_read_kernel(addrs, sizeof(addrs), sk-__sk_common.skc_addrpair); saddr (__be32)(addrs 32); daddr (__be32)addrs; } else if (address_family AF_INET6) { bpf_probe_read_kernel(saddr, sizeof(saddr), sk-__sk_common.skc_v6_rcv_saddr.in6_u.u6_addr32); bpf_probe_read_kernel(daddr, sizeof(daddr), sk-__sk_common.skc_v6_daddr.in6_u.u6_addr32); } // 3. 读取端口 bpf_probe_read_kernel(ports, sizeof(ports), sk-__sk_common.skc_portpair); // 4. 构造事件 struct connect_event_t conn; __builtin_memset(conn, 0, sizeof(conn)); conn.timestamp_ns bpf_ktime_get_ns(); conn.pid pid; conn.tid current_pid_tgid; conn.fd fd; conn.family address_family; conn.sock (u64)sk; // 5. 根据 active 标志设置源/目标 if (active) { // 主动连接 (connect) conn.dport bpf_ntohs((u16)ports); conn.sport ports 16; conn.saddr saddr; conn.daddr daddr; } else { // 被动连接 (accept) conn.sport bpf_ntohs((u16)ports); conn.dport ports 16; conn.saddr daddr; // 交换源/目标 conn.daddr saddr; } bpf_get_current_comm(conn.comm, sizeof(conn.comm)); // 6. 发送事件 bpf_perf_event_output(ctx, connect_events, BPF_F_CURRENT_CPU, conn, sizeof(struct connect_event_t)); return 0; }10.4 完整调用链10.4.1 主动连接Client 发起 connect应用程序 ↓ connect(fd, addr, len) ↓ [kprobe] __sys_connect → probe_connect: 保存 fd ↓ [kprobe] inet_stream_connect → probe_inet_stream_connect: 保存 sock ↓ 内核建立连接 ↓ [kretprobe] __sys_connect → retprobe_connect: - 从 tcp_fd_infos 获取 fd sock - 调用 kretprobe_connect(activetrue) - 从 sk 提取 IP、端口 - 发送 connect_event (is_destroy0) ↓ 用户态接收事件 → parseEbpfData(connect_events) → addConn: 存储 pidfd - ConnInfo{tuple, sock}10.4.2 被动连接Server 接受 accept应用程序 ↓ accept(listen_fd, addr, len) ↓ [kprobe] __sys_accept4 → probe_connect: 保存 fd (复用) ↓ [kprobe] inet_accept → probe_inet_accept: 保存 sock ↓ 内核接受连接 ↓ [kretprobe] __sys_accept4 → retprobe_accept4: - 获取返回的 new_fd - 从 tcp_fd_infos 获取 sock - 调用 kretprobe_connect(activefalse) - 发送 connect_event (is_destroy0) ↓ 用户态接收事件 → parseEbpfData(connect_events) → addConn: 存储 pidfd - ConnInfo{tuple, sock}10.4.3 连接销毁内核关闭 socket ↓ [kprobe] tcp_v4_destroy_sock / tcp_v6_destroy_sock → probe_tcp_v4_destroy_sock: - 只发送 sock 和 is_destroy1 ↓ 用户态接收事件 → parseEbpfData(connect_events) → destroyConn: - 通过 sock 查找 pidfd - 删除 pidConns[pid][fd] - 删除 sock2pidFd[sock]10.5 关键设计点10.5.1 为什么需要临时 map (tcp_fd_infos)?因为 kprobe 和 kretprobe 是分开触发的kprobe时有 fd但还没有完整的 socket 信息kretprobe时连接已建立有完整信息但需要之前保存的 fd所以使用tcp_fd_infos临时存储pid_tgid - {fd, sock}10.5.2 active 参数的作用if (active) { // connect: 本地 - 远程 conn.sport ports 16; conn.dport bpf_ntohs((u16)ports); conn.saddr saddr; conn.daddr daddr; } else { // accept: 远程 - 本地 conn.sport bpf_ntohs((u16)ports); conn.dport ports 16; conn.saddr daddr; // 交换 conn.daddr saddr; }activetrue(connect): 本地是源远程是目标activefalse(accept): 远程是源本地是目标10.5.3 为什么用 sock 作为销毁事件的 key?因为销毁时没有 fdfd 可能已经被关闭没有 pid进程可能已经退出只有 sock内核 socket 结构体指针是唯一标识所以维护sock2pidFd映射来反向查找。10.6 总结10.6.1 核心流程eBPF Hook 捕获连接事件├─ connect: kprobe kretprobe ├─ accept: kprobe kretprobe └─ destroy: kprobe提取连接信息├─ pid, tid, fd ├─ sock (内核指针) └─ tuple (IP:Port)用户态存储映射├─ pidConns: pidfd - ConnInfo └─ sock2pidFd: sock - [pid, fd]TLS 事件关联├─ 通过 pidfd 查找 ConnInfo └─ 获取 tuple 和 sock10.6.2 关键价值connect_events 是 TLS 流量分析的基础设施它解决了一个核心问题TLS 加密流量只有 pid 和 fd如何知道它对应的网络连接信息IP、端口通过 connect_events我们可以✅ 知道每个 TLS 连接的源/目标 IP 和端口✅ 跟踪连接的完整生命周期✅ 关联同一 socket 上的所有 TLS 数据包✅ 支持流量分析、审计、监控等场景11. ecapture实现相关文章文章都在ecapture专栏里[eCapture] GoTLS Perf 事件有序下发[ecapture]捕获TLS明文流量[ecapture]Connect Events获取[ecapture]go1.20 tls fd抽取[ecapture] eBPF hook gotls 收包乱序根因分析[ecapture] gotls三种模式实现说明与上层应用职责

更多文章