重新思考浏览器输入了 URL 并按下回车之后到底发生了什么——本地 DNS 部分

作为一个没有去过大厂的不怎么会写代码的萌新,我总是认为面试的过程是一个验证对方水平以及互相探讨问题并以此来确认对方学习能力的过程,而非一个应试或者考试的过程,至少对于一些正常岗位而言是这样。

而所谓的”笔试“和你问我答的面试感觉只应该出现需要低成本大规模筛选面试者(比如校招)或者某些奇怪的外包公司(比如 *为 的外包或者一些银行等国企的开发岗位似乎特别喜欢搞这种奇怪的”笔试“,牛客网为此提供了平台可谓“功不可没”)。

作为喜欢背八股文面试题的朋友肯定不陌生,有个非常”经典“的面试题叫做「输入 URL 到页面展示到底发生了什么」,你看,随便搜索一下就能看到这种文章:

他们占据了 Google 搜索的前列,也非常符合喜欢背题的人快速“预习”需求:

我们来看看这个「看完吊打面试官」的文章,部分内容如下:

1、请求一旦发起,浏览器首先要做的事情就是解析这个域名,一般来说,浏览器会首先查看本地硬盘的 hosts 文件,看看其中有没有和这个域名对应的规则,如果有的话就直接使用 hosts 文件里面的 ip 地址。

2、如果在本地的 hosts 文件没有能够找到对应的 ip 地址,浏览器会发出一个 DNS请求到本地DNS服务器 。本地DNS服务器一般都是你的网络接入服务器商提供,比如中国电信,中国移动。

3、查询你输入的网址的DNS请求到达本地DNS服务器之后,本地DNS服务器会首先查询它的缓存记录,如果缓存中有此条记录,就可以直接返回结果,此过程是递归的方式进行查询。如果没有,本地DNS服务器还要向DNS根服务器进行查询。

作为一个没有接触过 DNS 的萌新,在遇到面试官问道这个问题,可能面试官在说「能不能解释一下浏览器输入了…」的时候你已经飞快的通过大脑的 KV 存储,想到已经该如何输出背下来的结论了,等面试官话音刚落,你变开始了流式输出:「浏览器输出了 URL 按下回车之后首先浏览器会通过查询本地的 hosts 文件,然后 hosts 文件中没有发现记录,就请求 DNS 服务器,然后一级级往上查询直到根 DNS 服务器…」

这个问题我觉得本身是个比较有意思的问题,因为可以考察面试者对于整个流程的了解程度,但是通过这样背诵而来的回答这样真的能「看完吊打面试官」么?

要知道我们电脑,浏览器的发展日新月异,但是这个问题作为一个「经典面试题」似乎从来都是上面的回答,如果作为面试官,听到这样的回答之后会不会第一反应想到几个问题,也是本文想要讨论的点,如下:

  • 现代的浏览器本身会不会缓存 DNS 记录?
  • 浏览器有权限直接去读 hosts 文件么?
  • 浏览器是怎么发出的 DNS 请求?(这个时候请别回答是发出的 UDP 请求)
  • Hosts 文件之后就直接是外部 DNS 服务器了么?

由于我不卖课,也不推广一些什么(也许是)自己写的「Java 精品合集」,如果读者对上面讨论的问题感兴趣,不妨在这里停留一下并思考一下上面我提出的问题,或者想想看这里面还有没有别的点其实也是可能和目前的结构不一样的。

为了帮助大家在此停顿,我在这里插播一个图片~

现代的浏览器本身会不会缓存 DNS 记录?

我们先来说说第一个问题——现代的浏览器本身会不会缓存 DNS 记录?

答案是会的,以 Chrome 为例,我们需要先:

  1. 打开 chrome://net-export/ 然后开始录制一段时间的记录
  2. 打开 https://netlog-viewer.appspot.com/ 并上传刚刚的记录

本来这个操作可以在 chrome://net-internals/#dns 一步完成的,不知道为啥 Chrome 搞成了上面两步,还引入了一个外部网站

从上面的截图我们可以观测到两个信息:

  1. Chrome 浏览器本身缓存了 DNS 的请求,叫做 Host resolver cache
  2. 这个缓存最大只能缓存 1000 条记录

