后浪笔记一零二四

使用自定义网桥时,docker容器内nameserver为127.0.0.11:53

从Docker 1.10版本开始,当容器被分配到自定义网络(如用户定义的bridge网络或overlay网络)时,Docker会为这些容器提供一个内置的DNS服务器,其地址固定为127.0.0.11。这个内置的DNS服务器允许容器通过服务名称相互发现和通信,尤其是在多容器应用中非常有用。

dns是127.0.0.11,所以该dns server肯定是本地监听的,使用netstat -anu查看容器本地的所有udp监听:

1
2
3
4
$ nsenter -t $(docker inspect -f {{.State.Pid}} b884630283ce) -n netstat -anu
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State      
udp        0      0 127.0.0.11:41287        0.0.0.0:*    

可以发现:没有进程在监听53端口,只有一个奇怪的41287

查看容器的nat iptables:

1
2
3
4
5
6
$ nsenter -t $(docker inspect -f {{.State.Pid}} b884630283ce) -n iptables -t nat -L -n -v
(...省略无关内容)
Chain DOCKER_OUTPUT (1 references)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 DNAT       tcp  --  *      *       0.0.0.0/0            127.0.0.11           tcp dpt:53 to:127.0.0.11:37711
   29  1884 DNAT       udp  --  *      *       0.0.0.0/0            127.0.0.11           udp dpt:53 to:127.0.0.11:41287

出现了这个奇怪的41287端口,这样就能解释netstat的输出内容了,整个解析过程到此是:

  • 进程向127.0.0.11:53发出DNS请求
  • iptables将发送到127.0.0.11:53的数据包NAT到127.0.0.11:41287

谁在监听41287端口?显然不可能是容器内的进程:

1
2
3
$ nsenter -t $(docker inspect -f {{.State.Pid}} b884630283ce) -n lsof -i :41287
COMMAND  PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
dockerd 7041 root   29u  IPv4 123505      0t0  UDP 127.0.0.11:41287

可以发现是dockerd在监听这个端口,宿主机的dockerd进程通过 setns 系统调用可以进入其他network namespace。

查看dockerd进程的系统调用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
$ strace -p 7041 -f -s 10000

(...省略无关内容)
[pid  7055] setns(38, CLONE_NEWNET) = 0
[pid  7055] socket(AF_INET, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_IP) = 39
[pid  7055] setsockopt(39, SOL_SOCKET, SO_BROADCAST, [1], 4) = 0
[pid  7055] connect(39, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("192.168.29.2")}, 16) = 0
[pid  7055] getsockname(39, {sa_family=AF_INET, sin_port=htons(50886), sin_addr=inet_addr("172.18.0.2")}, [112->16]) = 0
[pid  7055] getpeername(39, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("192.168.29.2")}, [112->16]) = 0
[pid  7055] setns(10, CLONE_NEWNET)     = 0
[pid  7055] write(39, "w\364\1\0\0\1\0\0\0\0\0\0\00278\003143\003241\003199\7in-addr\4arpa\0\0\f\0\1", 45) = 45
(...省略无关内容)


其中192.168.29.2是容器ip,172.18.0.2是宿主机/etc/resolv.conf配置的nameserver。

所以,当nameserver为127.0.0.11:53时,其域名解析的流程如下:

  • 进程向127.0.0.11:53发出DNS请求
  • iptables将发送到127.0.0.11:53的数据包NAT到127.0.0.11:41287,也就是dockerd进程
  • dockerd通过setns系统调用进入容器的network namespace,然后再向真正的DNS服务器发出请求,由于请求是从容器的network namespace发出的,请求包的IP地址是容器的IP地址
  • dockerd收到DNS响应,将结果写入127.0.0.11:41287
  • iptables再将127.0.0.11:41287重写回127.0.0.11:53,进程接收到DNS响应,请求完毕

注意:如果dns服务器的53端口做了限制,只对宿主机ip放行,其他ip全部拒绝的话,那么容器内是无法访问dns服务器的。所以这种情形需要放行容器的网段。

