蛮荆

Docker 网络原理概览

2023-04-03

概述

Docker 原生网络是基于 Linux 的 网络命名空间(net namespace) 和 虚拟网络设备(veth pair)实现的。 当 Docker 进程启动时,会在宿主机上创建一个名称为 docker0虚拟网桥,在该宿主机上启动的 Docker 容器会连接到这个虚拟网桥上。

$ ifconfig

# 输出如下

docker0: ... mtu 1500
inet 172.17.0.1  netmask 255.255.0.0  broadcast 172.17.255.255
...

虚拟网桥的工作方式和物理交换机类似,宿主机上所有的容器通过虚拟网桥连接在一个二层网络中

docker0 子网中分配一个 IP 给容器使用,并设置 docker0 的 IP 地址为容器的默认网关。在宿主机上创建一对虚拟网卡 veth pair 设备, Dockerveth pair 设备的一端放在新创建的容器中,并命名为 eth0(容器的网卡), 另一端放在宿主机中,以 vethxxx 类似的名字命名, 并将这个网络设备连接到 docker0 网桥中。

Docker 会自动配置 iptables 规则和配置 NAT,便于连通宿主机上的 docker0 网桥,完成这些操作之后,容器就可以使用它的 eth0 虚拟网卡,来连接其他容器和访问外部网络。

Docker 中的网络接口默认都是虚拟的接口,Linux 在内核中通过 数据复制 实现接口之间的数据传输,可以充分发挥数据在不同 Docker 容器或容器与宿主机之间的转发效率, 发送接口发送缓存中的数据包,将直接复制到接收接口的缓存中,无需通过物理网络设备进行交换

# 查询主机上 veth 设备
$ ifconfig | grep veth*

veth06f40aa: 
...
vethfdfd27a:

图片来源: https://www.suse.com/c/rancher_blog/introduction-to-container-networking/

虚拟网桥 docker0 通过 iptables 配置与宿主机器上的网卡相连,符合条件的请求都会通过 iptables 转发到 docker0, 然后分发给对应的容器

# 查看 docker 的 iptables 配置
$ iptables -t nat -L

# 输出如下

Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination
DOCKER     all  --  anywhere             anywhere             ADDRTYPE match dst-type LOCAL

Chain DOCKER (2 references)
target     prot opt source               destination
RETURN     all  --  anywhere             anywhere

网络驱动

Docker 的网络子系统支持插拔式的驱动程序,默认存在多个驱动程序,并提供核心网络功能。

名称 描述
bridge 默认的网络设备,当应用程序所在的容器需要通信时使用
host 移除容器与宿主机之间的网络隔离,直接使用宿主机的网络
overlay 将多个容器连接,并使集群服务能够相互通信
ipvlan 使用户可以完全控制 IPv4 和 IPv6 寻址
macvlan 可以为容器分配 MAC 地址
none 禁用所有网络
Network plugins 通过 Docker 安装和使用第三方网络插件

图片来源: Docker——容器与容器云

Docker daemon 通过调用 libnetwork 提供的 API 完成网络的创建和管理等功能。libnetwork 中使用了 CNM 来完成网络功能, CNM 中主要有沙盒(sandbox)、端点(endpoint)和网络(network)3 种组件。

  • 沙盒:一个沙盒包含了一个容器网络栈的信息。一个沙盒可以有多个端点和多个网络,沙盒可以对容器的接口、路由和 DNS 设置等进行管理,沙盒的实现可以是 Linux network namespace、FreeBSD Jail或者类似的机制
  • 端点:一个端点可以加入一个沙盒和一个网络。一个端点只属于一个网络和一个沙盒,端点的实现可以是 veth pair、Open vSwitch 内部端口或者相似的设备
  • 网络:一个网络是一组可以直接互相联通的端点。一个网络可以包含多个端点,网络的实现可以是Linux bridge、VLAN 等

bridge 模式

bridge 是默认的网络模式,为容器创建独立的网络命名空间,容器具有独立的网卡等所有的网络栈。使用该模式的所有容器都是连接到 docker0 这个网桥, 作为 虚拟交换机 使容器可以相互通信,但是由于宿主机的 IP 地址与容器 veth pair 的 IP 地址不在同一个网段,所以为了和宿主机以外的网络通信, Docker 采用了端口绑定的方式,也就是通过 iptables 的 NAT,将宿主机上的端口流量转发到容器。

bridge 模式已经可以满足 Docker 容器最基本的使用需求了,但是其与外界通信时使用 NAT,增加了通信的复杂性,在复杂场景下使用会有限制

$ docker network inspect bridge

# 输出如下 (节选部分信息)
[
    {
        "Name": "bridge",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.17.0.0/16",
                    "Gateway": "172.17.0.1"
                }
            ]
        },
        "Containers": {
            # 使用 bridge 网络的容器列表
        },
    }
]

通过上面的输出可以看到,虚拟网桥 的 IP 地址就是 bridge 网络类型的网关地址

我们可以从输出的 Containers 容器列表中找一个容器,查看其网络类型和配置:

$ docker inspect 容器ID