这个缓存在 Chrome 的官方文档中被称为 built-in resolver 或者 async resolver, 它的文档可以在 Chrome Host Resolution 这里看到,当满足以下所有触发条件的时候便会使用:

  • DnsClient 已通过 net::HostResolverManager::SetInsecureDnsClientEnabled(true) 启用不安全请求,或安全 DNS 模式不是 net::SecureDnsMode::OFF
  • 系统 DNS 配置已成功确定。
  • 请求主机名不以 “.local” 结尾。
  • 该请求不是带有 HOST_RESOLVER_CANONNAME 标志的地址查询。

以上内容为 ChatGPT 翻译

至于为什么这个缓存最大是缓存 1000 条记录,我们只能读一下代码了,在 https://chromium.googlesource.com/chromium/src/+/main/net/dns/host_cache.cc#1185 这里我们可以看到如下定义:

// static
std::unique_ptr<HostCache> HostCache::CreateDefaultCache() {
#if defined(ENABLE_BUILT_IN_DNS)
  const size_t kDefaultMaxEntries = 1000;
#else
  const size_t kDefaultMaxEntries = 100;
#endif
  return std::make_unique<HostCache>(kDefaultMaxEntries);
}

对于这个开关 ENABLE_BUILT_IN_DNS 相关的代码可以在这里看到:

// Enables or disables the built-in asynchronous DnsClient. If enabled, by
// default (when no |ResolveHostParameters::source| is specified), the
// DnsClient will be used for resolves and, in case of failure, resolution
// will fallback to the system resolver (in tests, HostResolverProc from
// HostResolverSystemTask::Params). If the DnsClient is not pre-configured
// with a valid DnsConfig, a new config is fetched from NetworkChangeNotifier.
//
// Setting to |true| has no effect if |ENABLE_BUILT_IN_DNS| not defined.
virtual void SetInsecureDnsClientEnabled(bool enabled,
                                          bool additional_dns_types_enabled);

不过至于为什么 kDefaultMaxEntries 这里定义为了 1000 ,目前我还没找到对应的出处。

浏览器是怎么发出的 DNS 请求

你认为浏览器可以直接读取系统的 hosts 文件来使用么?当然不是,我们继续看 Chrome 的文档:

Usually called the “system resolver” or sometimes the “proc resolver” (because it was historically always implemented using net::HostResolverProc). Results are queried from the system or OS using the getaddrinfo() OS API call. This source is only capable of address (A and AAAA) resolves but will also query for canonname info if the request includes the HOST_RESOLVER_CANONNAME flag. The system will query from its own internal cache, HOSTS files, DNS, and sometimes mDNS, depending on the capabilities of the system.

可以知道操作系统提供了一个叫做 getaddrinfo() 的系统 API 供浏览器使用,所以浏览器需要发送一个 DNS 请求也只是和这个接口交互而已,浏览器本身不会自己发送任何奇怪的请求甚至直接读取一个系统文件。

DNS 请求顺序

但是这里其实有个问题,一般来说根据常规的「面试题面经」我们知道 DNS 请求顺序是:hosts 文件(未命中) -> DHCP 下发的称为本地 DNS 服务器(未命中) -> 上游 DNS 服务器(未命中) -> 根 DNS 服务器。

但是在现代电脑中一般有这么个东西,比如 systemd-resolved

为什么我会关注到这个呢,因为它在我电脑上最近启动不太正常。

我们看 systemd-resolved - ArchWiki 可以知道它是一个 systemd 提供的服务,会监听在 D-Bus 和 127.0.0.53 上对系统上的应用提供服务,且对于 systemd 的系统来说是默认安装并启用的:

systemd-resolved is a systemd service that provides network name resolution to local applications via a D-Bus interface, the resolve NSS service (nss-resolve(8)), and a local DNS stub listener on 127.0.0.53. See systemd-resolved(8) for the usage.

systemd-resolved is a part of the systemd package that is installed by default.

举个例子,虽然我电脑所处的 DHCP 环境下下发的是 192.168.33.501.1.1.1 这两个 DNS 服务器:

nmcli device show wlp3s0 | grep IP4

IP4.ADDRESS[1]:                         192.168.33.242/24
IP4.GATEWAY:                            192.168.33.50
IP4.ROUTE[1]:                           dst = 192.168.33.0/24, nh = 0.0.0.0, mt = 600
IP4.ROUTE[2]:                           dst = 0.0.0.0/0, nh = 192.168.33.50, mt = 600
IP4.DNS[1]:                             192.168.33.50
IP4.DNS[2]:                             1.1.1.1

但是我们实际用 dig 发起请求,或者看 /etc/resolv.conf 就会发现其实是 127.0.0.53:

