之前回答了知乎上的一个问题 以子域名的形式使用localhost,有什么坑吗?,本文是该回答的备份。
一开始的回答:
localhost
本身只是一个主机名,约定俗成指向回环地址 127.0.0.1
和 ::1
,如果你新配的主机名不叫 localhost
,那其实叫什么都无所谓,可以是 a.localhost
、b.localhost
,也可以是 a.local
、b.local
,具体取决于你本机的 hosts 和 DNS 配置。
后来发现,我在没有修改 hosts 的情况下,竟然也能用 Chrome 访问 foo.localhost
,事情并没有想象中简单。
排查 DNS
首先借助 dig
命令验证是不是 DNS 做了处理。
C:\Users\keqingrong>dig foo.localhost +nocomments +nocmd
; <<>> DiG 9.14.0 <<>> foo.localhost +nocomments +nocmd
;; global options: +cmd
;foo.localhost. IN A
foo.localhost. 9247 IN A 127.0.0.1
;; Query time: 5 msec
;; SERVER: 192.168.199.1#53(192.168.199.1)
;; WHEN: Tue Sep 28 22:47:51 ;; MSG SIZE rcvd: 56
192.168.199.1
是极路由的 DNS 地址,确实返回了 127.0.0.1
。不过因为路由器会自动获取运营商分配的 DNS,所以还需要进一步验证,比如江苏电信的 DNS。
C:\Users\keqingrong>dig @218.2.2.2 foo.localhost +nocomments +nocmd
; <<>> DiG 9.14.0 <<>> @218.2.2.2 foo.localhost +nocomments +nocmd
; (1 server found)
;; global options: +cmd
;foo.localhost. IN A
foo.localhost. 9967 IN A 127.0.0.1
;; Query time: 5 msec
;; SERVER: 218.2.2.2#53(218.2.2.2)
;; WHEN: Tue Sep 28 22:51:14 ;; MSG SIZE rcvd: 45
由此可见是江苏电信的 DNS 对 foo.localhost
做了解析,所有的 DNS 都会这样吗?让我们再验证其他公共 DNS 的行为,比如 Cloudflare DNS。
C:\Users\keqingrong>dig @1.1.1.1 foo.localhost +nocomments +nocmd
; <<>> DiG 9.14.0 <<>> @1.1.1.1 foo.localhost +nocomments +nocmd
; (1 server found)
;; global options: +cmd
;foo.localhost. IN A
. 86400 IN SOA a.root-servers.net. nstld.verisign-grs.com. 2021092800 1800 900 604800 86400
;; Query time: 174 msec
;; SERVER: 1.1.1.1#53(1.1.1.1)
;; WHEN: Tue Sep 28 22:54:51 ;; MSG SIZE rcvd: 115
Cloudflare DNS 只返回了一条 SOA 记录,看来并不是所有 DNS 都会将 foo.localhost
解析到 127.0.0.1
。
SOA 记录: 起始授权机构(Start Of Authority),用于标识 NS 记录中的主服务器
在 macOS 上测试的结果和 Windows 一致。
$ dig foo.localhost +nocomments +nocmd
;foo.localhost. IN A
foo.localhost. 8473 IN A 127.0.0.1
;; Query time: 2 msec
;; SERVER: 192.168.199.1#53(192.168.199.1)
;; WHEN: 二 9 28 23:00:42 CST 2021
;; MSG SIZE rcvd: 56
如果断网,本机将无法顺利解析 foo.localhost
,使用 dig
或 ping
命令获取不到回环地址 IP,因为依赖上游 DNS。但是此时浏览器依然顽强,可以正常访问 http://foo.localhost/
,一定是浏览器做了特殊处理。
像
curl
只处理了localhost
,其他域名依赖 DNS 解析,因此不支持foo.localhost
,见 document the new 'localhost' treatment。
排查浏览器
查阅 Chromium 代码,找到了定义于 net/base/url_util.cc
的 IsLocalhost()
函数,localhost
和以 .localhost
结尾的 host
都视作本地回环地址的主机名:
// https://github.com/chromium/chromium/blob/ef3c0b7e3f9387e57570cdfd6c7e65ee5add4ec9/net/base/url_util.cc#L388
bool IsLocalhost(const GURL& url) {
return HostStringIsLocalhost(url.HostNoBracketsPiece());
}
bool HostStringIsLocalhost(base::StringPiece host) {
IPAddress ip_address;
if (ip_address.AssignFromIPLiteral(host))
return ip_address.IsLoopback();
return IsLocalHostname(host);
}
bool IsLocalHostname(base::StringPiece host) {
std::string normalized_host = base::ToLowerASCII(host);
// Remove any trailing '.'.
if (!normalized_host.empty() && *normalized_host.rbegin() == '.')
normalized_host.resize(normalized_host.size() - 1);
return normalized_host == "localhost" ||
IsNormalizedLocalhostTLD(normalized_host);
}
bool IsNormalizedLocalhostTLD(const std::string& host) {
return base::EndsWith(host, ".localhost");
}
相应的的单元测试:
// https://github.com/chromium/chromium/blob/ef3c0b7e3f9387e57570cdfd6c7e65ee5add4ec9/net/base/url_util_unittest.cc#L447
TEST(UrlUtilTest, IsLocalhost) {
EXPECT_TRUE(HostStringIsLocalhost("localhost"));
EXPECT_TRUE(HostStringIsLocalhost("localHosT"));
EXPECT_TRUE(HostStringIsLocalhost("localhost."));
EXPECT_TRUE(HostStringIsLocalhost("localHost."));
EXPECT_TRUE(HostStringIsLocalhost("127.0.0.1"));
EXPECT_TRUE(HostStringIsLocalhost("127.0.1.0"));
EXPECT_TRUE(HostStringIsLocalhost("127.1.0.0"));
EXPECT_TRUE(HostStringIsLocalhost("127.0.0.255"));
EXPECT_TRUE(HostStringIsLocalhost("127.0.255.0"));
EXPECT_TRUE(HostStringIsLocalhost("127.255.0.0"));
EXPECT_TRUE(HostStringIsLocalhost("::1"));
EXPECT_TRUE(HostStringIsLocalhost("0:0:0:0:0:0:0:1"));
EXPECT_TRUE(HostStringIsLocalhost("foo.localhost"));
EXPECT_TRUE(HostStringIsLocalhost("foo.localhost."));
EXPECT_TRUE(HostStringIsLocalhost("foo.localhoST"));
EXPECT_TRUE(HostStringIsLocalhost("foo.localhoST."));
}
该特性最早是在 2015 年 6 月的一次提交中引入,见 Resolve RFC 6761 localhost names to loopback。
Gecko 相关的代码位于 netwerk/dns/DNS.cpp
:
// https://github.com/mozilla/gecko-dev/blob/a8f7e8c66bedda0b3cfbd52494cc2e9fc12f606a/netwerk/dns/DNS.cpp#L168
bool IsLoopbackHostname(const nsACString& aAsciiHost) {
// If the user has configured to proxy localhost addresses don't consider them
// to be secure
if (StaticPrefs::network_proxy_allow_hijacking_localhost()) {
return false;
}
nsAutoCString host;
nsContentUtils::ASCIIToLower(aAsciiHost, host);
return host.EqualsLiteral("localhost") ||
StringEndsWith(host, ".localhost"_ns);
}
是在 2020 年 10 月的一次提交中引入,见 Bug 1220810 - Hardcode localhost to loopback。
从 Chromium 的提交信息中可以看到两个 IETF 链接:
- 来自 Apple 公司的提案 RFC 6761 Special-Use Domain Names https://datatracker.ietf.org/doc/html/rfc6761
- 来自 Google 公司的草案 Let 'localhost' be localhost. https://datatracker.ietf.org/doc/html/draft-west-let-localhost-be-localhost
其中提到将如下域名作为特殊用途的保留域名:
- 私有地址:
*.in-addr.arpa.
- 部分顶级域名: "test.", "localhost.", "invalid."
- 示例域名: "example.", "example.com.", "example.net.", "example.org."
在域名分配机构 IANA(Internet Assigned Numbers Authority, 互联网数字分配机构)的网站上也可以查到相关信息,见 IANA-managed Reserved Domains,他们还提供了一份更详细的域名数据 Special-Use Domain Names,包含了所有用于特殊用途的域名,可以在其中找到顶级域名 local.
和 localhost.
。
经验证,在不改本机 hosts 的前提下,Chromium 系的 Chrome、Edge,Firefox,Safari,以及 Windows 10 21H1 上的 IE,打开 foo.localhost 都能访问到本地的 nginx 服务,唯一无法识别的是一台 Windows 10 1904 上的 IE。
相关链接
- https://github.com/chromium/chromium
- https://github.com/mozilla/gecko-dev
- https://github.com/curl/curl
- RFC 6761 Special-Use Domain Names https://datatracker.ietf.org/doc/html/rfc6761
- Let 'localhost' be localhost. https://datatracker.ietf.org/doc/html/draft-west-let-localhost-be-localhost
- IANA-managed Reserved Domains https://www.iana.org/domains/reserved
- Special-Use Domain Names https://www.iana.org/assignments/special-use-domain-names/special-use-domain-names.xhtml