揭秘 docker 容器内 DNS 解析原理
这正是宿主机【我使用的是 Ubuntu 20.04 虚拟机】使用的 DNS 服务器ip地址。以这种方式启动的容器,容器内部的/etc/resolv.conf 文件中配置的 ip 是宿主机使用的 DNS 服务器ip。以 docker compose 启动的容器,使用的 DNS 服务器是 dockerd 程序内部的 DNS 服务器。在使用 docker compose 时,我们知道,一个容器可以使用另
背景
这几天在使用 docker 中,碰到了在容器中 DNS 解析的一些问题。故花些时间弄清了原理,写此文章分享。
1. docker run 命令启动的容器
以启动一个 busybox 容器为例:
root@ubuntu20:~# docker run -itd --name u1 busybox
63b59ca8aeac18a09b63aaf4a14dc80895d6de293017d01786cac98cccda62ae
root@ubuntu20:~# docker exec -it u1 sh
/ #
/ # ping www.baidu.com
PING www.baidu.com (14.119.104.189): 56 data bytes
64 bytes from 14.119.104.189: seq=0 ttl=127 time=34.976 ms
64 bytes from 14.119.104.189: seq=1 ttl=127 time=35.369 ms
^C
--- www.baidu.com ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 34.976/35.172/35.369 ms
在容器里可以 ping 通外部的域名。
过程
查看容器中的 /etc/resolv.conf 文件内容:
/ # cat /etc/resolv.conf
# This file is managed by man:systemd-resolved(8). Do not edit.
#
# This is a dynamic resolv.conf file for connecting local clients directly to
# all known uplink DNS servers. This file lists all configured search domains.
#
# Third party programs must not access this file directly, but only through the
# symlink at /etc/resolv.conf. To manage man:resolv.conf(5) in a different way,
# replace this symlink by a static file or a different symlink.
#
# See man:systemd-resolved.service(8) for details about the supported modes of
# operation for /etc/resolv.conf.
nameserver 192.168.30.2
search localdomain
使用的 DNS 服务器 ip 是 192.168.30.2。
这正是宿主机【我使用的是 Ubuntu 20.04 虚拟机】使用的 DNS 服务器ip地址。在宿主机上使用 systemd-resolve --status 命令可以看到:
root@ubuntu20:~# systemd-resolve --status
....
Link 2 (ens33)
Current Scopes: DNS
DefaultRoute setting: yes
LLMNR setting: yes
MulticastDNS setting: no
DNSOverTLS setting: no
DNSSEC setting: no
DNSSEC supported: no
Current DNS Server: 192.168.30.2
DNS Servers: 192.168.30.2
DNS Domain: localdomain
...
结论
以这种方式启动的容器,容器内部的 /etc/resolv.conf 文件中配置的 ip 是宿主机使用的 DNS 服务器ip。
参考文章:How does the Docker DNS work?
2. docker compose 启动的容器
在使用 docker compose 时,我们知道,一个容器可以使用另一个容器的服务名来获取它的ip地址。
看个例子,docker-compose.yml 文件内容如下:
version: '2'
services:
redis:
image: redis:3.2
busybox:
image: busybox
stdin_open: true
tty: true
它定义了两个服务 redis 和 busybox。
进入 busybox 容器内,可以使用 “redis”来获取 redis 容器的 ip 地址:
root@ubuntu20:~/test# docker-compose up -d
Creating network "test_default" with the default driver
Creating test_redis_1 ... done
Creating test_busybox_1 ... done
root@ubuntu20:~/test# docker-compose ps
Name Command State Ports
------------------------------------------------------------------
test_busybox_1 sh Up
test_redis_1 docker-entrypoint.sh redis ... Up 6379/tcp
root@ubuntu20:~/test# docker exec -it test_busybox_1 sh
/ #
/ # ping redis -c 1
PING redis (192.168.112.2): 56 data bytes
64 bytes from 192.168.112.2: seq=0 ttl=64 time=1.103 ms
--- redis ping statistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min/avg/max = 1.103/1.103/1.103 ms
反之亦然。这是怎么做到的呢?
过程
先看看容器中的 /etc/resolv.conf 文件:
/ # cat /etc/resolv.conf
search localdomain
nameserver 127.0.0.11
options edns0 trust-ad ndots:0
它使用的 DNS 服务器 ip 地址居然是个环回地址 127.0.0.11 !那是怎么做到可以解析域名的呢?
在容器内的环回口抓下包看下:
root@ubuntu20:~/test# docker inspect test_busybox_1 | grep Pid
"Pid": 211432,
"PidMode": "",
"PidsLimit": null,
root@ubuntu20:~/test# nsenter -t 211432 -n tcpdump -i lo -nn
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on lo, link-type EN10MB (Ethernet), capture size 262144 bytes
10:11:39.223669 IP 127.0.0.1.58808 > 127.0.0.11.59712: UDP, length 34
10:11:39.223707 IP 127.0.0.1.58808 > 127.0.0.11.59712: UDP, length 34
10:11:39.224305 IP 127.0.0.11.53 > 127.0.0.1.58808: 6751 0/0/0 (23)
10:11:39.224682 IP 127.0.0.11.53 > 127.0.0.1.58808: 18524 1/0/0 A 192.168.112.2 (44)
这里有个小技巧:因为 busybox 容器中并没有 tcpdump 程序,所以使用 nsenter
进入了容器的网络命名空间,执行抓包程序。
通过抓包可以看到:DNS 请求被发给了 127.0.0.11.59712 ?说明有某个服务监听这个 ip地址+端口。
使用 ss 命令查看下:
root@ubuntu20:~/test# nsenter -t 211432 -n ss -unlp
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
UNCONN 0 0 127.0.0.11:59712 0.0.0.0:* users:(("dockerd",pid=1078,fd=78))
发现监听这个 ip 地址加端口的居然是 dockerd 程序!原来 DNS 请求是发送给了 dockerd 程序处理了!
但是 DNS 请求目的端口不是 53 吗?怎么变成了这个 59712,这是怎么回事?看下 iptables nat 表:
root@ubuntu20:~/test# nsenter -t 211432 -n iptables -t nat -S
-P PREROUTING ACCEPT
-P INPUT ACCEPT
-P OUTPUT ACCEPT
-P POSTROUTING ACCEPT
-N DOCKER_OUTPUT
-N DOCKER_POSTROUTING
-A OUTPUT -d 127.0.0.11/32 -j DOCKER_OUTPUT
-A POSTROUTING -d 127.0.0.11/32 -j DOCKER_POSTROUTING
-A DOCKER_OUTPUT -d 127.0.0.11/32 -p tcp -m tcp --dport 53 -j DNAT --to-destination 127.0.0.11:43107
-A DOCKER_OUTPUT -d 127.0.0.11/32 -p udp -m udp --dport 53 -j DNAT --to-destination 127.0.0.11:59712
-A DOCKER_POSTROUTING -s 127.0.0.11/32 -p tcp -m tcp --sport 43107 -j SNAT --to-source :53
-A DOCKER_POSTROUTING -s 127.0.0.11/32 -p udp -m udp --sport 59712 -j SNAT --to-source :53
原来有一条 DNAT 规则,将目的 ip 为127.0.0.11,端口为 53 的 udp 数据包,目的端口修改为 59712。一切真相大白!
结论
以 docker compose 启动的容器,使用的 DNS 服务器是 dockerd 程序内部的 DNS 服务器。
它是通过以下三步来实现的:
- dockerd 在容器的网络命名空间中创建一个监听 127.0.0.11 的 udp socket
- 设置容器内 /etc/resolv.conf 文件ip 为 127.0.0.11
- 容器内添加 iptable DNAT 规则
更多推荐
所有评论(0)