必须用tcp_connect kprobe作为起点,因其是客户端发起connect()后、发出SYN前的最后一个稳定钩子,能获取完整五元组和进程信息;而tracepoint:sock:inet_sock_set_state仅在socket状态实际变化时触发,对未创建socket的失败连接完全静默。

直接用 libbpf-go 挂载到 tcp_connect 和 inet_csk_complete_hashdance(或 tcp_v4_conn_request)这两个内核函数上,再结合 tracepoint:sock:inet_sock_set_state 补全状态跃迁,才能可靠捕获 SYN 发出但未收到 ACK 的连接失败事件。靠 conntrack -E 或 ss -tunap 根本看不到这类“胎死腹中”的连接。
为什么 tcp_connect kprobe 是起点,而不是 tracepoint
连接建立失败往往卡在客户端发完 SYN 就终止,此时服务端甚至没进协议栈——tracepoint:sock:inet_sock_set_state 只在 socket 状态真正变化时触发,对未创建 socket 的失败连接完全静默。tcp_connect 是内核中 client 侧发起 connect() 后、构造并发出第一个 SYN 包前的最后一个稳定钩子,它能拿到完整的五元组、PID/TID、进程名,且所有内核版本(4.19+)都稳定存在。
- 必须用
kprobe:因为tcp_connect是普通函数,不是 tracepoint;libbpf-go加载时需指定AttachType: ebpf.AttachKprobe - 避免用
tcp_v4_connect:该函数在某些路径下会被绕过(如 UDP socket 复用、AF_INET6 场景),而tcp_connect是统一入口 - 读取参数要小心:
ctx->args[0]是struct sock *,得用bpf_probe_read_kernel读取其sk_daddr/sk_dport字段;别信硬编码偏移,必须依赖 BTF
如何识别“SYN 发了但没回”这种失败
单靠 tcp_connect 只能知道“尝试连”,不能判断成败。必须和状态机联动:监听 tracepoint:sock:inet_sock_set_state,当看到某 socket 从 TPC_ESTABLISHED 或 TPC_SYN_RECV 回落到 TPC_CLOSE,或者压根没收到任何状态更新,才可判定失败。但更高效的做法是捕获内核丢包点:
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 同时挂
kretprobe:tcp_connect:返回值为 0 表示 SYN 已发出;非 0(如 -EINPROGRESS)说明 connect() 调用本身失败(比如路由不可达、本地端口耗尽) - 加一个
kprobe:ip_local_out过滤出目的端口为 80/443 且 protocol == IPPROTO_TCP 的包,再检查其 TCP flag 是否含TH_SYN且无TH_ACK——这是真正的“裸 SYN” - 失败归因靠时间戳对齐:用
bpf_ktime_get_ns()记录tcp_connect时间,若 3 秒内未在inet_sock_set_state中见到对应 socket 的TPC_ESTABLISHED,就标记为“客户端超时”
Go 用户态怎么安全消费这些事件
内核侧把失败事件写入 perf_event_array 或 ring_buffer,Go 侧不能用 Map.Lookup 去轮询——这会漏数据、卡 goroutine。必须用 PerfEventArray.Read() 配合循环解析:
立即学习“go语言免费学习笔记(深入)”;
- 定义 Go struct 时字段顺序、大小、对齐必须和内核侧 C struct 完全一致;推荐用
btfgen自动生成,别手写 - 每次
Read()返回的是原始字节流,用unsafe.Slice(buf, n)转成[]byte,再按固定 offset 解析 PID、IP、port、timestamp - 关键字段如 IP 地址要用
binary.BigEndian.Uint32()转换,别用int32强转——网络字节序和主机字节序不一致 - 高频场景下禁用 JSON 序列化;直接拼接日志字符串或写入 Prometheus Counter,例如:
connect_failure_total.WithLabelValues(dstIPStr, strconv.Itoa(int(dstPort))).Inc()
最容易被忽略的是:失败连接的源端口可能已被快速复用,导致多个失败事件共享同一个五元组;必须用 pid + tgid + timestamp 组合作为唯一 key,否则聚合统计会严重失真。

















