这篇是排查工具笔记,和 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。
一句话概括两者:
| 改连接目标 | 能改端口 | SNI | Host 头 | 证书校验用哪个名字 | |
|---|---|---|---|---|---|
--resolve | ✅ | ❌(只能匹配,不能换成别的端口) | 不变 | 不变 | URL 里的域名 |
--connect-to | ✅ | ✅ | 不变 | 不变 | URL 里的域名 |
| 裸 IP 访问 | ✅ | ✅ | 变成 IP | 变成 IP | IP,几乎必然失败 |
二、--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.204remote_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=000192.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.cnexample.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=301301 是因为请求落到了默认虚拟主机,被重定向走了。这就是「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 |
| SNI | www.23-4.cn | 无(或 IP) |
| Host 头 | www.23-4.cn | 160.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 -servername | curl 不方便单独控制 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)。
踩坑点汇总:
--resolve的端口必须和 URL 一致。https://对应 443,写成 80 会静默失效——curl 找不到匹配项,就退回正常解析,你还以为参数起作用了。- IPv6 优先。目标站点同时有 A/AAAA 时,不加
--resolve可能一直在打 IPv6,看起来「换了机器没用」。加-4或--resolve强制指定。 --connect-to的空字符串表示「任意」或「沿用原值」。--connect-to ::10.0.0.1:8080表示「所有主机所有端口都连到 10.0.0.1:8080」——调试方便,但很容易连错自己都不知道。- 两者都不改变
Host头。要测「Host 头不对会怎样」,得用-H 'Host: other.example.com',那是另一条路径。