docker的iptables-nat链
注意:如下是在/etc/docker/daemon.json中配置"userland-proxy": false 时的iptables规则。
# 将外部主机发往本机的流量引向DOCKER链
# --dst-type LOCAL:匹配 目标是本机IP 的流量(包括物理网卡IP、虚拟IP、localhost等)
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
# 对于从本机进程发出的数据包(OUTPUT链),如果其目标地址是本机的某个IP地址,也将其跳转到 DOCKER链。
# 容器内访问宿主机时触发
-A OUTPUT -m addrtype --dst-type LOCAL -j DOCKER
# 使用127.0.0.1访问容器时,将源ip从127.0.0.1改为物理网卡的ip
-A POSTROUTING -o docker0 -m addrtype --src-type LOCAL -j MASQUERADE
# 对于源地址属于 172.31.234.0/24网段(这是您的容器网络),且出口网卡不是 docker0(即数据包是发往宿主机外部网络)的流量,执行 MASQUERADE动作。
# 容器内访问外网时触发
-A POSTROUTING -s 172.31.234.0/24 ! -o docker0 -j MASQUERADE
# 容器内访问自己映射出去的端口时触发。
-A POSTROUTING -s 172.31.234.5/32 -d 172.31.234.5/32 -p tcp -m tcp --dport 9120 -j MASQUERADE
# DNAT规则
-A DOCKER -p tcp -m tcp --dport 9120 -j DNAT --to-destination 172.31.234.5:9120
为何有了 DNAT,还需要 docker-proxy 进程呢?
$ ps -ef | grep docker-proxy
docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 9120 -container-ip 172.31.234.5 -container-port 9120
Docker在IPv6网络下并未在ip6tables上为容器添加相应的DNAT规则,如果关闭docker-proxy,容器外部无法访问容器内部网络。在不借助ipv6nat等外部工具的情况下,docker-proxy是IPv6场景下的刚需。
在老内核(2.6.x)环境下,容器内部通过hairpin方式访问自己暴露的服务时,内核bridge不允许将包发送回到源接口。虽然新内核可以通过开启hairpin mode解决,但老内核无法启用此模式,此时必须依赖docker-proxy
docker-proxy和iptables的DNAT规则是双冗余机制,只要存在一种机制即可实现端口映射。这种设计确保了在网络配置复杂或iptables规则意外失效时,服务仍然可用。
在标准IPv4环境且不使用老内核的情况下,可以通过在/etc/docker/daemon.json中配置"userland-proxy": false来关闭docker-proxy,完全依赖iptables DNAT实现端口映射。
生产环境建议禁用userland-proxy,完全依赖iptables DNAT,既能提升性能又能减少资源消耗。
使用自定义网桥时,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保持一致
源码分析:
- 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
|
- 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()
}
|
- 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
}
|
- 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
}
|
- 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
}
|
- setupResolutionFiles
1
2
3
4
5
6
7
|
func (sb *sandbox) setupResolutionFiles() error {
/*
其他代码
*/
return sb.setupDNS()
}
|
- 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
」,也可搜索「 极客油画 」找到我。
上一篇 «
下一篇 »