curl --resolve 与 --connect-to 的区别:只换连接目标,不换 SNI 和 Host

curl --resolve 与 --connect-to 的区别:只换连接目标,不换 SNI 和 Host

234
234
2026-10-24 / 0 评论 / 16 阅读 / 7407 字 / 正在检测是否收录...
这篇是排查工具笔记,和 DNS 加密本身无关。站内的加密 DNS 服务在 dns.23-4.cn。

有三种场景会让你需要「连到某个特定 IP,但让 TLS 和 Host 头还是原来的域名」:

  • 换了服务器,DNS 还没切,想先在新机器上验证站点能不能正常起来;
  • 怀疑 DNS 解析到了旧 IP,想证明「问题在解析,不在服务」;
  • 同一台机器上跑了多个虚拟主机,想精确命中其中一个。

/etc/hosts 能解决第一种,但它要 root、会影响整机、还要记得改回来。curl 有两个参数是专门为这件事设计的:--resolve 和 --connect-to。它们看起来像同一件事,实际差别很关键——用错了会在证书和 SNI 上踩坑,而且报错信息完全指不到真正的原因。

下面的输出都是我在一台 Ubuntu 20.04、curl 7.68 上实际跑出来的。

一、先看手册怎么说

两条命令的 man 原文,差别一眼就能看出来。

--resolve:

--resolve <host:port:address[,address]...>
    Provide a custom address for a specific host and port pair. Using
    this, you can make the curl requests(s) use a specified address and
    prevent the otherwise normally resolved address to be used. Consider
    it a sort of /etc/hosts alternative provided on the command line. The
    port number should be the number used for the specific protocol the
    host will be used for.

--connect-to:

--connect-to <HOST1:PORT1:HOST2:PORT2>
    For a request to the given HOST1:PORT1 pair, connect to HOST2:PORT2
    instead. This option is suitable to direct requests at a specific
    server, e.g. at a specific cluster node in a cluster of servers.
    This option is only used to establish the network connection. It does
    NOT affect the hostname/port that is used for TLS/SSL (e.g. SNI,
    certificate verification) or for the application protocols. "HOST1"
    and "PORT1" may be the empty string, meaning "any host/port". "HOST2"
    and "PORT2" may also be the empty string, meaning "use the request's
    original host/port".

关键就那一句:--connect-to 只管建立网络连接,不影响 TLS/SSL 用的主机名和端口(也就是 SNI、证书校验),也不影响应用层协议。

--resolve 手册里没写这句,但它的行为是一样的——它替换的只是「域名到地址」的解析结果,SNI 和 Host 头仍然来自你请求的 URL。

一句话概括两者:

改连接目标能改端口SNIHost 头证书校验用哪个名字
--resolve✅❌(只能匹配,不能换成别的端口)不变不变URL 里的域名
--connect-to✅✅不变不变URL 里的域名
裸 IP 访问✅✅变成 IP变成 IPIP,几乎必然失败

二、--resolve:命令行版的 /etc/hosts

先看基准。不加任何参数:

curl -sS -o /dev/null -w 'code=%{http_code} ip=%{remote_ip}\n' https://www.23-4.cn/

实测:

code=200 ip=240e:95d:3020:43::4006

注意 remote_ip 是个 IPv6 地址。这台机器同时有 A 和 AAAA 记录,curl 优先选了 IPv6。这本身就是个坑——你想测 IPv4 那台的时候,可能一直在打 IPv6。

现在强制走 IPv4:

curl -sS -o /dev/null -w 'code=%{http_code} ip=%{remote_ip}\n' \
  --resolve www.23-4.cn:443:160.202.238.204 \
  https://www.23-4.cn/

实测:

code=200 ip=160.202.238.204

remote_ip 变成了 160.202.238.204,状态码还是 200。说明:连接确实打到了指定 IP,而 TLS 握手、SNI、Host 头全都还是 www.23-4.cn——所以服务端正常返回了这个站点的内容。

--resolve 的格式是 host:port:address,端口必须写,而且必须和 URL 里的端口一致(https 就是 443)。它匹配的是「主机 + 端口」这个组合,所以同一个主机不同端口要写多条:

curl --resolve www.example.com:443:10.0.0.1 \
     --resolve www.example.com:8443:10.0.0.2 \
     https://www.example.com/

三、--connect-to:能换端口,这是它真正的价值

--resolve 不能表达「连到 443 但从 9999 端出去」。--connect-to 可以,格式是 HOST1:PORT1:HOST2:PORT2。

先验证端口真的生效——把它指到一个没人监听的端口:

curl -sS -o /dev/null -w 'code=%{http_code}\n' \
  --connect-to www.23-4.cn:443:160.202.238.204:9999 \
  https://www.23-4.cn/

实测:

curl: (7) Failed to connect to 160.202.238.204 port 9999: Connection refused
code=000

报错里明确是 port 9999。这就证明了 PORT2 被真正用上了,不是摆设。

同样的思路可以证明 --resolve 真的改了目标 IP。故意指一个不可达地址:

curl -sS -o /dev/null -w 'code=%{http_code}\n' \
  --resolve www.23-4.cn:443:192.0.2.1 \
  https://www.23-4.cn/

实测:

curl: (28) Connection timed out after 6001 milliseconds
code=000

192.0.2.1 是 RFC 5737 保留的文档用地址,永远不可达。超时说明 curl 真的去连它了,而不是忽略了参数。

四、最容易踩的坑:SNI 不会跟着变

这是两个参数最反直觉的地方,也是它们的设计意图。

