当前位置: 首页 > news >正文

[ecapture]Connect Events获取

说明:

  • 使用caddy(go程序 反向代理工具)做测试示例,分析ecaptureV1 conn_events;
  • 下文中的用户态(ecapture)指的ecapture go用户态实现的代码(这里分析的是我们的小定制版)

关键词:ecaptureebpf

1. 概述

connect_events是用于捕获 TCP 连接建立和销毁事件的 eBPF map,主要目的是关联 TLS 流量和网络连接信息。

核心作用

  • 捕获连接的 pid, fd, sock, tuple (IP:Port) 信息
  • 为 TLS 事件提供连接上下文(通过 pid+fd 查找 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_INET6(ipv4、ipv6统一),但实际连接的是 IPv4 客户端。这时内核在 skc_v6_rcv_saddr / skc_v6_daddr 中存储的是 IPv4-mapped IPv6 地址,格式为:::ffff:x.x.x.x
Caddyfile写成 :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(主动连接 - connect)
10.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 len
10.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 flags
10.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()系统调用返回时
  • 作用:提取完整连接信息并发送事件
  • 参数active=true表示主动连接
10.2.2 连接建立 Hook(被动连接 - accept)
10.2.2.1 Hook 4:__sys_accept4(kprobe)
SEC("kprobe/sys_connect") // 复用 probe_connect int probe_connect(struct pt_regs* ctx) { // 保存 fd }
  • 触发时机:应用调用accept()
  • 作用:保存 fd
10.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); // active=false } } return 0; }
  • 触发时机accept()返回时
  • 作用:提取连接信息并发送事件
  • 参数active=false表示被动连接
10.2.3 连接销毁 Hook
10.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_connect

static __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(active=true) - 从 sk 提取 IP、端口 - 发送 connect_event (is_destroy=0) ↓ 用户态接收事件 → parseEbpfData("connect_events") → addConn: 存储 pid+fd -> 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(active=false) - 发送 connect_event (is_destroy=0) ↓ 用户态接收事件 → parseEbpfData("connect_events") → addConn: 存储 pid+fd -> ConnInfo{tuple, sock}
10.4.3 连接销毁
内核关闭 socket ↓ [kprobe] tcp_v4_destroy_sock / tcp_v6_destroy_sock → probe_tcp_v4_destroy_sock: - 只发送 sock 和 is_destroy=1 ↓ 用户态接收事件 → parseEbpfData("connect_events") → destroyConn: - 通过 sock 查找 pid+fd - 删除 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; }
  • active=true(connect): 本地是源,远程是目标
  • active=false(accept): 远程是源,本地是目标
10.5.3 为什么用 sock 作为销毁事件的 key?

因为销毁时:

  • 没有 fd:fd 可能已经被关闭
  • 没有 pid:进程可能已经退出
  • 只有 sock:内核 socket 结构体指针是唯一标识

所以维护sock2pidFd映射来反向查找。

10.6 总结

10.6.1 核心流程
  1. eBPF Hook 捕获连接事件
├─ connect: kprobe + kretprobe ├─ accept: kprobe + kretprobe └─ destroy: kprobe
  1. 提取连接信息
├─ pid, tid, fd ├─ sock (内核指针) └─ tuple (IP:Port)
  1. 用户态存储映射
├─ pidConns: pid+fd -> ConnInfo └─ sock2pidFd: sock -> [pid, fd]
  1. TLS 事件关联
├─ 通过 pid+fd 查找 ConnInfo └─ 获取 tuple 和 sock
10.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:三种模式实现说明与上层应用职责

http://www.jsqmd.com/news/654566/

相关文章:

  • 电子小白学完基本元器件之后,电子新手该怎么走?
  • 如何突破城通网盘下载限制?ctfileGet直连解析工具全解析
  • 快速上手SiameseAOE:从用户评论中自动提取“属性-观点”对
  • QMCDecode终极指南:轻松解密QQ音乐加密音频文件
  • 5步掌握NSC_BUILDER:Switch游戏文件管理的完整路径
  • 程序员Token消耗排行榜:原来最烧钱的不是写代码!
  • 如何创建语句级触发器_表级操作监控与日志记录实现
  • 2026信息安全就业方向+前景(超详细)|小白入门到精通,收藏永不会踩坑
  • Python学习超简单第二弹:函数
  • PyTorch 2.6镜像效果展示:开箱即用的GPU加速环境实测
  • 揭秘2026年口碑好的浴室柜加盟品牌拓展,价格多少钱 - 工业品牌热点
  • 如何用SD-PPP插件实现Photoshop与AI绘图的无缝协作:3大核心功能详解
  • 国际电信联盟(International Telecommunication Union, ITU)是联合国下属历史最悠久的专门机构
  • 3分钟掌握图表数据提取:WebPlotDigitizer让科研分析效率翻倍
  • Wan2.2-I2V-A14B与Java集成实战:JDK1.8环境下构建企业级调用客户端
  • 论文通关不踩坑:Paperxie,查重降重双 buff 拉满的学术神器
  • 千问3.5-2B赋能前端设计:智能UI组件代码生成与样式建议
  • 掌握Python数据分析核心技巧实战
  • 12.主程序代码word版本少了功能,不全
  • JavaScript中原型链的查找机制与终点null的意义
  • 30+文档平台一键下载:告别繁琐操作,免费获取全网学习资料
  • 题解:Just Jump
  • ctfileGet:告别广告等待,5分钟掌握城通网盘直连解析技术
  • 大模型、RAG、Agent 一起落地后,为什么AI系统测试比传统测试难这么多?
  • Steam成就管理器终极指南:5分钟学会如何轻松解锁和管理游戏成就
  • Ostrakon-VL-8B在网络安全中的应用:识别与分析截图中的敏感信息与钓鱼界面
  • MySQL 事务日志写入机制
  • 图表数据提取神器:WebPlotDigitizer让科研效率提升10倍
  • org.openpnp.vision.pipeline.stages.MaskRectangle
  • GD32F450以太网(2-2):LAN8720A寄存器配置与实战调试指南