Skip to content

网络连接跟踪

目的与范围

网络连接跟踪是负责将网络连接与进程关联并维护文件描述符到套接字地址映射的子系统。这使 eCapture 能够:

  1. 将网络数据包归属到进程:通过在 tcp_sendmsg/udp_sendmsg 上的 kprobe 填充的 network_map
  2. 跟踪连接生命周期:通过在 __sys_connect__sys_accept4tcp_v4_destroy_sock 上的 kprobe
  3. 将文件描述符映射到连接元组:通过从 ConnDataEvent 填充的用户空间 pidConnssock2pidFd 结构

该系统在三个层面运行:

  • 内核空间连接生命周期跟踪kern/openssl.h 中对 connect/accept/destroy 的 kprobe)
  • 内核空间数据包归属kern/tc.h 中对 sendmsg 的 kprobe 和 TC 分类器)
  • 用户空间 FD 到元组的映射(由 user/module/probe_openssl.go 中的 MOpenSSLProbe 维护)

有关 TC 数据包捕获机制本身的信息,请参阅第 3.3 页。有关消费连接事件的事件处理流程的详细信息,请参阅第 2.2 页。

架构概述

网络连接跟踪系统由两个并行的子系统组成:

系统 1:连接生命周期跟踪(kern/openssl.h)

  • 对 connect/accept/destroy 的 Kprobe:跟踪连接何时创建和销毁
  • tcp_fd_infos 映射:连接建立期间的中间存储
  • connect_events perf 数组:向用户空间发出 ConnDataEvent
  • 目的:为 SSL 数据归属提供 FD 到元组的映射

系统 2:数据包归属(kern/tc.h)

  • 对 tcp_sendmsg/udp_sendmsg 的 Kprobe:在发送数据时填充 network_map
  • network_map:将连接元组映射到进程上下文(PID/UID/comm)
  • TC 分类器:在 network_map 中查找数据包以进行进程归属
  • 目的:使 TC 能够使用进程元数据捕获数据包

架构图

来源: kern/openssl.h:41-55, kern/openssl.h:87-92, kern/openssl.h:129-134, kern/openssl.h:374-525, kern/tc.h:39-77, kern/tc.h:135-383, user/module/probe_openssl.go:78-106

连接生命周期跟踪(kern/openssl.h)

该子系统通过钩取系统调用和内核函数来跟踪网络连接的创建和销毁。它向用户空间发出包含文件描述符、套接字指针和连接元组信息的 ConnDataEvent 结构。

连接事件结构(connect_event_t)

connect_event_t 结构在连接建立或销毁时发出:

结构定义(kern/openssl.h:41-55):

字段类型描述
saddrunsigned __int128源地址(IPv4 或 IPv6)
daddrunsigned __int128目标地址
commchar[16]进程命令名称
timestamp_nsu64纳秒时间戳
socku64内核套接字结构指针
pidu32进程 ID
tidu32线程 ID
fdu32文件描述符
familyu16地址族(AF_INET 或 AF_INET6)
sportu16源端口
dportu16目标端口
is_destroyu8如果是销毁事件则为 1,创建事件则为 0
pad[7]u8[7]填充以避免对齐问题

注意: 该结构标记为 __attribute__((packed)),以防止可能导致用户空间事件反序列化问题的填充空洞。

中间存储:tcp_fd_infos 映射

tcp_fd_infos 映射在连接建立期间临时存储文件描述符和套接字指针对。它用于关联多个钩点之间的信息。

映射定义(kern/openssl.h:129-134):

struct tcp_fd_info {
    u64 sock;   // 套接字结构指针
    int fd;     // 文件描述符
};

BPF_MAP_TYPE_HASH: tcp_fd_infos
键: u64 (pid_tgid)
值: struct tcp_fd_info
最大条目数: 10240

生命周期:

  1. 入口探针在 tcp_fd_infos[pid_tgid] 中存储 FD
  2. 中间探针向同一条目添加套接字指针
  3. 返回探针读取两个值,发出事件,删除条目

