译者注:原文 Basic Network Troubleshooting,作者 Jan Schaumann 在史蒂文斯理工学院教授《系统管理》和《Unix 高级编程》课程。本文使用的大部分工具在 Unix/Linux 系统中开箱即用。
译者对文章中部分内容做了拆分和顺序调整,补充或删除了一些晦涩或不常见内容。部分命令在 macOS 12.3 上进行了复现操作,其结果替换了原文中的对应部分。
有各种各样的原因会让你访问不了一个远端主机,不要上来就觉着是 DNS 出了问题。快速定位问题原因(或是确定该找谁解决问题)是一项十分有用的技能,然而我常常见到一些资深工程师在错误的方向上浪费了宝贵的时间,而这些问题——只要你看懂了报错日志——本该是能被迅速解决的。
先问问自己这个问题:“到底是 DNS、网络还是应用程序出了问题?”,本文将列举一些典型的错误场景,并说明如何据此定位问题,以便您将来更有把握地回答这个问题。所有这些技巧归根结底就是看懂报错信息。
Unable to resolve(无法解析)
好吧,有时候真就是 DNS 的问题。
在 macOS 12.3 上可能长这样:
$ ssh foo.netmeister.org
ssh: Could not resolve hostname foo.netmeister.org: nodename nor servname provided, or not known
或者在 CentOS 8.2 上:
$ ssh foo.netmeister.org
ssh: Could not resolve hostname foo.netmeister.org: Name or service not known
等等!杀鸡毋需 ping(8),traceroute(8) 和 tcpdump(1) 这样的牛刀。问题很明显,could not resolve,你的机器还没试着向对方发送半个数据包,因为它没搞明白怎么把 foo.netmeister.org 这个名字变成 IP 地址。
先检查下有没有手滑多按了几个字母,假如都没问题,接下来该检查什么?
试试权威 DNS 服务器(Authoritative name server)
译者注:这个翻译名称来自《计算机网络:自顶向下方法》。DNS 系统是典型的分布式数据库场景,大体来说有三种 DNS 服务器:根 DNS 服务器,顶级域(Top-Level Domain, TLD)DNS 服务器和权威 DNS 服务器。
粗略地讲,客户端先向根服务器请求 org 的 TLD 服务器的 IP 地址,再向该 TLD 服务器请求
netmeister.org的权威 DNS 服务器地址,最后向权威 DNS 服务器请求foo.netmeister.org的 IP 地址。具有公共主机的机构必须提供公共可访问的 DNS 记录,一般大学和大型企业都会实现和维护自己的权威 DNS 服务器来存储这些记录,或是交给某个服务提供商的权威 DNS 服务器。
当然在实际应用中,局域网或 ISP 运营商还会设置本地 DNS 服务器(就是你系统中的网络设置上展示的那个),上文提到的客户端行为一般是由该本地 DNS 服务器代劳的。
查询 netmeister.org 的权威 DNS 服务器:
$ dig +short netmeister.org ns
ns-179-c.gandi.net.
ns-181-a.gandi.net.
ns-143-b.gandi.net.
选一台权威 DNS 服务器,查询有没有 foo.netmeister.org 这个主机名称的 DNS 记录:
$ dig +noall +answer +comments @ns-179-c.gandi.net. foo.netmeister.org
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 38091
;; flags: qr aa rd; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1
;; WARNING: recursion requested but not available
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
status: NOERROR 说明 DNS 服务器正常工作,查询获得了正确的响应。现在可以确定 DNS 没记录 foo.netmeister.org,接下来加个解析记录或者联系下域名持有者就完事了。
注意子域名
如果主机名有好几级的话,就不能这么武断地得出结论了。
假设有这么个主机 bar.dns.netmeister.org。向 ns-179-c.gandi.net. 查询 DNS 记录:
$ dig +noall +answer +comments @ns-179-c.gandi.net. bar.dns.netmeister.org
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 51181
;; flags: qr rd; QUERY: 1, ANSWER: 0, AUTHORITY: 2, ADDITIONAL: 3
;; WARNING: recursion requested but not available
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
这个权威 DNS 服务器没有相应的 DNS 记录。再查询下 dns.netmeister.org 的权威 DNS 服务器:
$ dig +short dns.netmeister.org ns
panix.netmeister.org.
原来是因为子域名的 DNS 记录储存在另一台服务器上。
$ dig +noall +answer @panix.netmeister.org bar.dns.netmeister.org
bar.dns.netmeister.org. 3600 IN A 198.51.100.1
访问不了权威 DNS 服务器时该怎么办?
$ dig +noall +answer +comments @ns-179-c.gandi.net. foo.netmeister.org
[ time elapses ]
;; connection timed out; no servers could be reached
这能说明问题出在权威 DNS 服务器上吗?不一定!有没有一种可能,就是你的系统访问不了域名服务器,但你配置的域名解析器(比如 /etc/resolv.conf 里的本地 DNS 服务器)可以呢?如果管理员阻止了网络里其他设备向外发送的 DNS 流量,就会造成这个现象。
那么在不使用本机域名解析机制,也不往外发送去 53 端口的 UDP 数据包的话,还有什么办法判断这个主机名是否存在呢?有几个方法:
TCP
不是 53 端口上的 UDP 才能用 DNS,可以试试 TCP:
$ dig +tcp +noall +answer +comments @ns-179-c.gandi.net. foo.netmeister.org
;; communications error to 2604:3400:aaac::b4#53: host unreachable
不过点儿有点背,这个查询还是被阻断了。
DNS-over-HTTPs
可以用 Google 或 Cloudflare 家的公共 DNS 测试。自 2021 年 3 月起 dig(1) 支持通过 +https 选项启用 DoH,如果你的 dig(1) 版本旧了点,也可以用 JSON API。
译者注:很遗憾,截至翻译之日,CentOS 8.2 和 macOS 12.3 预装的 dig 均未支持
$ curl -s -H 'accept: application/dns-json' 'https://cloudflare-dns.com/dns-query?name=foo.netmeister.org&type=A'
{"Status":0,"TC":false,"RD":true,"RA":true,"AD":true,"CD":false,"Question":[{"name":"foo.netmeister.org","type":1}],"Authority":[{"name":"netmeister.org","type":6,"TTL":10800,"data":"ns1.gandi.net. hostmaster.gandi.net. 1646724077 10800 3600 604800 10800"}]}%
使用 jq 提取下结果:
$ curl -s -H 'accept: application/dns-json' 'https://cloudflare-dns.com/dns-query?name=foo.netmeister.org&type=A' | jq '.Answer'
null
这个方法不能帮你直接向权威 DNS 服务器发起查询,但至少可以确认有没有域名服务器认得这个主机名。
消除 /etc/hosts 谜团
人们经常沮丧地发现 DNS 查询的结果与其他工具的表现并不一致,最后往往查到本地的 /etc/hosts 有改动。常见症状如下:
$ host foo.netmeister.org
Host foo.netmeister.org not found: 3(NXDOMAIN)
$ ssh foo.netmeister.org
ssh: connect to host foo.netmeister.org port 22: Connection refused
或者这样:
$ host bar.dns.netmeister.org
bar.dns.netmeister.org has address 198.51.100.1
bar.dns.netmeister.org has IPv6 address 2001:db8::c2de:2d22:5ca1:2727
$ ping6 bar.dns.netmeister.org
PING6(56=40+8+8 bytes) 2001:470:30:84:e276:63ff:fe72:3900 --> 2001:db8::9a6f:ba98:b763:574e
ping6: sendmsg: Network is unreachable
ping6: wrote 2001:db8::9a6f:ba98:b763:574e 16 chars, ret=-1
ping6: sendmsg: Network is unreachable
ping6: wrote 2001:db8::9a6f:ba98:b763:574e 16 chars, ret=-1
^C
--- 2001:db8::9a6f:ba98:b763:574e ping6 statistics ---
2 packets transmitted, 0 packets received, 100.0% packet loss
$ grep bar.dns.netmeister.org /etc/hosts
2001:db8::9a6f:ba98:b763:574e bar.dns.netmeister.org
你发起了 DNS 查询,得到一个地址(2001:db8::c2de:2d22:5ca1:2727),但是另一个命令 ping6 使用了库函数 gethostbyname(3),这个函数先试了试 /etc/hosts,返回了 2001:db8::9a6f:ba98:b763:574e。
如果是这样,纠正 /etc/hosts 文件,把那个修改的人(没准是你自己)拉来喷一顿,然后把这个文件改成只读(用 sudo chflags schg /etc/hosts 或者 sudo chattr +i /etc/hosts)。
其他域名解析问题
抛开 DNS 的问题,还有其他原因会导致域名解析失败,比如这个:
$ ssh foo.netmeister.org
ssh: Could not resolve hostname foo.netmeister.org: Temporary failure in name resolution
检查下 /etc/resolv.conf,没准是压根儿没配 DNS 服务器。
或者长这样(具体取决于系统和 SSH 的版本):
$ ssh foo.netmeister.org
ssh: Could not resolve hostname foo.netmeister.org: Name or service not known
有可能是配置的 DNS 服务器都访问不了。
也有可能长这样:
$ ssh foo.netmeister.org
ssh: Could not resolve hostname foo.netmeister.org: Non-recoverable failure in name resolution
说明你能访问配置的 DNS 服务器,但服务器认为你没有查询权限,于是返回了 status: REFUSED。
Connection refused(连接被拒)
$ ssh foo.netmeister.org
ssh: connect to host foo.netmeister.org port 22: Connection refused
你把目标主机名转换成 IP 地址后向对方发送了一个 TCP SYN,对方响应了一个 TCP RST,因为没人在 22 端口上接听。
20:29:46.690536 IP 172.16.1.15.54702 > foo.netmeister.org.ssh: Flags [S], seq 1750579353, win 65535, options [mss 1460,nop,wscale 6,nop,nop,TS val 674037520 ecr 0,sackOK,eol], length 0
20:29:46.701705 IP foo.netmeister.org.ssh > 172.16.1.15.54702: Flags [R.], seq 0, ack 1750579354, win 0, length 0
如果主机名和端口都没问题,那问题大概在对方身上了。但是要注意 连接被拒 和接下来这个完全是两码事。
Operation timed out(操作超时)
$ ssh foo.netmeister.org
ssh: connect to host foo.netmeister.org port 22: Operation timed out
连接被拒 证明了目标主机是可达的,而 操作超时 则说明使用这个协议和端口访问目标主机是行不通的。tcpdump(1) 可以查看系统重复发送且未得到回应的 SYN:
20:57:10.360287 IP 172.16.1.15.55111 > foo.netmeister.org.ssh: Flags [S], seq 2997342946
20:57:11.361373 IP 172.16.1.15.55111 > foo.netmeister.org.ssh: Flags [S], seq 2997342946
20:57:12.361702 IP 172.16.1.15.55111 > foo.netmeister.org.ssh: Flags [S], seq 2997342946
20:57:13.362352 IP 172.16.1.15.55111 > foo.netmeister.org.ssh: Flags [S], seq 2997342946
20:57:14.363566 IP 172.16.1.15.55111 > foo.netmeister.org.ssh: Flags [S], seq 2997342946
...
所以只要你确定端口和主机名是对的,问题就在对面身上,或者至少在你俩间的链路上,因为数据报不知道丢到哪里去了。
ping(8) 和 traceroute(8)
终于可以祭出 ping(8) 和 traceroute(8) 了。ping(8) 也许能够告诉你目标主机到底有没有出问题,但是不要忘了,ICMP 报文可能在半路上就被丢了或者阻止了,所以 ping 失败了并不能证明目标主机无了。
译者注:有很多防火墙确实会这么干
traceroute(8) 也许能告诉你到哪一步的时候路子就断了,前提是这个的报文没有被阻断,UDP 端口也能用之类的。traceroute -P tcp -p <port> 什么的从某种意义上来说还能救一下。
No route to host(没有到达目标主机的路由)
$ ssh foo.netmeister.org
ssh: connect to host foo.netmeister.org port 22: No route to host
你也获得 IP 地址了,但是系统不知道怎样把数据报发送过去。比如这是个 IPv6 地址,系统却不支持 IPv6 栈。也有可能是路由信息错了或有缺失,但不管是哪一个,都是客户端该考虑的问题。
上面那个是 BSD 的,看看 macOS 这边的:
$ ssh foo.netmeister.org
ssh: connect to host foo.netmeister.org port 22: Undefined error: 0
啧,Apple 我谢谢你全家哦,这可真是帮了个大忙。
Connection closed by remote host(连接被远程主机关闭)
你也许会碰到这个问题,和 #连接被拒 还蛮像的。
$ ssh foo.netmeister.org
kex_exchange_identification: Connection closed by remote host
你也解析完主机名了,也能通过端口 22 和对方说上话了,然后对方关闭了连接。这有可能是因为对面监听 22 端口的程序不支持 SSH 协议,一般来说是用了什么代理机制或是加了什么负载均衡,也有可能就是你搞错端口了。
小结
上面这些例子用了 SSH 做示范,一般可以套用到其他基于 TCP 的协议比如 HTTP 上。当然应用程序还有各自的错误情形,不同的应用程序向用户传递的消息也不尽相同。
但是在你开始慌慌张张地调试应用前,可以先问问自己:“到底是 DNS、网络还是应用程序出了问题?”。