问题背景
近日我因为写hass插件分析某天燃气的API时遇到一个异常现象:对目标 API 接口 http://xxxx/api/my/getUserInfo 的请求,在直连网络下返回 200 OK,但一旦通过 Clash 启用代理,服务端即返回 参数异常。
该请求为明文 HTTP(非 HTTPS),使用 POST
方法,携带标准 JSON 数据及 Content-Type: application/json
头部。由于不涉及 TLS 层,可排除证书校验、SNI 指纹、ECH、SSL Pinning 等常见代理干扰因素。
问题核心在于:同一请求,仅因是否经过代理而行为不同。这表明问题出在代理对请求的"透明"修改上。
出于隐私保护、安全合规和信息脱敏 的考虑,后文的域名都用
example.com
代替, 对应的 secret / token 等信息也做了一定的随机。
第一阶段排查:应用层差异分析(curl --verbose
)

可以看到,在所有参数都不变的情况下,当我把代理指向给Clash后,返回结果就变了,即使我的clash是直连,他仍然意外的修改了我的某些请求数据。
第二阶段:为何不能使用 Proxyman或者Charles?
Fiddler、Charles、Proxyman 等抓包工具本质上是 HTTP 中间人代理(MITM Proxy) ,其工作原理依赖于客户端主动配置代理,并在应用层解析 HTTP 流量。
但在本场景中,Clash 已作为系统级透明代理接管了所有流量,导致:
- Fiddler/Charles/Proxyman 无法再"代理代理";
- 即使配置代理链,也可能因端口冲突或协议封装导致抓包失败。
理论上,我们也可以设置一个更为复杂的代理链来完成, 只要我们的抓包工具处于后置代理的状态,让流量先经过梯子🪜 ,然后再经过抓包工具 也可以完成抓包。不过我在Proxyman上找到的了 External Proxy 工作原理应该是前置代理,所以不可以。其他抓包工具我没有,就没有了解。
因此,必须使用直接从网络接口捕获原始数据包 的工具,如 tcpdump
或 Wireshark
第三阶段:tcpdump 抓包与 Wireshark 分析
抓包命令
使用 tcpdump
捕获目标主机 80 端口的所有流量:
bash
sudo tcpdump -i any port 80 -w getUserInfo.pcap
分别执行直连与代理请求各一次,停止抓包。
分析方式
方法一:Wireshark 图形化分析(推荐)
-
使用 Wireshark 打开
getUserInfo.pcap
-
应用显示过滤器:
inihttp.request.method == "POST" && http.request.uri contains "getUserInfo"
-
找到两个请求包,分别右键 → "Follow → HTTP Stream" ;
-
对比两个 TCP 流的内容。
方法二:命令行快速比对
我们可以继续使用tcpdump将其转化为 Ascii 后 grep查看我们感兴趣的:
bash
tcpdump -r getUserInfo.pcap -A | grep -A 50 "getUserInfo"
输出后,使用 diff

真相浮现:Header 名称大小写差异
通过对比,可以看到两次请求的haeder发生了变化,第一个没有经过clash的流量,自定义header基本都小写 ,但经过第二个之后,就变成首字母大写。消息体、URL、其他标准头部均完全一致 进一步验证:使用 curl
手动修改这些头的大小写测试:

注意图中的 Part
这个header被我可以修改成大写了,这就是罪魁祸首!
返回值也出现了"参数错误",与代理行为一致。
结论:后端服务对 HTTP Header 名称进行了大小写敏感匹配,违反了 RFC 7230 规范。
协议标准:HTTP Header 名称必须不区分大小写
"Each header field consists of a case-insensitive field name followed by a colon (':')..."
即:HTTP Header 字段名是大小写不敏感的(case-insensitive) 。
这意味着,以下所有形式在语义上等价:
dart
part
Part
pArt
一个符合规范的 HTTP 服务端实现,必须将这些视为同一头部。
深层原因:Clash 的 Go 实现与 net/http 的规范化行为
Clash使用 Go 语言编写,其 HTTP 处理基于标准库 net/http
。
这是因为当流量经过Clash之后,底层会重新将HTTP Header 规范化 CanonicalHeaderKey
CanonicalHeaderKey returns the canonical format of the header keys. The canonicalization converts the first letter and any letter following a hyphen to upper case; the rest are converted to lowercase. For example, the canonical key for "accept-encoding" is "Accept-Encoding". If s contains a space or invalid header field bytes, it is returned without modifications.
在本例中,Clash 刚好把我们的小写 part
-> Part
。
Clash 的"规范化"是 Bug 吗?
严格来说,不是 Bug。
Go 的 net/http
包在处理 Header 时,默认使用 CanonicalHeaderKey
进行规范化(如 Content-Type
),这是语言标准库的设计选择。但在反向代理场景中,某些实现(包括 Clash 的部分模块)可能选择统一转为小写,以兼容 HTTP/2 要求(HTTP/2 强制 Header 名为小写)。
然而,问题在于:这种"规范化"是否应作为默认行为?
在理想世界中,所有服务都遵守 RFC,Header 大小写无关紧要。但在现实世界中,大量老旧系统、私有 API、WAF 规则、日志分析脚本都依赖特定大小写格式。
此时,代理的"好意"反而成了破坏者...
相关开源项目 issue
此类问题并非孤例,社区中已有多个类似反馈:
- traefik/traefik#466:用户报告 Traefik 代理导致自定义头部大小写被规范化,后端无法识别。
- golang/go#37834:讨论是否应提供 API 以获取原始 Header 大小写,表明该问题已被 Go 社区关注。
吐槽与反思:安全 ≠ 混淆
最初我甚至怀疑:这家服务是否刻意使用非常规 Header 大小写(如 cOnTeNt-tYpE
)作为一种"混淆"手段,用以抵御自动化攻击。 但进一步排查发现,其 API 接口在其他方面存在明显安全缺陷(比如验证码可爆破),因此这一猜测基本可以排除------更可能的原因是后端实现粗糙,缺乏对 HTTP 协议的基本理解。
值得一提的是,该 API 在通信安全上采用了 AES + RSA 混合加密,这一点值得肯定。
但令人遗憾的是,在 2025 年的今天,其核心接口仍在使用明文 HTTP,未部署任何 SSL/TLS 加密。
这不仅违背了现代 Web 安全的基本实践,也使得所有认证信息、用户数据暴露在中间人攻击之下
代理的"透明性"
从设计哲学来看,一个好的代理应当是"无感"的------它应像一条透明的管道,忠实地传递字节流,而不应擅自"修正"或"规范化"请求内容。 遗憾的是,Clash 在本次场景中并未做到这一点。这也直接导致了这篇文章的产生
我在抓包过程之中,也试过iOS的另一款知名代理工具,Surge,我并没有发现这个问题~