nameserver 127.0.0.53
options edns0 trust-ad
search .

dig 请求结果如下:

 dig docs.pingcap.com

...

;; ANSWER SECTION:
docs.pingcap.com.	34	IN	A	106.75.96.118 [北京市 优刻得信息科技有限公司 (UCloud) BGP 数据中心]

;; Query time: 20 msec
;; SERVER: 127.0.0.53 [局域网 IP]#53(127.0.0.53 [局域网 IP]) (UDP)
...

Windows 和 Mac 上也有类似的机制,分别叫做 DNS Client Service (DNSCache) 和 mDNSResponder

也就是说在相对比较现代的电脑上,我们应用中的 DNS 请求也并没有直接发送到系统获得的 DNS 服务器上,而是发到了另一个系统级别的缓存中了。

所以到这里,我们可以修正一下上面的回答,实际的请求顺序是:浏览器 DNS 缓存(未命中) -> 浏览器调用 getaddrinfo() 查询数据,其中,getaddrinfo() 的查询背后有多个缓存,顺序如下文所述。

getaddrinfo() 是按照什么顺序查询的

由于上面有 systemd-resolved 的存在,我们就有点疑惑了,那这个 getaddrinfo() 函数又是根据什么顺序来查询的 DNS 呢?

我们从 What does getaddrinfo do? 文章中可以知道 getaddrinfo() 在 Linux 系统上会阅读 /etc/nsswitch.conf 来决定查询顺序,比如在我的电脑上这个文件中的关键信息是:

# In order of likelihood of use to accelerate lookup.
passwd:     files sss systemd
shadow:     files
group:      files sss systemd
hosts:      files myhostname mdns4_minimal [NOTFOUND=return] resolve [!UNAVAIL=return] dns
services:   files sss
netgroup:   files sss
automount:  files sss

从 hosts 行可以看到顺序是 files myhostname mdns4_minimal [NOTFOUND=return] resolve [!UNAVAIL=return] dns,翻译一下就是:

  • files: 尝试在 /etc/hosts 文件中查找主机信息。
  • myhostname: 尝试通过系统的主机名服务来解析主机名。
  • mdns4_minimal: 尝试通过 mDNS(Multicast DNS)来解析主机名。
  • [NOTFOUND=return]: 如果前面的查找未找到主机信息,立即返回而不进行后续查找。
  • resolve: 尝试通过 systemd 的解析服务来解析主机名。
  • [!UNAVAIL=return]: 如果 systemd 解析服务不可用,立即返回而不进行后续查找。
  • dns: 最后尝试通过传统的 DNS 解析服务来解析主机名。

到这里,我们可以进一步获得更加清晰的结论,以我的电脑默认配置为例,实际的 DNS 查询顺序是:

  1. 浏览器 DNS 缓存
  2. 浏览器调用 getaddrinfo() 查询数据,其中 getaddrinfo() 会根据如下顺序查询
    1. 先看 /etc/hosts 文件
    2. 通过主机名服务/mDNS 查询
    3. 通过 systemd-resolved 查询
    4. 最后通过 DHCP 下发的 DNS 查询

在 Windows 系统上,这个 nssswitch 的功能被一个称为 Name Resolution Policy Table 的东西替代,可以参考文档 The NRPT | Microsoft Learn

如果最后一步通过 DHCP 下发的 DNS 查询也没有查到结果的话,那么就是由那个 DNS 服务器负责向自己的上游查询记录,也就是所谓的递归查询,并返回查询结果了。

你看,「输入 URL 到页面展示到底发生了什么」确实是一系列有趣提问的开端,我们其实只要稍微思考一下就会发现内部有许多我们想不到的细节,面试官可以通过一步步引导面试者并提出相关的问题来判断面试者对于这一系列问题的思考究竟是背诵的答案还是有自己独特的理解。

当然,由于这个问题过于常见,个人感觉作为面试官而言如果不是对此有非常深入的研究的话或许还是换个角度切入比较友好,减少双方背题/答案的时间,增加面试的乐趣(也许),比如——「在你目前面试所用的电脑的环境下,你浏览器上输入了 google.com 之后 DNS 请求是发往了哪个地址?」以及「如果那个地址没有返回正确的 IP 的话,一般如何解决?」。

References

  1. Chrome Host Resolution
  2. What does getaddrinfo do?
  3. Disable DNS Cache on macOS | Apple Developer Forums
  4. The NRPT | Microsoft Learn

Chinese

3441 字

2024-01-08 13:00 +0000