DNS、TLS 与 HTTPS:从找到地址到证明服务身份
访问一个 HTTPS 地址,要先理解地址中的名称和实际通信端点:
https://api.example.com:8443/orders?status=open
├─ https 通信方案:使用 HTTPS
├─ api.example.com 目标主机名,参与解析和证书名称校验
├─ 8443 显式端口;省略时 HTTPS 默认 443
├─ /orders 资源路径
└─ status=open 查询参数域名可以指向多个地址,一个地址也可以承载多个域名。DNS 为连接提供候选端点;TLS 在交换业务内容前验证预期服务的身份并建立加密通道;HTTP 再描述要进行的操作。
域名怎样找到服务入口
名称层级、区域与委派
DNS 名称由标签组成,从右向左表示逐级细分。完整限定域名可以显式以根的点结尾:
api.example.com.
│ │ │ └─ 根 .
│ │ └──── com 顶级域
│ └──────────── example.com 域
└──────────────── api 子域名称,常用作服务主机名“子域”描述名称之间的关系;“主机名”描述它在应用中的用途。一个子域既可以有 A/AAAA 地址记录,也可以继续包含更下层名称。www 只是常见标签,HTTP 不要求域名必须以 www 开头。
区域(zone)是由一组权威 DNS 服务器管理的数据范围。父区域可以通过 NS 记录把子区域委派给另一组权威服务器,所以一个域的整棵子树不一定由同一个区域管理。注册域名、指定权威 DNS、添加业务记录是三个操作:注册商管理注册及相关委派设置,DNS 托管方维护区域,应用部署方提供实际服务端点。
域名标签有长度和编码规则;国际化域名需要按 IDNA 规则转换为 DNS 使用的形式。对用户输入的域名,应使用平台提供的解析/转换库,不以“包含几个点”判定它是否有效或属于某个组织。DNS 概念与委派定义名称树与区域的关系,DNS 报文与记录格式给出具体编码。
常见记录类型
| 类型 | 保存什么 | 常见用途与注意点 |
|---|---|---|
| A / AAAA | IPv4 / IPv6 地址 | 同一名称可返回多条;地址顺序不是固定健康保证 |
| CNAME | 另一个规范名称 | 表示别名,解析器继续跟随;普通 CNAME 不能与同名其他数据随意并存 |
| NS | 区域权威服务器名称 | 用于委派和权威服务定位 |
| SOA | 区域管理信息、序列号和定时参数 | 辅助区域同步及负缓存处理 |
| MX | 邮件交换服务器及优先级 | 邮件路由,不决定 HTTP 请求入口 |
| TXT | 文本字段 | 域名验证、邮件策略等应用按各自规则解释 |
| PTR | 反向名称 | 地址到名称的反向查询,不替代正向身份校验 |
| CAA | 允许签发证书的 CA 策略 | 供 CA 签发检查,客户端仍需验证证书 |
| SRV | 服务目标、端口、优先级和权重 | 仅在应用协议支持时参与发现 |
| HTTPS / SVCB | 服务绑定与连接参数 | 可携带 ALPN、端口等提示,客户端支持程度不同 |
CNAME 指向名称,不能填写包含路径的 https:// URL;DNS 也不提供 HTTP 301/302 重定向。部分托管平台的 ALIAS/ANAME 或 CNAME flattening 是平台扩展,部署前应查其区域顶点、TTL 和故障行为。IANA DNS 参数表列出记录类型;HTTPS/SVCB 的连接规则见 RFC 9460。
一次解析经过哪些位置
应用
→ 本机名称服务:hosts、系统 resolver、可能的本地缓存
→ 配置的递归解析器
├─ 已有可用缓存 → 返回
└─ 需要查询 → 根的委派 → 顶级域的委派 → 目标区域权威服务器
← 地址、别名或否定结果递归解析器替客户端取得最终结果;迭代查询中,被询问的服务器可以返回下一步应询问的服务器。缓存和已有委派会省略不少查询,实际抓包不一定从根开始。操作系统的名称服务也可能先命中 hosts,使本次调用完全不发送 DNS 报文。
传统 DNS 常用 UDP 53,遇到截断等情况需要 TCP 53,TCP 也用于其他 DNS 场景。只放行 UDP 可能造成“小答案成功、大答案失败”。DoT 和 DoH 将解析器通信放进加密传输;DNSSEC 验证签名数据的来源与完整性。这几种功能解决不同问题,DNSSEC 本身不加密查询内容。DNS TCP 要求、DNSSEC 概述
容器可使用内置 DNS 解析同网络的服务名称;Kubernetes 的短服务名还受命名空间和搜索域影响。api 这样的短名会因运行位置不同而补全为不同名称。跨环境配置应明确使用哪一种名称,不把开发机 hosts 中的结果当作生产解析行为。
TTL、负缓存和已有连接
TTL 给记录缓存规定存活时间。客户端查询递归缓存时,常看到剩余 TTL,而非权威配置的原始 TTL。修改记录前降低 TTL,需要等已有旧 TTL 缓存过期;修改后的短 TTL 不能撤销已经发出去的长 TTL 答案。
不存在的名称可以返回 NXDOMAIN;名称存在但没有所查类型时,可能是 NOERROR 且无对应答案,即 NODATA。两者都可能被负缓存。新增一条记录后部分客户端仍失败,应检查负缓存及实际使用的解析器。负缓存规范
JVM 还可能缓存名称查询结果,正向、负向和陈旧缓存策略由网络安全属性及运行配置决定。InetAddress API列出相关属性。HTTP 连接池复用现有连接时可以根本不再解析;DNS 切换的效果因此还取决于连接年龄、健康检查和旧入口下线方式。
IPv4、IPv6 都可用时,客户端可以采用 Happy Eyeballs,错开尝试多个候选并使用先成功的连接。这样能减少单个地址族黑洞造成的等待,但具体客户端未必实现相同策略。RFC 8305说明受控竞速方式。逐个地址都重新分配完整超时会放大总耗时,应给整次调用设置总预算。
用真实证书完成一次 HTTPS 请求
实验环境与文件
下载 DNS/TLS 实验包,解压进入 dns-tls-https。Linux Bash 环境需要 JDK 17+、OpenSSL 3、curl 7.76+;使用普通用户,18443 仅绑定回环。源码包括:
dns-tls-https/
├─ make-certs.sh 新建本地 CA 与服务器证书
├─ TlsClient.java 用专用信任库完成 Java HTTPS 请求
├─ run.sh 自动运行成功、错误名称、错误信任三个分支
└─ README.md只有 Docker 的机器可使用网络实验工具源码构建 local/network-web-tools:1,构建命令见 TCP 实验环境。镜像安装软件时使用 root,最终运行身份为 10001;不要把这两个身份混为一谈。执行:
docker run --rm --user 10001:10001 --read-only --network none \
--cap-drop ALL --tmpfs /tmp:rw,exec,size=256m \
-v "$PWD:/src:ro" local/network-web-tools:1 bash /src/run.sh实测输出:
trustedName=200
wrongNameRejected=60 untrustedCaRejected=60
javaHttps=200 tls=TLSv1.3脚本启动真实 OpenSSL TLS 服务,curl 和 Java 都建立实际连接。它生成自己的短期 CA,不修改系统信任库;结束后关闭服务并清理自己的临时目录。若端口已被占用,脚本失败并输出监听错误,不复用来源不明的现有服务。
创建证书并启动服务
想分步观察时,在 Linux 的源码目录中执行。CERT_DIR 必须是尚不存在的新目录:
CERT_DIR="$PWD/local-certs"
bash make-certs.sh "$CERT_DIR"
openssl x509 -in "$CERT_DIR/server.pem" -noout -subject -issuer -ext subjectAltNameCA 签发的叶子证书包含 DNS:api.example.test 和 DNS:localhost 两个 SAN,用途为 serverAuth。输出应显示这两个名称,以及叶子签发者 Local teaching CA。脚本采用 umask 077 限制新文件权限,私钥只用于本机实验;已存在的目录会被拒绝,防止误覆盖。
openssl s_server -accept 127.0.0.1:18443 \
-cert "$CERT_DIR/server.pem" -key "$CERT_DIR/server.key" \
-www -quiet此终端保持运行,另开终端进入相同源码目录并重新设置 CERT_DIR。openssl s_server 的 -www 提供简单测试响应,不是业务 Web 服务器。s_server 手册解释监听和证书选项,x509 手册解释证书查看方式。
CERT_DIR="$PWD/local-certs"
curl -q --noproxy '*' --fail-with-body \
--connect-timeout 2 --max-time 5 \
--cacert "$CERT_DIR/ca.pem" \
--resolve api.example.test:18443:127.0.0.1 \
-o /dev/null -w 'http=%{http_code} peer=%{remote_ip}\n' \
https://api.example.test:18443/结果为 http=200 peer=127.0.0.1。--resolve 为指定的主机名和端口提供连接地址,URL 中的 api.example.test 仍用于 HTTPS 名称校验及相应的 SNI/HTTP 主机信息;--cacert 只为这个调用指定信任根。curl 手册分别定义地址覆盖、代理和证书选项。
-q 必须放在 curl 后第一个选项位置,避免默认 curlrc 干扰;--noproxy '*' 明确排除代理。企业网络若禁止直连,不应照搬这个对照方法绕过网络规定,应在允许的测试环境中运行,或按代理的实际连接与解析行为重新设计验证。
分别制造名称错误和信任错误
保持服务端证书、端口与地址不变,只把 URL 名称改成证书未包含的名称:
curl -q --noproxy '*' --connect-timeout 2 --max-time 5 \
--cacert "$CERT_DIR/ca.pem" \
--resolve wrong.example.test:18443:127.0.0.1 \
https://wrong.example.test:18443/
printf 'exit=%s\n' "$?"curl 应以 60 退出,错误说明名称不匹配。若开启了 set -e,需要像 run.sh 一样暂时保存失败码,不能让预期负例中断全部检查。
再恢复正确名称,移除 --cacert:此临时 CA 没有进入系统信任库,仍应以 60 退出,但原因是证书链无法连接到受信任颁发者。相同退出码可以对应不同验证失败,必须同时读取错误信息。自动脚本不仅检查数字,还分别检查名称与签发者错误线索。
修复方法是使用正确服务名,并向客户端提供经过核验的信任链。-k、信任所有证书的 TrustManager、关闭 hostname verifier 都会移除重要检查,不属于证书修复。
TlsClient.java 把实验 CA 放入一个独立 KeyStore,再用标准 TrustManagerFactory 初始化 SSLContext。Java HttpClient 保留默认 HTTPS 名称校验,访问证书包含的 localhost;没有修改全局 cacerts,也没有覆盖成“总是成功”的验证器。JSSE 指南解释密钥管理和信任管理。实际应用还要检查所用客户端库是否启用了正确的 endpoint identification。
分步实验结束后,在 s_server 终端按 Ctrl+C。local-certs 含实验私钥,不加入仓库、不复制到生产;确认目录正是本次生成目录后删除。需要再次运行时重新生成,不沿用过期测试证书。
TLS 协商与服务身份验证
TLS 1.3 的主要交换
ClientHello
版本、密码套件、密钥份额、SNI、ALPN 候选
↓
ServerHello
选定参数、服务端密钥份额
↓
加密的后续握手
EncryptedExtensions
Certificate / CertificateVerify(证书认证场景)
Finished
↓
客户端校验并发送 Finished
↓
应用流量密钥保护 HTTP 数据这是常见的完整、服务端证书认证握手。会话恢复、HelloRetryRequest、客户端证书认证会改变具体消息;不能要求每次抓包都出现完整证书链。TLS 1.3 使用密钥协商导出握手及应用流量密钥,证书私钥用于相应的认证证明,并非直接拿它加解密每个 HTTP 字节。TLS 1.3 规范
SNI 用于告诉服务器希望访问的名称,以便同一地址选择证书和站点。ALPN 协商应用协议:常规 TLS/TCP 的 HTTP 可以选择 h2 或 http/1.1;HTTP/3 的 h3 在 QUIC 中协商,不能直接在普通 TCP TLS 连接上启用。ALPN 规范
TLS 1.3 的服务端证书消息已加密,但普通 ClientHello 中的名称信息未必隐藏;是否保护它取决于客户端、DNS 配置和服务器是否支持相应的加密 ClientHello 部署。HTTPS 也不会隐藏所有 IP 地址、时序与流量大小。
校验的输入来自哪里
| 输入 | 验证什么 |
|---|---|
| 客户端原本要访问的服务名 | 与叶子证书 SAN 的 DNS-ID 或 IP-ID 匹配 |
| 服务器提供的叶子和中间证书 | 构建有效认证链,验证签名及证书约束 |
| 客户端信任库 | 决定接受哪些根或配置的信任锚 |
| 当前系统时间 | 检查证书有效期 |
| 应用和平台策略 | 检查用途、算法、吊销及其他要求 |
DNS CNAME 的目标名、反向 PTR 名称不能随意替换原始服务名作为校验依据。用 IP 字面量访问时,通常需要匹配证书中的 IP SAN;仅包含某个 DNS 名称的证书不会因此对该 IP 自动有效。通配符也不是任意后缀匹配,通常只匹配最左侧的一个标签。TLS 服务身份规则
服务端一般应发送叶子及必要中间证书,根由客户端预先信任。开发机可能缓存中间证书,导致本机成功而干净容器失败;应从真实入口检查完整链,而不只在配置目录里看单个 PEM 文件。
mTLS 还要求客户端提供证书,使服务器验证调用方身份。证书中的身份仍需映射到账号、服务和租户权限:拥有合法工作负载证书的服务,不应因此获得全部订单的访问权。
恢复、重放与终止位置
TLS 会话恢复能减少握手成本。TLS 1.3 的 0-RTT 提前数据有重放风险,接口必须判断重复接收是否安全;扣款、创建订单等副作用请求不应只因为启用 HTTPS 就允许提前重放。HTTP 提前数据说明相关处理。
浏览器 ── TLS A ── 负载均衡器 ── TLS B 或受控明文 ── 应用
↑
TLS A 在此终止TLS A 保护到负载均衡器这一段。后端段是否加密、如何认证,需要独立配置;若再次 TLS,还要为 TLS B 校验证书和服务名。原始 scheme、主机和源地址往往由代理头传递,只能信任受控入口清洗后注入的值,具体处理见 状态、流式通信与代理。
证书轮换应先让验证方接受新链,再替换服务端证书,观察真实入口后撤销旧信任。只更新磁盘文件可能没有触发所有进程重新加载;检测应覆盖实际 SNI、区域、备用入口和剩余有效期。握手容量也要单独限制与观察,新连接风暴可能在进入 HTTP 之前耗尽 CPU。
分开检查解析、连接与握手
getent 与 dig 查询的对象不同
在出现故障的 Linux 宿主或容器中执行,NAME 替换为允许查询的完整名称:
NAME=example.com
getent ahosts "$NAME"
dig "$NAME" A +noall +comments +answer
dig "$NAME" AAAA +noall +comments +answer
dig "$NAME" SOA +noall +comments +answergetent 走系统名称服务配置,可以受 hosts 和 NSS 影响;dig 发起 DNS 查询,通常不使用 hosts。二者结果不同,应先检查解析路径,而非直接认定其中一个错误。浏览器、JVM 或自带 DoH 的客户端还可能采用另一条路径。BIND dig 文档
dig 的答案行依次包含名称、TTL、类别、类型和数据。重点读响应码及记录内容,不要求另一台机器返回相同 IP、TTL 或记录顺序。企业分流和本地代理也可能返回专用地址,不能把当前递归答案直接写成权威服务器的原始记录。
需要指定解析器时,RESOLVER 填组织允许查询的递归服务器 IP:
RESOLVER=192.0.2.53
dig @"$RESOLVER" "$NAME" A +time=2 +tries=1192.0.2.53 是文档占位地址,必须替换。若排查委派,先取得目标区域的 NS,再在网络允许时向对应权威地址查询;+trace 会直接迭代查询外部服务器,受出口策略限制,不作为所有企业环境都能执行的必经步骤。
保留主机名,只对照连接地址
普通请求与 --resolve 请求应保持 URL、证书信任、方法和代理路径一致。前面的回环实验已展示受控直连方式。在有授权的服务上,对照“正常解析”与“指定某个已知入口”可以缩小候选范围;指定地址成功只说明该地址的这次连接和请求可用,仍需检查普通解析返回的各地址、地址族及客户端选择行为。
curl 可记录各阶段累计时间:
curl -q --noproxy '*' --fail-with-body \
--connect-timeout 3 --max-time 10 -o /dev/null \
-w 'dns=%{time_namelookup} connect=%{time_connect} tls=%{time_appconnect} first=%{time_starttransfer} total=%{time_total}\n' \
https://example.com/这个公网检查仅在允许直连的环境使用;不得借参数绕开必须经过的代理。输出中的时间从调用开始累计,time_appconnect 减 time_connect 才可近似看本次 TLS 段耗时。连接复用、重定向和代理隧道会改变解释,不能无条件把每个字段当作独立阶段时长。
TLS 错误与后续动作
| 线索 | 检查对象 | 修复后的验证 |
|---|---|---|
| NXDOMAIN / UnknownHostException | 完整名称、搜索域、解析器和负缓存 | 在相同应用运行位置重新解析 |
| connect refused / timeout | 候选地址、地址族、端口、路由与监听 | 先恢复连接,再检查 TLS |
| unable to get local issuer | 中间链与客户端信任库 | 干净客户端执行同名握手 |
| hostname mismatch | URL 名称、SNI 站点和证书 SAN | 保留验证并改正确名称或证书 |
| expired / not yet valid | 入口实际证书及客户端时钟 | 轮换或修时钟,再测真实入口 |
| handshake failure | 协议、算法、mTLS 要求和服务日志 | 对照双方支持的策略,避免全部降级 |
| TLS 成功但 HTTP 403 | 应用身份与授权规则 | 检查主体、资源与权限,不更换 CA |
证书查看需要明确验证名称。对本地实验可以运行以下命令,CERT_DIR 沿用上面的目录:
openssl s_client -connect 127.0.0.1:18443 \
-servername api.example.test \
-verify_hostname api.example.test \
-CAfile "$CERT_DIR/ca.pem" -verify_return_error -brief </dev/null应看到验证成功以及实际 TLS 参数。仅设置 -servername 会发送 SNI,不能代替 -verify_hostname;-verify_return_error 使验证错误真正导致失败。命令依赖分步启动的 s_server,自动 run.sh 结束后端口已经关闭。s_client 手册
排障日志保留查询名、选中地址、连接错误和验证原因即可。不要把私钥、会话票据、完整 Authorization 或 Cookie 放进截图和公开工单。
权威资料与规范地址
按协议、系统调用与 Java API 查阅完整定义。