host模式或者使用默认的bridge网桥时,docker容器内nameserver和宿主机的/etc/resolv.conf保持一致

源码分析:

  1. containerStart

initializeNetworking()是重点,/var/lib/docker/containers/xxxx/resolv.conf文件的内容就是在此处被覆盖的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func (daemon *Daemon) containerStart(container *container.Container, checkpoint string, checkpointDir string, resetRestartManager bool) (err error) {
    /*
        其他代码
    */

    // 初始化网络
    if err := daemon.initializeNetworking(container); err != nil {
        return err
    }

    /*
        其他代码
    */

    // 创建容器
    err = daemon.containerd.Create(context.Background(), container.ID, spec, createOptions)

    // 启动容器
    pid, err := daemon.containerd.Start(context.Background(), container.ID, checkpointDir,
        container.StreamConfig.Stdin() != nil || container.Config.Tty,
        container.InitializeStdio)

    return nil
  1. initializeNetworking
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func (daemon *Daemon) initializeNetworking(container *container.Container) error {
    /*
        其他代码
    */

    if err := daemon.allocateNetwork(container); err != nil {
        return err
    }

    return container.BuildHostnameFile()
}
  1. allocateNetwork()
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func (daemon *Daemon) allocateNetwork(container *container.Container) error {
    /*
         其他代码
    */
    defaultNetName := runconfig.DefaultDaemonNetworkMode().NetworkName()
    if nConf, ok := container.NetworkSettings.Networks[defaultNetName]; ok {
        if err := daemon.connectToNetwork(container, defaultNetName, nConf.EndpointSettings, updateSettings); err != nil {
            return err
        }
    }

    /*
        其他代码
    */

    return nil
}
  1. connectToNetwork
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
func (daemon *Daemon) connectToNetwork(container *container.Container, idOrName string, endpointConfig *networktypes.EndpointSettings, updateSettings bool) (err error) {
    /*
        其他代码
    */

    if sb == nil {
        // 创建sanbox
        sb, err = controller.NewSandbox(container.ID, options...)

        /*
            其他代码
        */
    }
    /*
        其他代码
    */
    return nil
}
  1. NewSandbox
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func (c *container) NewSandbox(containerID string, options ...SandboxOption) (Sandbox, error) {
    var sb *sandbox
    /*
        其他代码
    */
    sb = &sandbox{
        id:             sandboxID,
        containerID:    containerID,
        endpoints:      []*endpoint{},
        epPriority:     map[string]int{},
        populatedEndpoints: map[string]struct{}{},
        config:         containerConfig{},
        controller:     c,
        extDNS:         []extDNSEntry{},
    }
    // setupResolutionFiles() 会初始化DNS相关的配置
    if err = sb.setupResolutionFiles(); err != nil {
        return nil, err
    }
    /*
        其他代码
    */
    return sb, nil
}
  1. setupResolutionFiles
1
2
3
4
5
6
7
func (sb *sandbox) setupResolutionFiles() error {
    /*
        其他代码
    */

    return sb.setupDNS()
}
  1. setupDNS
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func (sb *sandbox) setupDNS() error {
    /*
        其他代码
    */

    // newRC.Content是宿主机/etc/resolv.conf文件的内容(可能过滤部分内容)
    // 将newRC.Content写到/var/lib/docker/containers/xxxx/resolv.conf文件
    if err := ioutil.WriteFile(sb.config.resolvConfPath, newRC.Content, filePerm); err != nil {
    }

    /*
        其他代码
    */

    return nil
}

宿主机 /etc/resolv.conf 动态变化

在容器启动时,Docker 会将宿主机的 /etc/resolv.conf 内容复制到容器中。

如果宿主机 DNS 配置后续发生变化,已运行的容器不会自动更新,但新启动的容器会继承最新配置。


本文发表于 0001-01-01,最后修改于 0001-01-01。

本站永久域名「 jiavvc.top 」,也可搜索「 后浪笔记一零二四 」找到我。


上一篇 « 下一篇 »

赞赏支持

请我吃鸡腿 =^_^=

i ysf

云闪付

i wechat

微信

推荐阅读

Big Image