连接建立钩子

主动连接:__sys_connect

系统钩取 __sys_connect 系统调用及相关内核函数以跟踪出站连接:

钩子序列图:

实现细节:

  1. probe_connect(kern/openssl.h:374-382):

    • 钩子:__sys_connect 系统调用
    • 提取:从第一个参数提取 FD
    • 存储:{fd, sock=0}tcp_fd_infos[pid_tgid]
  2. probe_inet_stream_connect(kern/openssl.h:384-393):

    • 钩子:inet_stream_connect 内核函数
    • 提取:从第一个参数提取套接字指针
    • 更新:tcp_fd_infos[pid_tgid].sock
  3. retprobe_connect(kern/openssl.h:456-471):

    • 钩子:__sys_connect 系统调用返回
    • 调用:lookup_and_delete_fd_info()tcp_fd_infos 检索并删除条目
    • 验证:套接字指针非空
    • 调用:kretprobe_connect() 发出事件
  4. kretprobe_connect(kern/openssl.h:395-454):

    • 读取:使用 bpf_probe_read_kernel() 读取套接字字段:
      • sk->__sk_common.skc_family(地址族)
      • sk->__sk_common.skc_addrpair(IPv4 地址)或 skc_v6_*(IPv6)
      • sk->__sk_common.skc_portpair(网络字节序端口)
    • 验证:端口和地址非零(连接已建立)
    • 构建:带有 is_destroy=0connect_event_t 结构
    • 发出:事件到 connect_events perf 数组

主动连接标志:

active 布尔参数确定连接是出站(true)还是入站(false):

  • 出站(active=true)dport = bpf_ntohs(ports)sport = ports >> 16
  • 入站(active=false):交换源和目标(因为 accept 在套接字结构中将远程返回为源)

被动连接:__sys_accept4

系统钩取 __sys_accept4 系统调用以跟踪入站连接:

