
样例工程前后干了四件事:
阶段 | 主题 | 结果 |
|---|---|---|
1 | 双容器治理改造 | 冻结对象从"进程"改为"容器",治理拓扑定型 |
2 | 配置优化 | 删掉 8 个配置键,冻结语义 |
3 | 架构梳理 | 首连报错、冻结半径、污点粒度等边界问题归档 |
4 | 排障 | 污点链静默失效(主因)+ fork 挂载被内核拒(次因) |
sentinel 本身也跑在容器里,并且去治理 demo-agent 这个容器。这样把 cgroup freezer 冻结的对象可以指定为容器"。加了硬约束:
挂载治理目标走命令行(sentinel-cli),不进配置文件。
sentinel-mgmt | sentinel-agent | |
|---|---|---|
角色 | daemon + CLI + 工具链 | 被治理的 demo-agent |
权限 | privileged + | 无特权 |
关键点 | 能看到宿主 PID 和全局 cgroup 树 | connect 钩子只挂它的 cgroup |
冻结语义为"冻结整个 agent 容器"——同容器的无辜
进程会一起暂停,这是有意为之。
Windows: docker inspect -f '{{.State.Pid}}' sentinel-agent → 宿主 PID
sentinel 内:attach-agent.sh <PID>
(脚本读 /proc/<PID>/cgroup 找到容器 cgroup 路径 →
sentinel-cli cgroup <路径>)一次动作完成两件事:connect 钩子挂上该 cgroup + freezer 锁定冻结目标。
注意:挂载状态存在 daemon 内存里,daemon 重启后必须重新挂载——
这是"map 不 pin"取舍的一部分(见 3.5)。
原则:能靠固定语义解决的,不给用户旋钮。逐个决策过程:
键 | 去向 | 理由 |
|---|---|---|
| 删 | 挂载是运行时动作(CLI),不是静态配置 |
| 删 | 这三个开关只有一种正确组合,固化成行为 |
| 删 | 超时不是裁决,不该产生裁决动作 |
| 删(连同整个 pin 机制) | map 生命周期跟进程走,少一个 bpffs 运行前提 |
| 删(改为代码内常量 30s) | 避免为每个临时条目建 timer;常量旁写足注释 |
固化后的四种结局(无任何开关可改):
结局 | 动作 |
|---|---|
人工批准 | 临时放行(TTL)+ 解冻 |
人工拒绝 | 永久拉黑 + 解冻 |
审批超时 | 只解冻,不进黑名单,重连重新审批 |
没有审批通道 | 视同拒绝:拉黑 + 解冻 |
cgroup/connect 钩子没有能力"挂起"一个系统调用等审批——它只能
当场放行或当场拒绝。所以被拦进程的第一次连接必然失败(报权限错误),
重试就是重新触发审批的机制;审批通过后临时放行生效,重试即成功。
污点判定按进程(读过敏感文件的进程才升级),拦截/冻结按容器。
一个进程读敏感文件 → 它自己连任何目标都要审批 → 触发审批时整个容器陪冻。两层粒度各司其职。
匹配的是 openat 传入的原始字符串(容器内视角),前缀匹配 + 边界检查(/root/.ssh 不会匹配 /root/.sshx)。已知绕过面:相对路径、symlink 指向敏感文件(字符串不同就不命中)、跨容器同路径歧义。
要根治需路径规范化(bpf_d_path 方向),暂未做,已记录。
map 挂载状态生命周期 = daemon 进程。代价:重启即重置(策略从配置重新灌入、临时放行丢失、需重新挂载治理目标);收益:无 bpffs 前提、无残留状态、排查时心智负担小。对单机治理场景是划算的。
症状 | 根因 | 修法 |
|---|---|---|
冻结报"freezer 目标未设置",但治理目标明明挂着 | 我把 API 依赖里的 Freezer 漏接了( | 补上接线;并让接口在 Freezer 为空时告警,防止再次静默失联 |
| approve 要完整 request-id(形如 | 说明用法;(提议加 PID 自动解析,被否决,不再动) |
改造完成后验证场景 6(污染进程连白名单目标)发现:直接放行,不冻结不出卡。此后多轮排障,两个问题互相掩护。
内核对白名单放行是不发事件不打日志的——所以没有 [connect] 日志是正常的。真正异常的只有 [taint] 标记污染 缺失。这一步没走偏,省了大量时间。
排查路径(猜想 → 证据 → 结论):
猜想 | 证据 | 结论 |
|---|---|---|
挂错容器 | CLI 查治理目标正是 demo 所在容器 | 排除 |
监控路径没写入内核 |
| 排除 |
挂载"假成功" |
| 基本排除 |
程序没被执行 | 开统计后 | 排除 |
旧二进制 | 在二进制里搜诊断关键字 + md5 对比 | 排除 |
参数位置错位 | 内核 format 文件:真实位置差 8 字节 | 确认 |
根因因果链:
代码以为 filename 在位置 16 → 实际那里是 dfd(值通常是 -100)→ 把 -100 当内存地址读字符串 → 每次读失败 → 静默返回 → 程序执行 719 次,全部无声空转能潜伏是因为:加载成功、挂载成功、map 正确——三盏绿灯全亮,唯独数据是错的。
修复 + 防复发三层:改对结构体;编译期断言钉死关键位置;启动时读内核 format 文件自检,对不上就大声停用污点功能并打印真实位置(而不是默默空转);再用实测 format 原文做单测样本。布局由"long 类型多宽"决定,x86-64/arm64 一套通用;32 位内核会被启动自检显式拒绝。
解法:三层备胎——常规通道 → 老式 ioctl 通道 → fexit/kernel_clone(函数返回时拿子进程 PID,和 perf 子系统完全不沾边;kernel_clone 从5.10 起稳定存在;极老内核没 BTF 时自动剔除此程序重试,不强制依赖)。
副作用:分不清进程/协程创建,Go 起协程会各打一条传播日志(无害,判定永远看进程号,协程退出即清理)。
老式通道第一版用了 x/sys 的结构体,锁定的 v0.43.0 上字段名变了、编译失败。改为按内核文档手工拼 64 字节。
# 程序到底执行没有?(先开统计!)
echo 1 > /proc/sys/kernel/bpf_stats_enabled
bpftool prog show name sentinel_taint_openat # run_cnt 应该自己往上涨
# 确认挂载还活着 / 看内核里存了什么
bpftool perf show
bpftool map dump name watch_map # 监控路径
bpftool map dump name taint_map # 被标记的进程号
# 内核真实传参布局(永远别信记忆里的"标准布局")
cat /sys/kernel/tracing/events/syscalls/sys_enter_openat/format
# 确认事件上报通路还通:连一个黑名单目标,应看到 [connect] 日志原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。