原理

网络组成

docker网络主要由三部分组成:CNM、libnetwork和驱动

  • CNM是设计标准,其规定了docker网络架构的基础组成要素
  • libnetwork是CNM的具体实现
  • 驱动通过实现特定网络拓扑的方式来扩展该模型的能力

docker 网络抓包 docker networks_IP

CNM

CNM定义了三种基本元素:

  • 沙盒(sandbox):是一个独立的网络栈,其中包括以太网接口、端口、路由以及DNS配置
  • 终端(endpoint):就是虚拟网络接口,代表网络上可以挂载容器的接口,会分配IP地址,负责将沙盒连接到网络
  • 网络(network):是网桥的实现,可以连通多个接入点

docker 网络抓包 docker networks_IP_02


沙盒被放在容器内部,为容器提供网络连接

docker 网络抓包 docker networks_IP_03


从上面可以看出,容器A只有一个接口(终端)并连接到了网络A。容器B有两个接口(终端)并且分别接入了网络A和网络B。容器A和容器B之间是可以相互通信的,因为都接入了网络A。但是,如果没有三层路由器的支持,容器B的两个终端之间是不能通信的

一个终端对应一个网络,如果需要接入多个网络,就需要多个中断。

如下图,虽然容器A和容器B运行在同一个主机,但是其网络堆栈上在操作系统层面是相互独立的,这一点由沙盒机制保证

docker 网络抓包 docker networks_网络_04

libnetwork

之前,网络部分代码都在daemon中。从1.7.0版本开始,docker正式把网络与存储实现以插件化形式分别剥离开来,允许用户通过指令来选择不同的后端实现。剥离出来的网络实现叫做libnetwork

libnetwork实现了CNM中定义的三个组件,还实现了本地服务发现,基于Ingress的容器负责均衡,以及网络控制层和管理层功能

驱动

如果说libnetwork实现了控制层和管理层的话,那么驱动就负责实现数据层,比如,网络连通性和隔离性、创建网络对象等

docker 网络抓包 docker networks_docker 网络抓包_05


docker封装了很多内置驱动,通常叫做原生驱动或者本地驱动,比如bridge、overlay、macvlan。第三方写的docker网络驱动叫做远程驱动,比如calico

每个驱动都负责其上所有网络资源的创建和管理

分类

单机桥接网络

  • 单机:表示网络只能在单个docker主机上运行,并且只能与所在docker主机上的容器进行连接
  • 桥接表示二层交换机

每个docker主机都有一个默认的单机桥接网络,在linux上网络名称叫做bridge,除非通过命令行创建容器时指定参数--network,否则,默认情况下,新创建的容器都会连接到该网络

每个主机上的默认网络如下:

$ docker network ls
NETWORK ID     NAME      DRIVER    SCOPE
69f930d5f643   bridge    bridge    local

查看底层细节:

$ docker network inspect bridge
[
    {
        "Name": "bridge",
        "Id": "69f930d5f643c5b44e9f7febc4b63559f859fb092e0be9c179438c0a281c26a6",
        "Created": "2022-06-21T17:57:45.874414734+08:00",
        "Scope": "local",
        "Driver": "bridge", // docker网络由bridge驱动创建
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default", 
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.17.0.0/16",
                    "Gateway": "172.17.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "f5eadfe98cca28861e9bdc21a0b7d46aec7892863ded8e6d4c6a4ab1716280aa": {
                "Name": "tongjian_gb28181",
                "EndpointID": "b897ebf3e1620d27e1ebfff80990a066a685f4a34c383fcd866badf748a9232d",
                "MacAddress": "02:42:ac:11:00:02",
                "IPv4Address": "172.17.0.2/16",
                "IPv6Address": ""
            }
        },
        "Options": {
            "com.docker.network.bridge.default_bridge": "true",
            "com.docker.network.bridge.enable_icc": "true",
            "com.docker.network.bridge.enable_ip_masquerade": "true",
            "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
            "com.docker.network.bridge.name": "docker0",
            "com.docker.network.driver.mtu": "1500"
        },
        "Labels": {}
    }
]

在linux docker中,默认的"bridge"网络被映射到内部中为"docker0"的linux网桥,可以通过如下命令查看:

$ docker network inspect bridge | grep bridge.name
 "com.docker.network.bridge.name": "docker0",

docker 网络抓包 docker networks_docker_06


可以通过如下命令查看linux系统中的网桥:

$ brctl show
bridge name	bridge id		STP enabled	interfaces
docker0		8000.02420ec62db1	no		vethfc4efee

一行表示一个网桥(两行表示两个):

  • 名字叫做docker0
  • 当前没有开启SIP(STP enabled )
  • 有一个设备接入(interfaces)

