HTTP 请求头大小写差异:一次由 Clash 代理引发的疑难杂症

问题背景

近日我因为写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 工作原理应该是前置代理,所以不可以。其他抓包工具我没有,就没有了解。

因此,必须使用直接从网络接口捕获原始数据包 的工具,如 tcpdumpWireshark

第三阶段:tcpdump 抓包与 Wireshark 分析

抓包命令

使用 tcpdump 捕获目标主机 80 端口的所有流量:

bash 复制代码
sudo tcpdump -i any port 80 -w getUserInfo.pcap

分别执行直连与代理请求各一次,停止抓包。

分析方式

方法一:Wireshark 图形化分析(推荐)

  1. 使用 Wireshark 打开 getUserInfo.pcap

  2. 应用显示过滤器:

    ini 复制代码
    http.request.method == "POST" && http.request.uri contains "getUserInfo"
  3. 找到两个请求包,分别右键 → "Follow → HTTP Stream"

  4. 对比两个 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 名称必须不区分大小写

根据 RFC 7230, Section 3.2

"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,我并没有发现这个问题~

相关推荐
奋进的电子工程师3 天前
汽车软件研发智能化:AI在CI/CD中的实践
人工智能·ci/cd·汽车·软件工程·软件构建·代码规范
大怪v3 天前
老乡,别走!Javascript隐藏功能你知道吗?
前端·javascript·代码规范
Craze_rd4 天前
Go 开发规范1
go·代码规范
小刚子要努力5 天前
基于Koa实现轻量化服务引擎
node.js·代码规范
前端很开门5 天前
程序员的逆天操作,看我如何批量下载iconfont的图标和批量下载 svg 图标
前端·chrome·代码规范
一碗清汤面5 天前
打造AI代码审查员:使用 Gemini + Git Hooks 自动化 Code Review
前端·git·代码规范
Dream耀5 天前
Promise静态方法解析:从并发控制到竞态处理
前端·javascript·代码规范
NRatel5 天前
Unity项目基本风格/规范
unity·c#·游戏引擎·代码规范·规范
围巾哥萧尘6 天前
五秒钟挑战网页开发实战🧣
代码规范