「译介」基础网络故障排查

2022-03-18, 星期五, 12:05

译介System AdministrationTroubleshooting

译者注:原文 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、网络还是应用程序出了问题?”。