
同一个 API,为什么系统 curl 失败而 Homebrew curl 成功?
本文针对在国内云服务器(阿里云、腾讯云等)上部署 Web 服务时遇到的 HTTPS 连接问题,探寻 “同一个 API,为什么系统 curl 失败而 Homebrew curl 成功?”。
适用场景:本文针对在国内云服务器(阿里云、腾讯云等)上部署 Web 服务时遇到的 HTTPS 连接问题。如果你的服务部署在海外服务器,不会遇到本文描述的问题。
问题现象
在阿里云服务器上部署了一个 API 服务,使用 Caddy 作为反向代理,绑定域名 api.example.com。服务在本地测试一切正常,但从外部访问时出现诡异现象:
同样是 curl 命令,为什么有的成功有的失败?这个问题困扰了我好几个小时。
同样是 curl 命令,为什么有的成功有的失败?这个问题困扰了我好几个小时。
排查过程
第一阶段:怀疑代理问题
最初注意到失败的请求解析到了 198.18.x.x(代理软件的虚拟 IP 段),而成功的请求也解析到类似地址。查看 Clash 连接日志后发现:
1 2 3 4两个 curl 请求: - 规则:DOMAIN-SUFFIX - 代理链:DIRECT(直连) - 远程 IP:47.xx.xx.xx(相同)
两个请求的代理配置完全一致,都是直连到同一个 IP,排除代理路由问题。
第二阶段:怀疑 TLS 指纹
既然代理配置相同,区别只能在 curl 本身。对比两个版本:
- 系统 curl:使用 LibreSSL / Secure Transport
- Homebrew curl:使用 OpenSSL + Apple SecTrust
难道是阿里云对某些 TLS Client Hello 特征进行过滤?这个方向看似合理,但缺乏直接证据。
第三阶段:检查服务端配置
登录阿里云服务器,逐一排查:
1 2 3 4# 1. 安全组 - 所有端口已开放 # 2. 防火墙 - iptables INPUT/OUTPUT 都是 ACCEPT,ufw inactive # 3. Caddy 本地测试 - 完全正常 curl -v https://api.example.com/api/v1/test # ✅ 成功
服务端配置没有问题,Caddy 和后端服务都运行正常。
第四阶段:tcpdump 抓包(关键突破)
在 Caddy 服务器上抓包,同时从海外服务器发起请求:
1tcpdump -i eth0 host <海外服务器IP> and port 443 -nn
抓包结果揭示了关键信息:
1 2 3 4 5 613:40:29.944362 [S] → TCP SYN 13:40:29.944408 [S.] ← SYN-ACK 13:40:30.112387 [.] → ACK(三次握手完成) 13:40:30.116445 [P.] → 517 字节(TLS Client Hello) 13:40:30.116465 [.] ← ACK(Caddy 确认收到) 13:40:30.116469 [R] → RST !!!
关键发现:RST 包在 ACK 之后仅 4 微秒就到达了。从海外服务器到国内的网络延迟至少几十毫秒,4 微秒内返回 RST 是不可能的——这说明 RST 是阿里云边缘设备发出的,而非真正的服务器。
第五阶段:HTTP 测试(真相大白)
既然 HTTPS 在 TLS 层被拦截,那用 HTTP 绕过 TLS 试试:
1curl -v http://api.example.com/api/v1/test
返回结果:
1 2 3 4 5 6 7 8 9 10HTTP/1.1 403 Forbidden Server: Beaver <title>Non-compliance ICP Filing</title> <script> window.onload = function () { document.getElementById("mainFrame").src = "http://www.aliyun.com/beian/beian-block?id=..."; } </script>
真相大白:返回了 ICP 备案拦截页面。问题根本不是 TLS 指纹,而是域名没有完成 ICP 备案。
根因分析
阿里云的 ICP 备案检测机制
阿里云对托管在国内服务器上的网站强制要求 ICP 备案。其边缘安全设施会在流量到达服务器前进行检测:
1请求 → 阿里云边缘设备 → 检测域名备案状态 → 已备案:放行 / 未备案:拦截
拦截策略的差异
为什么不同来源表现不同?
阿里云对海外 IP 访问未备案域名的检测最为严格,几乎 100% 拦截。对国内 IP 的检测相对宽松,但某些 TLS 指纹仍可能触发拦截。
回到最初的问题:为什么两个 curl 表现不同?
通过 Clash 日志确认,两个 curl 的代理配置完全一致——都是 DIRECT 直连,都连接到同一个服务器 IP。那为什么一个成功一个失败?
答案在于 TLS 实现的差异:
不同的 TLS 库在发送 Client Hello 时,会有不同的:
- Cipher Suites 顺序
- TLS 扩展列表
- 支持的椭圆曲线
阿里云的边缘检测设备会分析这些特征。对于未备案域名,某些 TLS 指纹(特别是看起来像"脚本/爬虫"的特征)更容易触发拦截,而看起来像"正常浏览器"的指纹则可能被放行。
这也解释了为什么问题表现得如此诡异——同一台电脑、同一个网络、同一个目标,仅仅因为 curl 版本不同就产生了截然不同的结果。本质上,这是 ICP 备案检测 + TLS 指纹识别共同作用的结果。
解决方案
将域名换成已完成 ICP 备案的域名,重新配置 Caddy 后,所有 curl 版本都能正常访问:
1 2 3 4 5 6 7 8# 系统 curl - 之前失败,现在成功 /usr/bin/curl -v https://api.example.cn/api/v1/test # ✅ # Homebrew curl - 一直成功 /opt/homebrew/opt/curl/bin/curl -v https://api.example.cn/api/v1/test # ✅ # 海外服务器 - 之前失败,现在成功 curl -v https://api.example.cn/api/v1/test # ✅
经验总结
1. 国内服务器部署的铁律
在国内服务器上提供 Web 服务,域名必须完成 ICP 备案。 这不是建议,是强制要求。
2. ICP 拦截问题的排查方法
1 2 3 4 5 6 7 8 9 10 11# 方法一:HTTP 测试(最直接) curl -v http://your-domain.com/api/test # 如果返回 403 + 备案页面 → 确认是 ICP 问题 # 方法二:tcpdump 抓包 tcpdump -i eth0 port 443 -nn # 如果 TLS 握手后几微秒内收到 RST → 边缘设备拦截 # 方法三:openssl 测试 openssl s_client -connect your-domain.com:443 -servername your-domain.com # 如果 "SSL handshake has read 0 bytes" → 服务端未响应
3. 部署位置选择原则
个人使用情况总结如上,不一定适合所有。
4. 避免踩坑的建议
- 域名规划:提前准备已备案域名用于国内服务
- DNS 选择:国内服务使用国内 DNS 服务商(阿里云解析等)
- 测试覆盖:上线前从不同网络环境(国内/海外、不同运营商)测试访问
写在最后
这次排查走了不少弯路,从代理配置到 TLS 指纹,最后才发现是 ICP 备案这个"基础设施级"的问题。
回过头看,HTTP 测试是最简单直接的排查方法——绕过 TLS 直接暴露真正的拦截原因。而 tcpdump 抓包则提供了底层视角,4 微秒返回 RST 这个细节直接指向了"边缘设备拦截"的结论。
技术问题的排查就像侦探破案,证据链完整了,真相自然浮出水面。