curl -sS -o /dev/null -w 'code=%{http_code}\n' \
  --resolve example.com:443:160.202.238.204 \
  https://example.com/

实测:

curl: (60) SSL: no alternative certificate subject name matches target host name 'example.com'

TLS 握手阶段就失败了。因为 SNI 发出去的是 example.com,而对方服务器出示的证书是 *.23-4.cn:

timeout 8 openssl s_client -connect www.23-4.cn:443 -servername www.23-4.cn </dev/null 2>/dev/null \
  | openssl x509 -noout -subject -ext subjectAltName

实测:

subject=CN = *.23-4.cn
X509v3 Subject Alternative Name:
    DNS:*.23-4.cn, DNS:23-4.cn

example.com 不在这个证书的 SAN 里,校验就过不去。

这个「失败」恰恰证明参数工作正常:--resolve 只换了「连哪里」,没换「我是谁」。如果它把 SNI 也一起换了,你反而失去了用它验证真实站点的能力——因为那样测的就不是你的配置,而是另一个域名的配置了。

想看这个错配后面的真实响应,加 -k:

curl -sS -o /dev/null -w 'code=%{http_code}\n' -k \
  --resolve example.com:443:160.202.238.204 \
  https://example.com/

实测:

code=301

301 是因为请求落到了默认虚拟主机,被重定向走了。这就是「Host 不匹配」的典型表现:TLS 能过(因为 -k 跳过了校验),但 nginx 找不到对应的 server_name,于是交给默认 server 处理。

五、和「直接写裸 IP」有什么区别

很多人图省事直接写 IP:

curl -sS -o /dev/null -w 'code=%{http_code}\n' -k https://160.202.238.204/

实测:

code=301

看起来和上面一样,但两者不是一回事:

--resolve / --connect-to裸 IP
连接目标指定 IP就是这个 IP
SNIwww.23-4.cn无(或 IP)
Host 头www.23-4.cn160.202.238.204
命中哪个 server_name正确的那个默认 server
证书校验正常通过必然失败,只能 -k

用裸 IP 测出来的结果不能代表真实用户拿到的结果。它走的是默认虚拟主机,可能是另一个站、可能被重定向、可能直接 404。用它来判断「新机器部署好了没」会得到完全错误的结论。

这也是为什么我在 nginx 配置里留了这么一段注释:

# curl --resolve dns.23-4.cn:443:160.202.238.204 ... /dns-query   -> 200 ✓
# curl https://160.202.238.204/dns-query(裸 IP、无 SNI)        -> 301 ✗
# curl --resolve foo.23-4.cn:443:160.202.238.204 ... /dns-query  -> 301 ✗

第二条是裸 IP,第三条是证书能过但 server_name 不存在的子域名——注意 foo.23-4.cn 是能通过证书校验的(*.23-4.cn 覆盖它),所以它返回 301 而不是 TLS 错误。这两种 301 长得一样,原因完全不同。

六、什么时候用哪个

场景用哪个理由
验证新机器上的站点能否正常响应--resolve不需要改端口,最直观
想测某个非标准端口上的服务--connect-to唯一能改端口的
复现「DNS 解析到了旧 IP」--resolve精确指定旧 IP
测内网某台机器上的同域名服务--connect-to常常端口也不一样(比如 8080)
排查「是不是 SNI 的问题」两者都行 + openssl s_client -servernamecurl 不方便单独控制 SNI
想故意构造 Host 不匹配裸 IP 或改 -H 'Host:'这两个参数做不到

如果只是想让整台机器临时认某个 IP,用 --resolve 就够,别去改 /etc/hosts——改完忘了删,是另一类故障的开头。

七、验证你到底连到了哪

排查时最容易犯的错是:以为参数生效了,其实打到了别的地方。用 -w 把连接信息打出来,别靠猜。

curl -sS -o /dev/null \
  -w 'http=%{http_code} ip=%{remote_ip} port=%{remote_port} time=%{time_total}\n' \
  --resolve www.23-4.cn:443:160.202.238.204 \
  https://www.23-4.cn/

%{remote_ip} 是关键字段——它告诉你实际连到哪个地址。配合 %{remote_port} 可以确认端口,%{time_connect} 能看出握手耗时是否异常。

想看得更细,用 -v,重点看这三行:

* Connected to www.23-4.cn (160.202.238.204) port 443 (#0)
* TLSv1.3 (OUT), TLS handshake, Client hello (1)
* Server certificate:
*  subject: CN = *.23-4.cn

第一行证明「连到哪个 IP、哪个端口」,第二、三行证明「TLS 是跟谁握手的」。如果 Connected to 后面的 IP 不是你指定的那个,说明参数没生效(最常见原因是 port 写错,比如 URL 是 https:// 但 --resolve 里写了 :80)。

踩坑点汇总:

  1. --resolve 的端口必须和 URL 一致。https:// 对应 443,写成 80 会静默失效——curl 找不到匹配项,就退回正常解析,你还以为参数起作用了。
  2. IPv6 优先。目标站点同时有 A/AAAA 时,不加 --resolve 可能一直在打 IPv6,看起来「换了机器没用」。加 -4 或 --resolve 强制指定。
  3. --connect-to 的空字符串表示「任意」或「沿用原值」。--connect-to ::10.0.0.1:8080 表示「所有主机所有端口都连到 10.0.0.1:8080」——调试方便,但很容易连错自己都不知道。
  4. 两者都不改变 Host 头。要测「Host 头不对会怎样」,得用 -H 'Host: other.example.com',那是另一条路径。

相关阅读

0个赞
取消