兰 亭 墨 苑
期货 · 量化 · AI · 终身学习
首页
归档
编辑文章
标题 *
URL 别名 *
内容 *
(支持 Markdown 格式)
# 多跳链路排查方法论:当每一跳都在替换身份 > 一条每小时的奇怪请求,追了三天,最后追出一个公网 AI 网关的匿名暴露面。 > 真正难的从来不是抓包,而是**在链路上判断"谁是谁"**。 ## 引子:一个反直觉的现象 先说一个让排查一度跑偏的事实。 同一次请求,四个观测点看到的是四个不同的"发起方": | 观测位置 | 看到的来源 | 看到的 UA | |----------|-----------|-----------| | 最外层入口(Cloudflare 账本) | `31.77.203.199` | `ZOLTRAAK/0.2.0` | | 末端服务(Mac 日志) | `192.168.31.225` | `Go-http-client/1.1` | | 中间跳(路由器抓包) | `192.168.31.225` | `Go-http-client/1.1` | 如果只看末端日志,会得出"有个 Go 程序从内网打过来"——**完全指向错误方向**。 如果只看最外层账本,会知道"有个芬兰扫描器在打入口"——**正确,但看不到它是怎么穿透到末端的**。 只有把两端对上,才能拼出完整链路。 **这就是多跳链路排查的第一性困难:任何一跳看到的来源身份,都只代表"它的上一跳",不代表最终用户。** --- ## 一、为什么"代理链"必然让身份失真 这不是配置问题,是代理架构的固有特性。 ### 1.1 每一跳都会重写身份字段 ``` 外部调用方 → 入口 Worker : 注入自己的 Authorization → 中间网关 : 用自己 provider 的 key 替换 → 末端服务 : 看到的是中间网关的身份 ``` **User-Agent 同理**:外部传入的 UA 在过了第一跳之后就被丢弃——Go 的 `http.Client` 默认发 `Go-http-client/1.1`,Python 的 `requests` 默认发 `python-requests/x.y`。**中间跳的语言运行时,会悄悄把自己的默认 UA 盖上去。** ### 1.2 SNAT 会伪装源 IP 如果链路上有任何一层做了 NAT/SNAT,末端看到的源 IP 就是那个 NAT 设备的地址,不是真实发起方。 本例中,末端看到的源 IP 是路由器的 LAN 地址——真实发起方在**另一台通过虚拟网络隧道连进来的机器**上,经路由器 SNAT 后到达末端。**不查路由器,永远不会知道这个地址背后是谁。** ### 1.3 结论 > **在一条代理链上,"来源身份"是一个逐跳衰减的量。越靠近末端,越看不到真相。** 所以排查方法论的第一原则是:**同时采集"最外层"和"最内层"的元数据**。 - **最外层**(入口账本):告诉你"谁在调用"——追责/封禁的依据 - **最内层**(末端日志):告诉你"请求变成了什么"——理解链路作用的依据 - **中间跳**:告诉你"怎么连起来的" --- ## 二、排查的五个反直觉 ### 反直觉 1:单看一层日志会误判 多跳链路上,**每一环看到的"身份"都只代表上一跳**。所以: - 末端日志没有来源信息 = 排查的最大障碍(本例中,末端当时只记请求体,不记 `RemoteAddr`/`UA`,直接导致必须靠明文抓包补上,多花了两小时) - 补齐末端日志(`[REQ] remote=... ua=...`)**比抓出扫描器更有长期价值** ### 反直觉 2:"每小时"不等于"cron" 直觉上"每小时一次"很像定时任务。但实测间隔是 **62~77 分钟,不是精确 60**。 这个细节直接排除了所有 `cron`/定时器嫌疑——因为**定时任务是精确对齐的,而"请求→等响应→sleep 一小时"的循环会漂移**(响应耗时 in 变,所以间隔才漂)。 **一条可复用的判据:间隔精确对齐 = 定时器;间隔漂移 = 请求-响应-sleep 循环。** ### 反直觉 3:"报错"不等于"安全" 加固前实测暴露面:26 个模型中,15 个匿名可打通(200),11 个返回 500。 那 11 个 500 **全部是上游自身故障,无一因鉴权被挡**——请求照样穿透到了上游。**上游修好的那一刻,它们立刻就能被白嫖。** > **验证防护要测"通不通",而不是看"有没有报错"。把报错当安全,是排查中最危险的误读。** ### 反直觉 4:`grep -I` 是陷阱 排查编译型服务时,第一遍用 `grep -I`(跳过二进制)搜索特征串,结果**什么都没找到**,差点得出"源码里没有"的结论。 改用 `grep -a`(text 模式)重搜,才确认二进制里也没有。 > **搜编译型语言的程序,必须带 `-a`/`--text`。** 否则"搜不到"可能是假阴性。 ### 反直觉 5:时间吻合不等于因果 某个进程的启动时间是 09-11 06:25,而首次探针是 03:28——**早了 3 小时**。如果只看到"日期相同"就下结论,就会误判。 > **时间线必须精确到小时,不能只看日期。** --- ## 三、可复用的排查流程 基于这次实战,提炼出一套流程: ### 第 1 步:排除本机(穷举 + 清单法) 先用三条标准筛出所有嫌疑对象: 1. 有没有 HTTP 客户端代码(能发请求) 2. 有没有定时/循环机制(能每小时发) 3. 有没有生成随机串的能力 逐个排查。**排除法本身就是情报**——每排除一个,就缩小一圈范围,同时摸清自家基础设施有哪些组件。 **关键副产品**:这一阶段的"排除"会顺带产出一份**完整的调用方清单**,这份清单在加固阶段直接派上用场——**没有它就不敢打开强制开关**。 ### 第 2 步:多点位同时布防 探针每小时才来一次,单点抓不到就等于浪费一小时。 **同时在多个层布防**(端点/中间设备/上游),做到"一次抓到"。这次布了 4 个点位,真正给出决定性证据的是其中一个(明文请求头),其余是保险——**多点位的价值在于"只要有一个抓到就不算白等"**。 ### 第 3 步:抓明文,别急着上 Wireshark 目标是 HTTP 明文时,`tcpdump -A`(ASCII 模式)就能在输出里直接看到请求头和 JSON body,不用开 Wireshark 解析。 ### 第 4 步:用"账本"一击命中 本例的转折点,是入口 Worker 的 D1 记账表 `access_logs`(记录 `ip / country / app_name / model_name / user_agent / duration`)。 一条 SQL 就锁定了扫描器身份。**记账表是排查的黄金档案。** > **"每个入口都记调用方元数据"是一个值得坚持的习惯。** 它让"猜谁在打"变成"查谁在打"。 ### 第 5 步:两端对上,拼出完整链路 把最外层账本和最内层日志对时间戳,才能拼出完整链路。 --- ## 四、几个踩过的实操坑 | 坑 | 现象 | 解法 | |----|------|------| | SSH 断开会杀后台进程 | `tcpdump` 输出始终 0 字节 | 用 `setsid` 脱离会话,显式重定向三路 IO | | 精简系统没有 `timeout` | `No such file or directory` | 用 `setsid` + 从外部 `pkill` 收尾 | | `pkill -f tcpdump` 杀掉自己 | SSH 退出码 255 | 因为命令行本身含 "tcpdump" 字符串;改按进程名精确匹配 | | `conntrack` 匹配格式 | 日志永远为空 | 是 `dport=8082` 不是 `:8082` | | Zsh 的 glob | `grep -r "*.py"` 直接报错 | 用 `--include='*.py'` 或引号 | > **这些"命令静默失败"最容易浪费排查时间**——因为看起来命令跑了,实际什么都没做。 --- ## 五、加固的哲学:治本 vs 修补 排查出问题后,加固方案的选择比排查本身更能体现工程判断。 ### 5.1 为什么否掉 IP/UA 黑名单 最直觉的方案是"封掉那个 IP 和 UA"。但它被明确否掉: - 扫描器换 IP、换 UA、换 ASN 只需几秒 - 它只是把问题往后推,不解决根因 **黑名单的失效模型**: ``` 换 IP → 失效 换 UA → 失效 换 ASN → ASN 级封禁失效 用全新 IP → 所有规则重新配 ``` 本质:**黑名单是"枚举坏",而坏是无限的。** ### 5.2 为什么选凭据白名单 根因是入口**没有调用方身份概念**。解法是给它一个: - 服务端持 `X-Client-Token`(64 位随机,猜不到) - 浏览器靠 `Origin` 白名单 - 其余一切拒绝 **白名单的失效模型**:**枚举好,而好是有限的**——合法调用方就那么几个,枚举得完。 | 维度 | 黑名单 | 凭据白名单 | |------|--------|-----------| | 维护成本 | 持续增长 | 固定 | | 抗绕过 | 弱 | 强 | | 可审计 | 差 | 好 | | 误伤风险 | 高 | 低 | **代价**:需要改客户端。但"先盘清调用方"的价值就在这里——先确认合法调用方全是自己的机器,改起来的成本完全可控。 ### 5.3 校验放"代码里"还是"域名层" 本例有个关键决策:鉴权放 Cloudflare Worker 代码里,而非域名层的 Cloudflare Access。 原因:入口有**两个门**——主域名直连 + 另一个域名经服务端代理转发。**Access 是域名层的保护,只能盖住一个门**;放代码里则两个门都经过同一个 Worker,**一次改动覆盖全部入口**。 > **多入口是鉴权的常见盲区。不查清楚有几个门,鉴权方案就会漏掉一半。** --- ## 六、三条最值钱的教训 1. **"看不出是谁"本身就是最大的线索**——它说明日志有盲区,而补齐这个盲区比抓出具体某个扫描器更有长期价值。 2. **治本不是"拦住这个 IP",而是"让这个入口有身份概念"**。前者是修补,后者是修复。 3. **动手前先盘清调用方**。正是因为先拉出了"所有合法调用方都是自己的机器"这个事实,才敢放心打开强制开关。 --- ## 结语 多跳链路排查的核心难点,从来不是技术工具,而是**认知**: > 你看到的"来源",永远只是"上一跳"。 理解这一点,就会自然地同时看首尾两端、就会怀疑"精确间隔"、就会区分"报错"和"安全"、就会在做加固时优先考虑"身份"而非"特征"。 **排查和加固其实是同一件事的两面**——排查时排出的调用方清单,就是加固时的白名单。想清楚这一点,很多决策就顺了。 --- *(本文基于一次真实的公网网关匿名调用事件整理,已隐去具体域名与凭据。)*
配图 (可多选)
选择新图片文件或拖拽到此处
标签
更新文章
删除文章