网桥

  • docker服务默认会创建一个名称为docker0的linux网桥(其上有一个docker0内部接口),它在内核层联通了其他的物理或者虚拟网卡,这就将所有容器和本地主机都放在同一个物理网络
  • docker默认指定了docker0接口的IP地址和子网掩码(默认地址一般是172.17.0.1),让主机和容器之间可以通过网桥相互通信
$ ifconfig
docker0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 172.17.0.1  netmask 255.255.0.0  broadcast 172.17.255.255
        inet6 fe80::42:eff:fec6:2db1  prefixlen 64  scopeid 0x20<link>
        ether 02:42:0e:c6:2d:b1  txqueuelen 0  (以太网)
        RX packets 7180  bytes 40746725 (40.7 MB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 21782  bytes 10175709 (10.1 MB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
  • 每次创建一个新容器的时候,docker从可用的地址段中选择一个空闲的IP地址分配给容器的eth0端口,并使用本地主机上的docker0接口的IP地址作为容器的默认网关
  • 也就是说,docker0是连接容器的本地网桥,可以通过如下命令查看

网络启动过程(原理)

  • docker服务启动时会首先在主机上自动创建一个docker0虚拟网桥,实际上是一个linux网桥。网桥可以理解为一个软件交换机,负责挂载其上的接口之间进行包转发
  • 同时,docker随机分配一个本地未占用的私有网段中的一个地址给docker接口。此后启动的容器内的网口也会自动分配一个该网段的地址
  • 当创建一个docker容器的时候,同时会创建了一对veth pair互联接口。当向任一一个接口发送包时,另外一个接口自动接收到相同的包。互联接口一端位于容器内,即eth0;另一端在本地并被挂载到docker0网桥,名称以veth开头。通过这种方式,主机可以与容器通信,容器之间也可以相互通信。如此一来,docker就创建了在主机和所有容器之间的一个虚拟共享网络

网络相关参数

下面是与docker网络相关的命令参数

下面命令只能在docker服务启动时才能配置,修改后重启生效

  • -b BRIDGE or --bridge = BRIDGE:指定容器挂载的网桥
  • --bip = CIDR:定制docker0的掩码
  • -H SOCKET ... or --host = SOCKET ...:docker服务端接收命令的通道
  • --icc=true | false:是否支持容器之间进行通信
  • --ip-forward=true | false:是否打开转发功能
  • --iptables = true | false:禁止docker添加iptables规则
  • --mtu=BYTES:容器网络中的MTU

下面命令既可以在启动服务时指定(为默认值),也可以在docker容器启动时指定(可以覆盖默认值)

  • --dns=IP_ADDRESS:使用指定的DNS服务器
  • --dns-opt="":指定DNS选项
  • --dns-search=DOMAIN:指定DNS搜索域

下面命名只能在docker [container] run命令执行时使用(只针对容器)

docker 网络抓包 docker networks_运维_07


其中,–net支持如下五种模式:

docker 网络抓包 docker networks_运维_08


docker 网络抓包 docker networks_网络_09

配置容器DNS和主机名

Docker服务启动后会默认启用一个内嵌的DNS服务,来自动解析同一个网络中的容器主机名和地址,如果无法解析,则通过容器内的DNS相关配置进行解析。

相关配置文件

容器中主机名和DNS配置信息可以通过三个系统配置文件来管理:/etc/resolv.conf、/etc/hostname、/etc/hosts

启动一个容器,在容器中使用mount命令可以看到这三个挂载信息:

docker 网络抓包 docker networks_网络_10

  • docker启动容器时,会从宿主机上复制/etc/resolv.conf文件,并删除掉其中无法连接到的DNS服务器
  • /etc/hosts文件中默认只记录了容器自身的地址和名称
  • /etc/hostname记录容器的主机名

容器内修改配置文件

容器运行时,可以在运行中的容器里面直接边界/etc/resolv.conf、/etc/hostname、/etc/hosts
。但是这些修改是临时的,只在运行中的容器中保留,容器终止或者重启后并不会被保存下来,也不会被docker commit提交

通过参数指定

如果想要自定义容器的配置,可以在创建或者启动容器是利用下面的参数指定,注意一般不推荐和-net=host一起使用,会破坏宿主机上的配置信息。

docker 网络抓包 docker networks_运维_11

容器访问控制

容器的访问控制主要通过linux上的自带的iptables防火墙软件来进行管理和实现。

容器访问外部网络

  • 容器默认指定了网关为docker0网桥上的docker0内部接口。docker0内部接口同时也是宿主机的一个本地接口
  • 因此,容器默认情况下可以访问到宿主机本地网络
  • 如果容器响应通过宿主机访问到外部网络,必须通过宿主机进行转发

在宿主机中,检查转发是否打开,1表示打开了

$ sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 1

如果不为1,需要手动打开:

$ sysctl -w net.ipv4.ip_forward = 1

docker服务启动时会默认开启--ip-forward=true,自动配置宿主机系统的转发规则

容器之间相互访问

前提:

  • 网络拓扑是否联通。默认情况下,所有容器都会连接到docker0网桥上,这意味默认联通
  • 本地系统的防火墙iptables是否允许通过

容器间的访问有两种:

(1)访问所有端口

  • 当启动docker服务时,默认会添加一条“允许”转发策略到iptables的FORWARD链上。通过--icc=true|false(默认为true)参数可以控制默认的策略
  • 当然,可以在docker配置文件中配置DOCKER_OPTS=--icc=false来默认禁止容器之间的相互访问

(2)访问指定端口

  • 在通过-icc=false 禁止容器间相互访问后,仍可以通过--link=CONTATNER_NAME:ALIAS选项来允许访问指定容器的开放端口。

映射容器端口到宿主主机

默认情况下,容器可以主动访问到外部网络的连接,但是外部网络无法访问到容器

容器访问外部

(1)查看容器的IP地址

$ docker inspect --format='{{.NetworkSettings.IPAddress}}' ID  //# 查看 容器ip 地址
172.17.0.2

$ docker inspect --format '{{.Name}} {{.State.Running}}' ID   // 查看容器运行状态
/tongjian_gb28181 true

(2)docker0的地址

$ ifconfig
docker0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 172.17.0.1  netmask 255.255.0.0  broadcast 172.17.255.255
        inet6 fe80::42:eff:fec6:2db1  prefixlen 64  scopeid 0x20<link>
        ether 02:42:0e:c6:2d:b1  txqueuelen 0  (以太网)
        RX packets 7180  bytes 40746725 (40.7 MB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 21782  bytes 10175709 (10.1 MB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
ens32: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.0.59  netmask 255.255.255.0  broadcast 192.168.0.255
        inet6 fe80::f008:602b:2a4a:16ea  prefixlen 64  scopeid 0x20<link>
        ether 00:0c:29:6c:92:c9  txqueuelen 1000  (以太网)
        RX packets 364328  bytes 429651353 (429.6 MB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 151030  bytes 52958278 (52.9 MB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

从上面可以看出,容器内部的网络地址为172.17.0.2,本地网络地址为192.168.0.59。容器要能够访问外部网络,源地址不能为172.17.0.2,需要进行源地址映射,修改为本地IP地址192.168.0.59

(3)映射

映射是通过iptables的源地址伪装实现的。查看主机nat表上的POSTROUTING链的规则,噶爱链负责网包要离开主机前,改写其源地址

$ iptables -t nat -nvL POSTROUTING
Chain POSTROUTING (policy ACCEPT 359 packets, 26845 bytes)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 MASQUERADE  all  --  *      !docker0  172.17.0.0/16        0.0.0.0/0

从上面可以看出,规则表示将所有源地址在172.17.0.0/16网段,而且不是从docker0接口发出的流量(即从容器中出来的流量),动态伪装为系统网卡发出。

外部访问容器

端口映射允许将某个容器端口映射到docker主机端口上。对于配置中指定的docker端口,任何发送到该端口的流量,都会被转发到容器。可以在docker run时通过-p或者-P参数来启用。

docker 网络抓包 docker networks_docker 网络抓包_12


举个例子:

(1)将容器80端口映射到外部的5000端口

$ docker container run -d --name web --publish 5000:80 nginx

(2)确定端口映射

$ docker port web
80/tcp->0.0.0.0:5000

表示容器的80端口已经映射到主机所有接口上的5000端口

现在外部就可以通过docker主机端口5000,来访问容器内部的80端口了。

缺点:在只有单一容器的情况下,它可以绑定到主机的任意端口,这意味着其他容器就不能再使用已经被nginx容器占用的5000端口了。这也是单机桥接网络只适用于本地开发环境以及非常小的应用的原因

不管是哪种方法,其实也是在本地的iptables的nat表中添加相应的规则,将访问外部IP地址的包进行目标地址DNAT,将目标地址修改为容器的IP地址。

iptables -t nat -nvL

docker 网络抓包 docker networks_IP_13

关注两条链:

  • PREROUTING链:负责包到达网络接口,改写其目的地址,其中规则将所有流量都转发到DOCKER链
  • DOCKER链:将所有不是从docker0进来的包(意味着不是本地主机产生),同时目的端口为49153的修改其目的地址为172.17.0.2,目标端口修改为80

另外:

  • 规则映射地址为0.0.0.0,意味着接受主机来自所有网络接口上的流量。用户可以通过-p IP:host_port:container_port或者-p IP:port来指定绑定的外部网络接口,以制定更严格的访问规则
  • 如果希望映射绑定到某个固定的宿主机IP地址,可以在Docker配置文件中指定DOCKER_OPTS=“–ip=IP_ADDRESS”,之后重启docker服务即可生效