应用:把线上域名在本机解析到 dev server
前面各页把 DNS 当作一套全球一致的公共设施来讲。本页反过来用它:让同一个域名在一台机器上解析出与全世界不同的答案,从而把线上域名指向本机的 dev server。这件事在 DNS 里不需要任何特殊机制,因为「谁是权威」本就是逐级委任出来的,而客户端选择问谁则完全自由(见 §3 的分流层)。
动机来自两类单凭 localhost 无法满足的场景,共同点是校验方拿域名本身当身份。第三方登录的 callback 域名在开发者后台是固定配置的,例如 https://x181.cn/auth/callback;登录完成后第三方跳回该域名,本地要走通整条流程,就必须让这个域名在本机指向 dev server。passkey 与 WebAuthn 的
rpId(Relying Party ID)必须是当前源的可注册域名后缀,例如 app.example.com 上的页面可以用 example.com 作 rpId,但不能用别人的域名。localhost 本身属安全上下文、WebAuthn 在它上面可用,复现不出线上的原因不是缺 HTTPS,而是 rpId 只能取
localhost。
两者的诉求合起来是一句话:线上域名 + HTTPS,指向本地 dev server。下面分三块落地它——解析、证书、反代。
1 · 三件工具与三处配置
/etc/resolver 决定「查询发给谁」,dnsmasq 决定「返回什么」。DNS 这一侧是两层,缺一不可。前者决定查询发给谁,后者决定返回什么:
注 · 两层各自缺失时的表现是一样的,但原因不同。缺 /etc/resolver/x181.cn,macOS 不会把查询发给 dnsmasq,走默认上游解析到公网 IP,dnsmasq 里那条规则根本不会被命中;缺 dnsmasq 的 address=,查询到达了 dnsmasq 但无匹配规则,被转发上游,同样返回公网 IP。*.test
是特例:/etc/resolver/test 加 address=/.test/127.0.0.1 已整体覆盖,新建 *.test 域名无需再配 DNS。
2 · 一次页面请求的完整路径
值得单独记一笔的是最后一步那道 Host 校验。vite 拒绝非白名单的 Host,防的是 DNS rebinding:若不校验,任何恶意网页都能把自己的域名解析到 127.0.0.1,借访客的浏览器去访问其本机的 dev server,再把响应读回自己的服务器。这道防线的存在恰好说明「域名 →
本机」这条路对谁都开放——包括攻击者,所以自定义域名必须显式写进 server.allowedHosts。
警示 · 上述链路成立的前提是浏览器使用系统 resolver。部分浏览器(国内的 QQ 浏览器、360、UC 等尤甚)内置自有 DNS 或走 HTTPDNS,直接绕过系统 resolver,此时 /etc/resolver 加 dnsmasq 这条链完全不生效,x181.cn 会解析到公网。Chrome / Firefox 的 Secure DNS(即第 8 讲的
DoH)同理——它把解析交给了远端服务器,本机的分流规则自然管不着。调试时改用遵循系统 DNS 的浏览器,或在设置里关掉「安全 DNS / 网络加速」之类选项。
3 · 这是 split-horizon DNS 的最小实例
同一个名字对不同提问者给出不同答案,业内叫 split-horizon DNS(也叫 split-view)。企业内网最常见的形态是:内部解析器对 www.corp.example 应答内网地址,公网权威服务器对同一名字应答公网地址。
DNS 协议本身不提供这个能力,也不需要提供——它是「客户端可以自由选择问谁」这一自由度的直接后果。前面讲的委任链只回答了「谁有资格声称对某个名字有权威」,没有规定「客户端必须去问那条链」。/etc/resolver 做的就是改写这个选择:把 x181.cn 这一个后缀的查询发给
127.0.0.1,于是对本机而言,dnsmasq 就是 x181.cn 的权威服务器。
这也划出了它的代价边界。既然答案只在本机成立,那么任何不经过本机 resolver 的解析都看不到它:容器内的进程有自己的 /etc/resolv.conf、Node 的 dns.lookup 走系统解析而 dns.resolve 直接问上游、上一节那些自带 DNS 的浏览器更是完全绕开。排查「浏览器能开但 curl
不行」这类症状时,第一件事是确认这次解析到底经过了哪一层。
4 · 证书:为什么本地 HTTPS 需要自建 CA
浏览器对 HTTPS 会校验证书链,而本地域名拿不到公共 CA 签发。mkcert 的做法是自建一个本地根 CA、让各客户端信任它,再用它签发每个域名的叶子证书。
这条链与第 7 讲那条 DNSSEC 信任链形状相同、根的来历不同:DNSSEC 的根信任锚由 IANA 发布、全球一份,而本机这个根 CA 是自己生成的、只有装过它的设备认。两者都靠「父签子」传递信任,也都在换根密钥时要求所有依赖方同步更新——mkcert 换 CA 后各客户端要重新信任,正对应换 KSK 后父 zone 要更新 DS。
替代方案是 caddy 的 tls internal,用 caddy 自带 CA(需 sudo caddy trust);本页采用 mkcert,因其 CA 通常已受信、与既有证书一致。
5 · 劫持自己的域名会让 API 回环
把 x181.cn 解析到本机之后,页面里调用 https://x181.cn/api/… 就出了问题:vite 的 server.proxy 想把 API 转发到真实上游,可 x181.cn 又解析回自身,形成回环。
https.Agent,其 lookup 改用不被本地劫持的解析器;SNI 与 Host 保持不变,只换目的 IP。
解法的关键在于把「解析」与「连接」拆开。TLS 的 SNI 与 HTTP 的 Host 仍然发 x181.cn(否则证书与虚拟主机都对不上),只把目的 IP 换成另一条解析路径得到的真实上游地址。同目录的 vite-plugin-real-upstream.ts 是它的实现:dns: 'system' 取系统 DNS
并剔除回环地址——不剔的话又会问到本机 dnsmasq,二次劫持;仅当系统 DNS 全是回环时才回退公网解析器。
loc 是同目录的一个 Ruby 脚本,把上述 caddy 与 dnsmasq 两侧的开关封成 loc up <host> <port> / loc down <host>。它只动自己独占的目录(每个域名一个 .caddy 片段、dnsmasq 一个 loc.conf),撤销就是删文件加 reload。
6 · 真机访问同一域名
/etc/resolver 加 dnsmasq 只对 Mac 本机有效。手机要打开同一个域名,需要把它的解析也指向这台 Mac,并让它信任 mkcert 的根证书。
有一处容易忽略的坑:dnsmasq 返回的必须是 Mac 的局域网 IP,不能是 127.0.0.1——手机上的 127.0.0.1 是手机自己。也就是说同一条 dnsmasq 规则在两种场景下的正确答案不同,而 *.test 的通配不支持 dnsmasq 的 interface-name,所以通配域名在手机上仍只能解析到
127.0.0.1,真机测试必须用具名域名。
7 · 参考文献
- Cheshire, S., & Krochmal, M. (2013). Special-Use Domain Names (RFC 6761). IETF. (
.test/.localhost等保留名的用途与解析器应有的行为。) - Apple Inc. (2017). resolver(5) manual page. macOS。(
/etc/resolver/<domain>的按后缀 分流语义。) - Johns, M., Lekies, S., & Stock, B. (2013). Eradicating DNS rebinding with the extended same-origin policy. Proceedings of the 22nd USENIX Security Symposium, 621–636.