需要更新的 localhost 知识

之前回答了知乎上的一个问题 以子域名的形式使用localhost,有什么坑吗?,本文是该回答的备份。

一开始的回答:

localhost 本身只是一个主机名,约定俗成指向回环地址 127.0.0.1::1,如果你新配的主机名不叫 localhost,那其实叫什么都无所谓,可以是 a.localhostb.localhost,也可以是 a.localb.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,使用 digping 命令获取不到回环地址 IP,因为依赖上游 DNS。但是此时浏览器依然顽强,可以正常访问 http://foo.localhost/,一定是浏览器做了特殊处理。

curl 只处理了 localhost,其他域名依赖 DNS 解析,因此不支持 foo.localhost,见 document the new 'localhost' treatment

排查浏览器

查阅 Chromium 代码,找到了定义于 net/base/url_util.ccIsLocalhost() 函数,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 链接:

其中提到将如下域名作为特殊用途的保留域名:

  • 私有地址: *.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。

相关链接