# 输出如下 (节选部分信息)
[
  ...
  
  "NetworkSettings": {
    "Bridge": "",
    "Gateway": "172.17.0.1",
    "IPAddress": "172.17.0.4",
    "Networks": {
        "bridge": {
            "Gateway": "172.17.0.1",
            "IPAddress": "172.17.0.4",
        }
    }

  ...
]

通过上面的输出可以看到,虚拟网桥 的 IP 地址就是 bridge 网络类型的容器的网关地址

实现机制

在 iptables 做了 DNAT 规则,实现端口转发功能:

# iptables 配置查看
$ iptables -t nat -vnL

# 输出如下
Chain PREROUTING (policy ACCEPT 37M packets, 2210M bytes)

...

0     0 DNAT       tcp  --  !docker0 *       0.0.0.0/0            0.0.0.0/0            tcp dpt:8080 to:172.17.0.4:80

当容器需要将端口映射到宿主机时,Docker 会自动为该容器分配一个 IP 地址,同时新增一个 iptables 规则

host 模式

容器不会获得一个独立的网络命名空间,而是和宿主机共用一个。 容器不会虚拟出自己的网卡,配置自己的IP等,而是直接使用宿宿主机的。 但是容器的其他方面,如文件系统、进程列表等还是和宿宿主机隔离的,容器对外界是完全开放的,能够访问到宿主机,就能访问到容器。

host 模式降低了容器与容器之间、容器与宿主机之间网络层面的隔离性,虽然有性能上的优势,但是引发了网络资源的竞争与冲突,因此适用于容器集群规模较小的场景

启动一个网络类型为 hostNginx 容器:

$ docker run -d --net host nginx

Unable to find image 'nginx:latest' locally
latest: Pulling from library/nginx
...
f202870092fc40bc08a607dddbb2770df9bb4534475b066f45ea35252d6e76e2

查看网络类型为 host 的容器列表:

$ docker network inspect host

# 输出如下 (节选部分信息)
[
    {
        "Name": "host",
        "Scope": "local",
        "Driver": "host",
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "Containers": {
           # 使用 host 网络的容器列表  
           "f202870092fc40bc08a607dddbb2770df9bb4534475b066f45ea35252d6e76e2": {
                "Name": "frosty_napier",
                "EndpointID": "7306a8e4103faf4edd081182f015fa9aa985baf3560f4a49b9045c00dc603190",
                "MacAddress": "",
                "IPv4Address": "",
                "IPv6Address": ""
            }        
        },
    }
]

查看 Nginx 容器网络类型和配置:

$ docker inspect f202870092fc4

# 输出如下 (节选部分信息)
[
  ...
  
  "NetworkSettings": {
    "Bridge": "",
    "Gateway": "",
    "IPAddress": "",
    "Networks": {
        "host": {
            "Gateway": "",
            "IPAddress": "",
        }
    }
  ...
]

通过上面的输出可以看到,Nginx 容器使用的网络类型是 host,没有独立的 IP。

查看 Nginx 容器 IP 地址:

# 进入容器内部 shell

$ docker exec -it f202870092fc4 /bin/bash

# 安装 ip 命令

$ apt update && apt install -y iproute2

# 查看 IP 地址
$ ip a

# 输出如下
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    ...
2: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
    ...
10: eth0@if11: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    ...
    
# 退出容器,查看宿主机 IP 地址
$ exit
$ ip a

# 输出如下
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    ...
2: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
    ...
10: eth0@if11: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    ...

通过上面的输出可以看到,Nginx 容器内部并没有独立的 IP,而是使用了宿主机的 IP。

查看宿主机的端口监听状态:

$ sudo netstat -ntpl

# 输出如下
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      1378/nginx: master
tcp6       0      0 :::80                   :::*                    LISTEN      1378/nginx: master

通过上面的输出可以看到,监听 80 端口的进程为 nginx, 而非 docker-proxy

none 模式

容器拥有自己的 Network Namespace,但是并不进行任何网络配置。也就意味着该容器没有网卡、IP、路由等信息,需要手动为容器添加网卡、配置 IP 等, none 模式下的容器会完全隔离,容器中只有 lo 这个 loopback(回环网络)网卡用于进程通信。

none 模式为容器做了最少的网络设置,在没有网络配置的情况下,通过自定义配置容器的网络,提供了最高的灵活性。

启动一个网络类型为 hostNginx 容器:

$ docker run -d --net none nginx

Unable to find image 'nginx:latest' locally
latest: Pulling from library/nginx
...
d2d0606b7d2429c224e61e06c348019b74cd47f0b8c85347a7cdb8f1e30dcf86

查看网络类型为 none 的容器列表:

$ docker network inspect none

# 输出如下 (节选部分信息)
[
    {
        "Name": "none",
        "Scope": "local",
        "Driver": "null",
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "Containers": {
           # 使用 none 网络的容器列表  
           "d2d0606b7d2429c224e61e06c348019b74cd47f0b8c85347a7cdb8f1e30dcf86": {
                "Name": "hardcore_chebyshev",
                "EndpointID": "b8ff645671518e608f403818a31b1db34d7fce66af60373346ea3ab673a4c6b2",
                "MacAddress": "",
                "IPv4Address": "",
                "IPv6Address": ""
            }        
        },
    }
]

查看 Nginx 容器网络类型和配置:

$ docker inspect d2d0606b7d242

# 输出如下 (节选部分信息)
[
  ...
  
  "NetworkSettings": {
    "Bridge": "",
    "Gateway": "",
    "IPAddress": "",
    "Networks": {
        "none": {
            "Gateway": "",
            "IPAddress": "",
        }
    }
  ...
]

通过上面的输出可以看到,Nginx 容器使用的网络类型是 none,没有独立的 IP。

查看 Nginx 容器 IP 地址:

# 进入容器内部 shell

$ docker exec -it d2d0606b7d242 /bin/bash

# 访问公网链接

$ curl -I "https://www.docker.com"

curl: (6) Could not resolve host: www.docker.com

# 为什么会报错呢? 这是因为当前容器没有网卡、IP、路由等信息,是完全独立的运行环境,所以没有办法访问公网链接。

# 查看 IP 地址
$ hostname -I

# 没有任何输出,该容器没有 IP 地址

查看宿主机的端口监听状态:

$ docker port d2d0606b7d242

或者

$ sudo netstat -ntpl | grep :80

# 没有任何输出,Nginx 进程运行在容器中,端口没有映射到宿主机

container 模式

host 模式类似,容器与指定的容器共享网络命名空间。这两个容器之间不存在网络隔离,但它们与宿主机以及其他的容器存在网络隔离。 该模式下的容器可以通过 localhost 来访问同一网络命名空间下的其他容器,传输效率较高,且节约了一定的网络资源。在一些特殊的场景中非常有用,例如 k8s 的 Pod。

其他模式

出于篇幅考虑,这里不再赘述其他网络模式,感兴趣的读者可以根据文章末尾的引用连接自行阅读。

网络驱动概述

  • 当需要多个容器在同一台宿主机上进行通信时,使用 bridge
  • 当网络栈不应该与宿主机隔离,但是希望容器的其他方面被隔离时,使用 host
  • 当需要在不同宿主机上运行的容器进行通信时,使用 overlay
  • 当从虚拟机迁移或需要使容器看起来像物理宿主机时,使用 Macvlan, 每个容器都有一个唯一的 MAC 地址
  • 当需要将 Docker 与专门的网络栈集成,使用 Third-party

Docker 和 iptables

如果在公网可以访问的服务器运行 Docker,需要对应的 iptables 规则来限制访问主机上的容器或其他服务。

在 Docker 规则之前添加 iptables 规则

Docker 安装了两个名为 DOCKER-USERDOCKER 的自定义 iptables 链,确保传入的数据包始终先由这两个链进行检查。

# 可以通过该命令查看

$ iptables -L -n -v | grep -i docker

Docker 的所有 iptables 规则都被添加到 Docker 链中,不要手动修改此链 (可能会引发问题)。 如果需要添加在一些在 Docker 之前加载的规则,将它们添加到 DOCKER-USER 链中,这些规则应用于 Docker 自动创建的所有规则之前。

添加到 FORWARD 链中的规则在这些链之后进行检测,这意味着如果通过 Docker 公开一个端口,那么无论防火墙配置了什么规则,该端口都会被公开。 如果想让这些规则在通过 Docker 暴露端口时仍然适用,必须将这些规则添加到 DOCKER-USER 链中。

限制到 Docker 主机的连接

默认情况下,允许所有 外部 IP 连接 Docker 主机,为了只允许特定的 IP 或网络访问容器,在 DOCKER-USER 过滤器链的顶部插入一个规则。

例如,只允许 192.168.1.1 访问:

# 假设输入接口为 eth0
$ iptables -I DOCKER-USER -i eth0 ! -s 192.168.1.1 -j DROP

也可以允许来自源子网的连接,例如,允许 192.168.1.0/24 子网的用户访问:

# 假设输入接口为 eth0
$ iptables -I DOCKER-USER -i eth0 ! -s 192.168.1.0/24 -j DROP

阻止 Docker 操作 iptables

Docker 引擎的配置文件 /etc/docker/daemon.json 设置 iptables 的值为 false,但是最好不要修改,因为这很可能破坏 Docker 引擎的容器网络。

为容器设置默认绑定地址

默认情况下,Docker 守护进程将公开 0.0.0.0 地址上的端口,即主机上的任何地址。如果希望将该行为更改为仅公开内部 IP 地址上的端口,则可以使用 --ip 选项指定不同的IP地址。

集成到防火墙

如果运行的是 Docker 20.10.0 或更高版本,在系统上启用了 iptables, Docker 会自动创建一个名为 docker 的防火墙区域, 并将它创建的所有网络接口 (例如 docker0 ) 加入到 docker 区域,以允许无缝组网。

运行命令将 docker 接口从防火墙区域中移除:

firewall-cmd --zone=trusted --remove-interface=docker0 --permanent
firewall-cmd --reload

Reference

扩展阅读

转载申请

本作品采用 知识共享署名 4.0 国际许可协议 进行许可,转载时请注明原文链接,图片在使用时请保留全部内容,商业转载请联系作者获得授权。