Docker Container DNS 是怎么运行的
2022-04-09
Background
我在家里的树莓派服务器上搭建了一套 prometheuse / granfana 监控系统,用于监控自己网站的服务器、网关状况。
我的目的是想要把这些监控都放在 docker 里,然后用 portainer 直接配置,这样可以省去很多麻烦。
当我将树莓派添加到 portainer 时,直接使用了树莓派的主机名:`raspberrypi.local`,但是这个时候 portainer 却报错: 8.8.8.8
DNS 无法解析该域名。
Inspect
因为我平时查看监控都是直接 http://raspberrypi.local/xxx
看的,一直没有遇到问题。
这里很明显 portainer 并没有使用本地的 DNS 服务器(即局域网网关 192.131.1.1:53
),导致了无法解析本地主机名。
为了排除是树莓派出问题的可能,我用 python 试着解析了下域名:
import socket
print(socket.gethostbyname('raspberrypi'))
# output: '192.168.31.134'
print(socket.gethostbyname('raspberrypi.local'))
# output: '192.168.31.134'
果然没有任何问题,那就可以确定就是 portainer 实现的问题了。
因为 portainer 是在 docker 里跑的,这里自然而然就会想到是不是因为 docker 内部无法解析本地主机名,导致 portainer 不得不使用 8.8.8.8
?
登录我的 container 验证一下:docker run -it python:3.9-alpine
import socket
print(socket.gethostbyname('raspberrypi'))
# output: Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# socket.gaierror: [Errno -2] Name does not resolve
查看这个 container 的设置: docker inspect cb09f9a13918
,只需要关注最后面的网络设置即可:
"Networks": {
"bridge": {
"IPAMConfig": null,
"Links": null,
"Aliases": null,
"NetworkID": "cb218879dbeafdf5fe020940300c9cbf281068f85c2e60dec2833cbfbb2731e1",
"EndpointID": "0fb1d0addad4ab2fb471666a2b67ac1c25c755bfd65f2d8a728f40c8e902e61d",
"Gateway": "172.17.0.1",
"IPAddress": "172.17.0.4",
"IPPrefixLen": 16,
"IPv6Gateway": "",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"MacAddress": "02:42:ac:11:00:04",
"DriverOpts": null
}
}
手动给这个网关发 dns 请求,看看会不会收到回复:dig @172.17.0.1 raspberrypi
结果超时了:
; <<>> DiG 9.10.6 <<>> @172.17.0.1 hdcjh.xyz
; (1 server found)
;; global options: +cmd
;; connection timed out; no servers could be reached
作为对比,给我家里路由器再发一个同样的 dns 请求: dig @192.168.31.1 raspberrypi
输出:
; <<>> DiG 9.10.6 <<>> @192.168.31.1 raspberrypi
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 14654
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;raspberrypi. IN A
;; ANSWER SECTION:
raspberrypi. 0 IN A 192.168.31.134
;; Query time: 7 msec
;; SERVER: 192.168.31.1#53(192.168.31.1)
;; WHEN: Fri Mar 31 21:36:12 CST 2023
;; MSG SIZE rcvd: 56
现在就可以确认是 docker bridge 网关的 DNS 有问题,先进容器看看网关是怎么配置的:
cat /etc/resolv.conf
# Output:
# nameserver 8.8.8.8
# nameserver 114.114.114.114
这里既没有 bridge 的网关,也没有本地主机的网关,那肯定就访问不到树莓派了。
按照这个方向查查资料:
官方告诉了我两件事:
- bridge DNS 继承自本地主机(可明显 container 和本地主机不一样)
- 使用
--dns
可以手动添加额外的 dns
Solution
第二条好歹有点用,试试将 bridge 的网关添加到 python container 里看看能不能成功解析本地主机:
docker run -it --dns 172.17.0.1 python:3.9-alpine
发现总是返回 Try Again
,这种情况(我猜)是因为 DNS 的 UDP 包发到 bridge 后压根没回复导致的,因为之前直接用 bridge 解析 DNS 的时候就没有成功。
再试试直接将路由器的网关放进去:docker run -it --dns 192.168.31.1 python:3.9-alpine
这一次终于成功获取到了 ip 地址。那 portainer 加上 dns 选项后再试试:
在portainer 原本的 docker run 命令前面加上 --dns `dig anything | grep -E -e 'SERVER: \d+\.\d+\.\d+\.\d+\b' -o -m 1 | sed 's/.\{8\}//'`
这条命令用于自动将本地网关的 IP 地址打印出来。需要注意的是,当更换了局域网络后,需要重新启动 portainer 才能正确获取新的网关地址。
如图所示配置 portainer 后即可成功访问树莓派。