钩子实现:

  1. probe_connect(在 __sys_accept4 上)(user/module/probe_openssl_text.go:94-98):

    • 注意:重用与 connect 相同的函数,在 tcp_fd_infos 中存储 FD
    • UID:"kprobe_sys_accept4"
  2. probe_inet_accept(kern/openssl.h:478-487):

    • 钩子:inet_accept 内核函数
    • 提取:从第二个参数提取套接字指针(PT_REGS_PARM2
    • 更新:tcp_fd_infos[pid_tgid].sock
  3. retprobe_accept4(kern/openssl.h:489-510):

    • 钩子:__sys_accept4 系统调用返回
    • 提取:从返回值提取新的 FD(PT_REGS_RC
    • 验证:FD 非负
    • 调用:lookup_and_delete_fd_info() 和带 active=falsekretprobe_connect()

连接销毁钩子

probe_tcp_v4_destroy_sock(kern/openssl.h:512-525):

跟踪内核何时销毁 TCP 连接:

  • 钩子:tcp_v4_destroy_sock 内核函数
  • 提取:从第一个参数提取套接字指针
  • 发出:connect_event_t,包含:
    • sock 指针(用于与现有条目关联)
    • is_destroy = 1
    • 其他字段清零(销毁时仅 sock 重要)

目的: 允许用户空间在连接关闭时从 pidConns 中删除过期条目,而无需显式调用 close() 系统调用(例如,进程终止、内核清理)。

SSL_set_fd 钩子

除了 connect/accept 钩子外,系统还钩取 SSL_set_fd 及相关函数,以处理在 SSL 结构创建后设置 FD 的情况:

钩子(user/module/probe_openssl_text.go:130-150):

  • SSL_set_fd
  • SSL_set_rfd(读 FD)
  • SSL_set_wfd(写 FD)

实现(kern/openssl.h:531-543):

  • 存储映射:ssl_st_fd[ssl_addr] = fd
  • 使用场景:当 bio->num 为 0 时由 process_SSL_bio() 使用

来源: kern/openssl.h:41-55, kern/openssl.h:87-92, kern/openssl.h:129-134, kern/openssl.h:354-543, user/module/probe_openssl_text.go:76-150

数据包归属系统(kern/tc.h)

该子系统使 TC 分类器能够通过维护连接元组到进程上下文的映射来将捕获的数据包归属到进程。

network_map 结构

network_map 是一个 LRU(最近最少使用)哈希映射,存储由网络连接标识符索引的进程上下文信息。它使 TC 分类器能够确定哪个进程拥有特定数据包。

映射定义:

c
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");

键结构(net_id_t):

字段类型描述
protocolu32IP 协议(IPPROTO_TCP=6,IPPROTO_UDP=17)
src_portu32源端口号
src_ip4u32源 IPv4 地址
dst_portu32目标端口号
dst_ip4u32目标 IPv4 地址
src_ip6[4]u32[4]源 IPv6 地址
dst_ip6[4]u32[4]目标 IPv6 地址

值结构(net_ctx_t):

字段类型描述
pidu32进程 ID
uidu32用户 ID
comm[16]char[]进程命令名称(TASK_COMM_LEN)

重要: network_map 与上述连接生命周期跟踪系统分离。它由 sendmsg 钩子填充(而非 connect/accept),仅用于 TC 分类器的数据包归属。

来源: kern/tc.h:39-54, kern/tc.h:72-77

数据包归属的 Kprobe 钩子

系统钩取 tcp_sendmsgudp_sendmsg 内核函数,以在进程发送数据时填充 network_map。这些钩子独立于连接建立钩子,并服务于不同的目的:使 TC 分类器能够将数据包归属到进程。

TCP 钩子实现:

  • 入口点:kern/tc.h:290-347tcp_sendmsg kprobe
  • 读取:使用 bpf_probe_read() 读取 IPv4 和 IPv6 的 struct sock 字段
  • 处理:AF_INET(IPv4)和 AF_INET6(IPv6)套接字族
  • 提取:skc_num(本地端口)、skc_dport(远程端口,网络字节序)、IP 地址
  • 更新:使用连接元组和进程上下文更新 network_map

UDP 钩子实现:

  • 入口点:kern/tc.h:349-397udp_sendmsg kprobe
  • 与 TCP 钩子几乎相同,但将 protocol 设置为 IPPROTO_UDP
  • 重用相同的 struct tcphdr 布局进行端口提取(UDP 头具有相同的初始布局)

注意: 这些 kprobe 故意不按 target_pidtarget_uid 过滤,因为 TC 分类器需要所有进程的全面连接信息才能执行数据包归属。

为什么与 Connect 钩子分离?

  • Connect/accept 钩子可能在实际发送数据之前触发
  • Sendmsg 钩子保证连接处于活动状态且数据正在流动
  • LRU 驱逐自动删除非活动连接
  • 无需显式清理跟踪

来源: kern/tc.h:285-333, kern/tc.h:335-383

TC 分类器数据包关联

TC(流量控制)分类器在数据链路层(第 2 层)的入口和出口路径上拦截数据包。对于每个数据包,它们提取连接元组(协议、IP、端口)并在 network_map 中查找以将数据包归属到进程。

双向查找逻辑:

TC 分类器首先尝试在 network_map 中查找数据包的源到目标元组。如果未找到,它会交换源和目标(IP 和端口)并重试查找。这处理两个方向的数据包:

  • 出站数据包: 元组匹配由 tcp_sendmsg/udp_sendmsg 创建的条目(进程是发送者)
  • 入站数据包: 交换后的元组匹配条目(进程是接收者)

过滤器应用:

成功查找后,TC 分类器应用 PID 和 UID 过滤器(如果设置了 target_pidtarget_uid)以决定是否捕获数据包。只有与过滤条件匹配的数据包才会发出到 skb_events perf 数组。

来源: kern/tc.h:135-271, kern/tc.h:274-283

用户空间连接跟踪(MOpenSSLProbe)

目的与集成

MOpenSSLProbe 中的用户空间连接跟踪维护进程文件描述符(FD)与其对应套接字地址(元组)之间的映射。这弥合了 SSL/TLS 事件捕获和网络数据包捕获之间的差距:

  1. SSL 函数操作 FDSSL_read/SSL_write 使用文件描述符,而不是套接字结构
  2. SSL 数据事件需要丰富:来自 uprobe 的 SSLDataEvent 仅包含 PID 和 FD
  3. 数据包捕获需要关联:TC 捕获的数据包需要与 SSL 数据匹配

该跟踪由上述连接生命周期 kprobe(connect/accept/destroy)发出的 ConnDataEvent 结构填充。

与 eBPF 事件的关系:

  • 输入:来自 connect_events perf 数组的 ConnDataEvent
  • 源:__sys_connect__sys_accept4tcp_v4_destroy_sock 上的 Kprobe
  • 输出:可通过 GetConn(pid, fd) 查询的 ConnInfo 结构

来源: user/module/probe_openssl.go:78-82, user/module/probe_openssl.go:90-94, user/event/event_conn.go

数据结构

pidConns 结构:

  • 类型:map[uint32]map[uint32]ConnInfo
  • 外部键:进程 ID(PID)
  • 内部键:文件描述符(FD)
  • 值:包含元组字符串和套接字指针的 ConnInfo

sock2pidFd 结构:

  • 类型:map[uint64][2]uint32
  • 键:套接字指针(来自内核)
  • 值:用于反向查找的 [PID, FD] 数组

ConnInfo 结构:

  • tuple(字符串):格式为 "src_ip:src_port-dst_ip:dst_port" 的连接元组
  • sock(uint64):内核套接字结构指针用于一致性检查

来源: user/module/probe_openssl.go:78-82, user/module/probe_openssl.go:90-94

连接生命周期管理

连接创建(AddConn):

当分派 IsDestroy=0ConnDataEvent 时,调用 AddConn

  1. 验证 FD 非零 probe_openssl.go:399-402
  2. 获取 pidLocker 互斥锁以进行线程安全访问 probe_openssl.go:404
  3. 如果不存在则为 PID 创建嵌套映射 probe_openssl.go:406-409
  4. pidConns[pid][fd] 中存储 ConnInfo{tuple, sock} probe_openssl.go:410
  5. sock2pidFd[sock] = [pid, fd] 中创建反向映射 probe_openssl.go:413
  6. 记录调试消息 probe_openssl.go:415

连接销毁(DelConn → DestroyConn):

当分派 IsDestroy=1ConnDataEvent 时:

  1. DelConn 使用 time.AfterFunc 安排 3 秒延迟的 DestroyConn probe_openssl.go:455-462
  2. 延迟允许事件处理器完成此连接的事件合并
  3. DestroyConn 通过 processor.WriteDestroyConn(sock) 向处理器发出清理信号 probe_openssl.go:423
  4. sock2pidFd 中执行反向查找以找到 [pid, fd] probe_openssl.go:426-433
  5. 验证套接字一致性(存储的 sock == 提供的 sock) probe_openssl.go:441-445
  6. pidConns[pid][fd] 中删除条目,如果为空则删除 PID 映射 probe_openssl.go:446-449
  7. sock2pidFd 中删除反向映射 probe_openssl.go:431

连接查找(GetConn):

在处理 SSLDataEvent 结构时由 dumpSslData 调用:

  1. 验证 FD 非零 probe_openssl.go:465-467
  2. 获取 pidLocker 互斥锁 probe_openssl.go:469
  3. 执行两级查找:pidConns[pid][fd] probe_openssl.go:471-478
  4. 返回指向 ConnInfo 的指针或 nil(如果未找到) probe_openssl.go:479

线程安全: 所有操作都获取 pidLocker 互斥锁,以防止多个事件处理线程并发访问映射时出现竞争条件。

来源: user/module/probe_openssl.go:398-416, user/module/probe_openssl.go:418-462, user/module/probe_openssl.go:464-480

完整跟踪流程

本节描述两个跟踪系统如何协同工作,从系统调用到数据包捕获提供全面的连接跟踪。

两系统集成

关键点:

  1. 连接建立:

    • connect()accept() 系统调用触发 kprobe
    • 发出带有 FD、套接字指针和完整连接元组的 ConnDataEvent
    • 用户空间在 pidConns 中存储 FD→元组映射
  2. 首次数据传输:

    • send() 系统调用调用 tcp_sendmsg
    • Kprobe 使用元组→进程映射填充 network_map
    • 独立于连接建立跟踪
  3. SSL 数据捕获:

    • SSL_write 上的 Uprobe 捕获带有 PID 和 FD 的数据
    • 用户空间调用 GetConn(pid, fd) 检索连接元组
    • 事件被丰富并发送到协议解析器
  4. 数据包捕获:

    • TC 分类器从数据包头提取元组
    • network_map 中查找元组(由 sendmsg 填充)
    • 将数据包归属到进程,应用过滤器
    • 将带有进程元数据的数据包发出到用户空间

为什么需要两个系统?

  • 连接生命周期系统:为 SSL 数据提供 FD→元组映射
  • 数据包归属系统:为 TC 提供元组→进程映射
  • 不同的触发点(connect vs sendmsg)服务于不同的目的

来源: kern/openssl.h:374-525, kern/tc.h:285-383, user/module/probe_openssl.go:733-775

与模块分派器的集成

MOpenSSLProbe.Dispatcher 方法将连接相关事件路由到适当的处理程序:

go
func (m *MOpenSSLProbe) Dispatcher(eventStruct event.IEventStruct) {
    switch ev := eventStruct.(type) {
    case *event.ConnDataEvent:
        if ev.IsDestroy == 0 {
            m.AddConn(ev.Pid, ev.Fd, ev.Tuple, ev.Sock)
        } else {
            m.DelConn(ev.Sock)
        }
    case *event.TcSkbEvent:
        err := m.dumpTcSkb(ev)
    case *event.SSLDataEvent:
        m.dumpSslData(ev)
    // ... 其他事件类型
    }
}

事件流表:

事件类型源 eBPF 映射源 Kprobe/Uprobe处理程序目的
ConnDataEventconnect_events__sys_connect__sys_accept4tcp_v4_destroy_sockAddConn/DelConn维护 FD→元组映射
TcSkbEventskb_eventsTC 出口/入口分类器dumpTcSkb将数据包写入 pcapng
SSLDataEventtls_eventsSSL_read/SSL_write uprobedumpSslData使用元组丰富,发送到处理器

分派器在从 perf/ring 缓冲区解码事件后由 Module.Dispatcher 调用。事件从多个映射并发读取,解码为各自的事件结构,并路由到处理程序。

来源: user/module/probe_openssl.go:733-754, user/module/probe_openssl.go:756-775, user/module/imodule.go:409-448

配置与过滤

连接跟踪系统遵循通过命令行标志配置的进程和用户过滤:

内核空间过滤:

用户空间过滤:

  • 在处理 SSLDataEvent 时在 dumpSslData 中强制执行
  • 只有与过滤条件匹配的连接才会在 pidConns 中跟踪
  • 过滤器检查在调用 AddConn 之前发生

默认元组处理:

GetConn 返回 nil(未跟踪连接)时,SSLDataEvent 使用默认值:

这发生在以下情况:

  • FD 无效(≤0)
  • 连接未被捕获(被过滤掉,或 BIO 类型是非套接字)
  • 连接已销毁

对跟踪系统的影响:

  1. 连接生命周期 kprobe:不应用过滤器(跟踪所有连接以实现准确的 FD 映射)
  2. Sendmsg kprobe:不应用过滤器(TC 需要全面的 network_map
  3. TC 分类器:在发出数据包事件之前应用过滤器
  4. 用户空间 AddConn:存储连接时应用过滤器

来源: kern/common.h:66-70, kern/tc.h:242-244, user/module/probe_openssl.go:361-387, user/module/probe_openssl.go:756-775

网络连接跟踪 has loaded