这里需要注意的是,mDNS 的端口是 5353,DNS 是 53,所以在 docker 中连接树莓派时必须使用主机名 raspberrypi
而不是 mDNS 名 raspberrypi.local
。
DNS

DNS(Domain Name System)其实和 linux 文件目录完全一样,只不过 linux /
代表根目录,例如 /root/jiahao.chen/workspace/xxx
;而 DNS 用 .
来代表根目录,例如 www.hdcjh.xyz.
:
- 根目录:
.
- 一级目录:
xyz
- 二级目录:
hdcjh
- 三级目录:
www
注意:最后面的 .
并不是多写的,而是本来就存在,不信可以试试访问 https://www.hdcjh.xyz.
,照样可以访问本网站。只不过大多数浏览器都省略了尾缀的 .
而且 DNS 也默许而已。
发展到今天,DNS 有非常多的类型,当发出某个 DNS 请求后,DNS 服务器就会返回一个或多个 DNS 记录,下表列出了一些常用的 DNS 响应的记录类型和他们的使用场景,完整的记录类型可以在这里找到,或者参考 Cloudflare 的文档(推荐)。
Response Type | Response Data |
---|---|
A | 域名对应的 ipv4 地址 |
AAAA | 域名对应的 ipv6 地址 |
CNAME | 将域名转为另一个域名 |
PTR | ip 地址对应的域名 |
SVR | 服务器的类型 |
TXT | 文本信息,想存啥都行 |
手把手解析 DNS
- 使用 tcpdump 监听 53 端口,并把监听到的数据存到本地文件中:
sudo tcpdump -nAs 0 'port 53' -w dns_record.pcap
- 使用 dig 或者 python.socket.gethostbyname 来发送一条 DNS 请求,例如:
dig raspberrypi
- 用 wireshark 打开1⃣️中保存的文件,即可看到记录的 UDP DNS 请求。

这里只需要关注第 5、6 条关于 raspberry 的请求即可。
第五条,本机向路由器发送的 DNS 请求:

第六条,路由器返回的 DNS 响应:

另一个重要的协议是 rDNS,用来将 ip 地址转为域名。试试找到一条 IP 对应的域名: dig -x 192.168.1.16
。
mDNS
支持情况
Apple 全系列都双手双脚支持 mDNS,每当我手机亮屏的时候都会发 mDNS 告诉别人他在这,看看附近有啥服务可以用。然后 iPad 就会告诉他我可以投屏,Mac M1 也会站出来显示他的存在。

从上图中可以看到,我的名字(iPhone账户的姓名,包括汉字,图中没打印出来而已)赤裸裸的暴露在网络中。这其实给撩妹提供了可能,假设你在学校碰到了一个女生,然后就可以通过监听 5353 端口知道她的名字,然后上去和他打招呼:“好久不见“。由此可见学好 tcpdump 对脱单非常重要‼️
关于 docker 中访问 mDNS 地址 raspberrypi.local
是否可识别,可参考下表:
Environment | DNS Support | mDNS Support |
---|---|---|
本地主机 | ✅ | ✅ |
Docker alpine | ✅ | ❌ |
Docker alpine ping | ✅ | ❌ |
Portainer | ✅ | ✅ |
由此可见,在日常使用中能使用 DNS 就尽量使用 DNS。
实现原理
关于如何用 mDNS 搭建局域网服务可以参考这篇。
mDNS 就是在局域网没有 DNS 服务器的时候,利用 IP 多播来实现互相发现的一种机制,特点就是所有的 hostname 后缀都是 .local.
。
IP 多播 本质上其实就是 UDP 多播,IPv4 和 v6 都有专门定义的多播 IP 段,局域网内的机器可以自己选择是否加入多播组,从而接收相应消息。而UDP 多播 就是在 IP 多播的机制上使用了 UDP 而已,IP 多播是 UDP 多播的超集。
下表列出了 mDNS 的一些特性:
Property | Value |
---|---|
ipv4 多播地址 | 224.0.0.251 |
ipv6 多播地址 | ff02::fb |
port | 5353 |
测试自己的电脑是否支持 mDNS: ping <hostname>.local
或者 ping <hostname>.local.
都可以,最后在请求的时候都会变成 .local
参考文档
- RFC 6762
- 单播响应、单响应多 service、请求去除响应冗余、请求限速
- 具体的设计思路、原因,非常值得一看。
- zeroconf
- 第一个专门用于实现上述标准的组织
- Apple Bonjour
- 苹果的文档写的挺齐全,图文并茂
- github.com/hashicorp/mdns
- 具体的 mDNS golang 实现