Docker容器网络浅析

  1. Docker 网络类型
  2. Linux网络类型
  3. bridge网络
    1. 默认bridge网络
    2. 自定义bridge网络
    3. bridge网络选项
    4. network connect
    5. Port Mapping
    6. 自定义bridge和默认bridge的差异
  4. host网络
  5. none网络
  6. 跨Network Namespace的通信
    1. 创建bridge network
      1. FILTER表
      2. NAT表
    2. 运行基于bridge的Container
      1. Veth
      2. Container之间通信
    3. 手动实现跨NS的通信
  7. overlay网络
    1. vxlan
      1. VXLAN Frame格式
      2. 手动创建VXLAN
    2. Docker Swarm集群
      1. Swarm集群网络
      2. Standalone Containers基于overlay网络
      3. Serivces网络
  8. 总结
  9. 参考

在2年之前对容器化技术之Linux Namespace的相关知识进行了学习和梳理,了解了Linux内核如何支持虚拟化的相关技术特性;本文开始对Docker是如何进行容器网络管理的进行阐述;阐述的内容路径主要参考:Docker 官方的Network相关的知识图谱,对容器网络进行深入浅出的解析。

通过这篇文章,希望能够对容器和主机之间是如何进行网络管理的,同一个主机的容器之间,跨主机的容器之间是如何通过现有的underlay网络实现容器层面的互通的。

Docker 网络类型

容器网络是指容器之间或与Docker工作负载之外的其他工作负载进行连接和通信的能力。

容器默认启用网络能力,它们可以进行出站连接,容器不知道自己连接到哪种网络,也不知道它们的对等方是否也是Docker工作负载。启用网络驱动的容器能看到一个具有IP地址、网关、路由表、DNS服务和其他网络详细信息的网络接口。

Docker容器网络支持以下几种默认的网络驱动类型:

桥接网络,Docker的默认网络驱动程序,它在Docker主机内创建一个内部网络,允许容器之间以及与主机机器进行通信。桥接网络从私有子网中为容器分配IP地址,并使用Docker内置的DHCP服务器来管理IP分配。

主机网络驱动程序,Docker容器共享主机机器的网络命名空间。这意味着容器可以直接访问主机的网络接口和端口,Container和主机共享网络堆栈。当应用程序需要直接访问主机的网络资源或性能至关重要时,此驱动程序非常有用。

无网络驱动网络,将容器与任何网络访问隔离开来,包括主机和其他容器。当容器不需要网络访问或出于安全原因需要网络隔离时,此驱动程序非常有用。

在不同主机上的Docker守护程序之间能够进行通信。这在多主机环境中特别有用,其中容器需要在不同物理或虚拟机之间进行通信。overlay网络使用VXLAN(虚拟可扩展局域网)技术封装和路由主机之间的网络流量。

IPvlan网络对IPv4和IPv6地址提供了完全控制。它们允许您创建子网并将IP地址直接分配给容器,与默认的桥接网络相比提供了更多的灵活性和控制。根据您的网络需求,IPvlan网络可以配置为L2(第二层)或L3(第三层)模式。

Macvlan驱动程序为每个容器分配唯一的MAC地址,使它们看起来像是网络上的独立物理设备。当应用程序需要直接访问物理网络或需要绕过Docker默认网络堆栈的场景时,此驱动程序非常有用。

Linux网络类型

在介绍Docker网络类型之前,我们先看一下Linux本身支持的网络设备类型,Linux为了满足虚拟化,网络隔离,流量控制等各种复杂不同场景的网络需求,引入了各种不同的网络技术,除了我们常见的ethernetloopback两种网卡,我们可以通过ip link工具创建和管理如下Linux已经支持的网络设备类型:

  1. bridge (以太网桥设备)
  • 内核引入时间:大约在Linux 2.2内核版本中引入(1999年)。
  • 功能:bridge设备充当以太网桥,用于将多个网络接口连接在一起,使它们像在同一个局域网中一样工作。
  • 实际应用:广泛应用于虚拟化环境,如KVM、QEMU、VirtualBox等,用于将虚拟机或容器的虚拟网卡桥接到宿主机的物理网卡上,实现不同网络间的通信。
  1. can (控制器局域网络接口)
  • 内核引入时间:大约在Linux 2.4内核版本中引入(2001年)。
  • 功能:用于支持控制器局域网(CAN),这是一种专为汽车、工业和嵌入式系统设计的通信协议。
  • 实际应用:主要用于汽车电子系统(如汽车的车载网络),工业自动化(如PLC控制系统)等领域。
  1. dummy (虚拟网络接口)
  • 内核引入时间:大约在Linux 2.0内核版本中引入(1996年)。
  • 功能:不进行实际的数据传输,主要用于网络配置和测试。
  • 实际应用:用于模拟网络设备,进行网络配置和测试,一些网络服务的测试环境中也会使用。
  1. ifb (中间功能块设备)
  • 内核引入时间:大约在Linux 2.6内核版本中引入(2005年)。
  • 功能:用于网络流量的转发和控制,通常用于流量整形和流量控制。
  • 实际应用:在需要对网络流量进行监控和控制的场景中使用,如流量整形和网络性能测试。
  1. ipoib (基于Infiniband的IP设备)
  • 内核引入时间:大约在Linux 2.6内核版本中引入(2005年)。
  • 功能:在Infiniband网络上传输IP数据包,实现高带宽和低延迟的网络通信。
  • 实际应用:用于高性能计算(HPC)和数据中心网络,特别是在需要低延迟和高带宽的应用中。
  1. macvlan (基于链路层地址的虚拟接口)
  • 内核引入时间:大约在Linux 2.6.22内核版本中引入(2007年)。
  • 功能:将一个物理接口分割成多个虚拟接口,每个虚拟接口都有独立的MAC地址。
  • 实际应用:在容器网络中,通过给每个容器分配一个独立的MAC地址,使其可以像独立的设备一样连接到网络。
  1. vcan (虚拟局域CAN接口)
  • 内核引入时间:大约在Linux 3.0内核版本中引入(2011年)。
  • 功能:用于模拟和测试控制器局域网络(CAN)。
  • 实际应用:用于CAN网络的开发和测试,不需要实际的CAN硬件,如汽车电子系统的开发测试环境。
  1. veth (虚拟以太网接口)
  • 内核引入时间:大约在Linux 2.6.24内核版本中引入(2008年)。
  • 功能:成对存在,一个接口连接到一个网络命名空间,另一个接口连接到另一个网络命名空间。
  • 实际应用:用于容器(如Docker、Kubernetes)和虚拟机之间的网络连接,实现网络隔离和通信。
  1. vlan (基于802.1q标签的虚拟局域网接口)
  • 内核引入时间:大约在Linux 2.2内核版本中引入(1999年)。
  • 功能:在同一物理网络上创建多个逻辑网络,通过802.1q VLAN标签区分。
  • 实际应用:用于网络隔离和分段,提高网络安全性和管理效率,广泛应用于企业网络。
  1. vxlan (虚拟扩展局域网)
  • 内核引入时间:大约在Linux 3.7内核版本中引入(2012年)。
  • 功能:在物理网络上创建虚拟网络隧道,实现跨数据中心的虚拟网络连接。
  • 实际应用:用于大规模云计算环境和数据中心的虚拟网络,如OpenStack中的虚拟网络实现。
  1. ip6tnl (IPv4/IPv6 over IPv6的虚拟隧道接口)
  • 内核引入时间:大约在Linux 2.6内核版本中引入(2005年)。
  • 功能:在IPv6网络上传输IPv4或IPv6数据包,实现不同协议之间的互通。
  • 实际应用:用于IPv4和IPv6网络的互联互通,如IPv6过渡技术中的应用。
  1. ipip (IPv4 over IPv4的虚拟隧道接口)
  • 内核引入时间:大约在Linux 2.2内核版本中引入(1999年)。
  • 功能:在IPv4网络上传输IPv4数据包,用于创建隧道和实现网络隔离。
  • 实际应用:用于构建VPN和其他隧道网络,实现远程网络连接和安全通信。
  1. sit (IPv6 over IPv4的虚拟隧道接口)
  • 内核引入时间:大约在Linux 2.2内核版本中引入(1999年)。
  • 功能:在IPv4网络上传输IPv6数据包,实现IPv6网络的连接。
  • 实际应用:用于IPv6网络的部署和过渡,如6to4和6rd等过渡技术。

bridge网络

网络层面中,bridge network是一种链路层设备,用于在网络段之间转发流量。bridge可以是硬件设备,也可以是在主机内核中运行的软件设备。

Docker中,bridge network使用软件桥接,它允许连接到同一个bridge network的容器之间可以进行通信,同时,保证了未连接同一bridge network的容器网络之间的隔离。Docker桥接驱动程序会自动在主机中安装规则,以便不同桥接网络上的容器不能直接相互通信。

Bridge network只适用于在同一Docker daemon主机上运行的容器。要在不同Docker守护进程的主机上运行的容器之间进行通信,您可以在操作系统级别管理路由,或者使用overlay network.。

当启动Docker dameon后,会自动创建一个 default bridge network,启动的容器会自动连接此桥接网络,当然,我们也可以创建自定义的bridge network,用户定义的桥接网络,优先级高于默认的bridge network。

默认bridge网络

如下,本机启动Docker dameon后,可以看到默认创建的bridge驱动类型的桥接网络。

1
2
3
4
5
$ sudo docker network ls
NETWORK ID NAME DRIVER SCOPE
a1c6775b1ee5 bridge bridge local
a09d489d2abd host host local
ef2dbca65b2a none null local

可以通过docker network inspect 来查看对应的bridge网络的具体信息,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# docker network inspect a1c6775b1ee5
[
{
"Name": "bridge",
"Id": "a1c6775b1ee5708162e562d994f84538a29ab5ab5e3e0cfe05f831dc4c8b81d5",
"Created": "2022-10-17T15:05:57.378344235+08:00",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": null,
"Config": [
{
"Subnet": "192.168.10.0/24",
"Gateway": "192.168.10.1"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {},
"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": {}
}
]

其中比较重要的几个信息包括:

  • 网络设备类型信息:"Driver": "bridge",桥接网络;
  • bridge的网络的配置信息,其中主要包括子网网段信息:"Subnet": "192.168.10.0/24"
  • 是否和外部网络互通: "Internal": false

接着我们运行一个CentOS的image,如下:

1
2
3
4
5
6
$ docker run -d -it centos 
082a9748b88c408c25693ccd54ed8857ee627a7e5bfee2e9954bd30cc2c38f1f

$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
082a9748b88c centos "/bin/bash" 2 seconds ago Up 1 second stupefied_pare

然后我们再查看一下默认的bridge网络的信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
$ docker inspect a1c6775b1ee5
[
{
"Name": "bridge",
"Id": "a1c6775b1ee5708162e562d994f84538a29ab5ab5e3e0cfe05f831dc4c8b81d5",
"Created": "2022-10-17T15:05:57.378344235+08:00",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": null,
"Config": [
{
"Subnet": "192.168.10.0/24",
"Gateway": "192.168.10.1"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {
"082a9748b88c408c25693ccd54ed8857ee627a7e5bfee2e9954bd30cc2c38f1f": {
"Name": "stupefied_pare",
"EndpointID": "328398eb1759b4bcf56cbcf664d2092a9c3e5e066abd08b5f96e57a913de6c9f",
"MacAddress": "02:42:c0:a8:0a:02",
"IPv4Address": "192.168.10.2/24",
"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": {}
}
]

可以看到默认的bridge网络的信息中的Containers字段新增了刚刚启动的centos名为stupefied_pare的容器信息,里面包括了本bridge设备为该容器分配的网络地址信息:192.168.10.2/24

自定义bridge网络

我们可以通过docker network create命令来创建自定义的bridge网络信息,并设置其相关的属性。如下简单创建一个自定义的bridge网络的命令(详细的选项参考create命令):

1
$ docker network create --driver=bridge --subnet=192.168.0.0/24 test-bridge

我们可以查看刚刚创建的bridge网络:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
$ docker network ls -f 'driver=bridge'
NETWORK ID NAME DRIVER SCOPE
a1c6775b1ee5 bridge bridge local
76e84e49f1f9 test-bridge bridge local
$ docker network ls -f 'name=test-bridge'
NETWORK ID NAME DRIVER SCOPE
76e84e49f1f9 test-bridge bridge local
$ docker network inspect 76e84e49f1f9
[
{
"Name": "test-bridge",
"Id": "76e84e49f1f9773cb2296f29cf4b5be9d2483aa87095dfa6db0a71fe1b76dcb5",
"Created": "2024-10-20T00:59:38.219280696+08:00",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": {},
"Config": [
{
"Subnet": "192.168.0.0/24"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {},
"Options": {},
"Labels": {}
}
]

当我们创建,删除一个自定义的bridge网络,或者将一个容器从自定义的bridge网络中connect,disconnect的时候都发生了什么?Docker会通过特定操作系统的工具来管理底层网络基础设施,例如在Linux上添加和删除bridge设备,或配置iptables。当然这些都是实现细节,Docker可以完全用来管理自定义网络,而不用关注这些实现细节。

后面在分析「跨Network Namespace的通信」一节中,会仔细分析Docker是如何通过iptables来进行网络管理的。

bridge网络选项

前面我们在查看Docker默认创建的bridge网络的详细信息时,看到有一些复杂的选项配置,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ docker network inspect a1c6775b1ee5
[
{
"Name": "bridge",
"Id": "a1c6775b1ee5708162e562d994f84538a29ab5ab5e3e0cfe05f831dc4c8b81d5",
...
"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"
},
...
}
]

下面主要介绍一下这些参数:

  • "com.docker.network.bridge.name"

bridge网络对应的主机网卡的名字;下面是我主机上的三个bridge网络的网卡设备信息

1
2
3
4
5
6
7
8
9
10
$ ifconfig -v
br-2f8183e15eda: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 192.168.20.1 netmask 255.255.255.0 broadcast 192.168.20.255
...
br-76e84e49f1f9: flags=4099<UP,BROADCAST,MULTICAST> mtu 1500
inet 192.168.0.1 netmask 255.255.255.0 broadcast 192.168.0.255
...
docker0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 192.168.10.1 netmask 255.255.255.0 broadcast 192.168.10.255
...
  • "com.docker.network.bridge.host_binding_ipv4"

表示默认绑定的主机的网卡地址。如果启动Container的时候,暴露的端口没有指定主机网卡地址,例如:-p 80 or -p 8080:80,那么Docker会通过host_binding_ipv4的参数来监听对应主机网卡对应的端口,host_binding_ipv4: "0.0.0.0"表示默认绑定主机的所有IPv4和IPv6的网卡地址。忽略它的名字,它可以用来绑定IPv6,例如::表示Container暴露的端口只绑定所有的IPv6的网卡。

如果暴露端口的时候指定绑定的主机的ip,就会使用显示指定的主机地址,例如:-p 0.0.0.0:8080:80 限制只能绑定本机的所有IPv4网卡。

  • com.docker.network.bridge.enable_icc

控制着 同一个Docker 网络中容器间通信(Inter-Container Communication, ICC)。默认是通的,如果设置为false,那么即使在同一个network中,也是无法通信的,实现原理是通过 iptables FORWARD 链规则实现:

如下创建一个enable_icc=false的bridge网络:

1
$docker network create --opt "com.docker.network.bridge.enable_icc"="false"  --driver=bridge --subnet=192.168.20.0/24 no_icc_bridge

查看iptables的FORWARD链中关于最新创建的网卡br-2f8183e15eda规则如下:

1
2
3
4
5
$ iptables  -L FORWARD -n -v|grep br-2f8183e15eda
0 0 ACCEPT all -- * br-2f8183e15eda 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED
0 0 DOCKER all -- * br-2f8183e15eda 0.0.0.0/0 0.0.0.0/0
0 0 ACCEPT all -- br-2f8183e15eda !br-2f8183e15eda 0.0.0.0/0 0.0.0.0/0
0 0 DROP all -- br-2f8183e15eda br-2f8183e15eda 0.0.0.0/0 0.0.0.0/0

然后我们创建两个连接到no_icc_bridge网络的Container,进行网络测试,结果如下:

1
2
3
4
5
6
7
8
9
# 为Container分配的地址为:192.168.20.2
$ docker run -d -it --network no_icc_bridge centos

# 再启动一个Container,启动命令直接设置为ping 192.168.20.2
$ docker run -it --network no_icc_bridge centos ping 192.168.20.2
PING 192.168.20.2 (192.168.20.2) 56(84) bytes of data.
^C
--- 192.168.20.2 ping statistics ---
16 packets transmitted, 0 received, 100% packet loss, time 15120ms

我们创建一个连接到默认的bridge网络的Container,进行网络测试,结果就是如下正常的:

1
2
3
4
5
6
7
8
$ docker run  -it --network bridge  centos ping 192.168.10.2
PING 192.168.10.2 (192.168.10.2) 56(84) bytes of data.
64 bytes from 192.168.10.2: icmp_seq=1 ttl=64 time=0.090 ms
64 bytes from 192.168.10.2: icmp_seq=2 ttl=64 time=0.042 ms
64 bytes from 192.168.10.2: icmp_seq=3 ttl=64 time=0.051 ms
^C
--- 192.168.10.2 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1998ms
  • "com.docker.network.bridge.enable_ip_masquerade"

它控制 Docker 的 IP 伪装功能。一种 NAT(网络地址转换)技术,它允许容器通过宿主机的 IP 地址访问外部网络。如下,我的环境,Docker会默认创建如下iptables规则:

1
2
3
4
5
$ iptables -t nat -L POSTROUTING -n -v
Chain POSTROUTING (policy ACCEPT 15987 packets, 1178K bytes)
pkts bytes target prot opt in out source destination
0 0 MASQUERADE all -- * !br-76e84e49f1f9 192.168.0.0/24 0.0.0.0/0
54 3469 MASQUERADE all -- * !docker0 192.168.10.0/24 0.0.0.0/0

通过POSTROUTING链可以实现容器的网络包在离开主机时进行NAT地址转换:

1
2
3
           网络包处理流程
[数据包] -> PREROUTING -> 路由选择 -> FORWARD -> POSTROUTING -> [发出]
-> INPUT -> 本地进程 -> OUTPUT ->

network connect

不同的bridge网络的Container之间是不通的,我们可以通过docker network connect将不同bridge网络的Container连接到同一个网络,然后进行通信。

如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 运行容器的IP以及对应的网络信息
$ docker ps -q | xargs -n 1 docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}} {{end}} {{.Name}} {{.NetworkSettings.Networks}}'
192.168.0.2 /confident_shockley map[test-bridge:0xc4205d6000]
192.168.10.2 /stupefied_pare map[bridge:0xc420490cc0]

# 登录confident_shockley容器,ping 192.168.10.2,发现网络不同
$ docker exec -it confident_shockley ping 192.168.10.2
PING 192.168.10.2 (192.168.10.2) 56(84) bytes of data.
^C
--- 192.168.10.2 ping statistics ---
3 packets transmitted, 0 received, 100% packet loss, time 2008ms

# pedantic_albattani容器连接到默认的bridge网络
$ docker network connect bridge confident_shockley
# 运行容器的IP以及对应的网络信息
$ docker ps -q | xargs -n 1 docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}} {{end}} {{.Name}} {{.NetworkSettings.Networks}}'
192.168.10.3 192.168.0.2 /confident_shockley map[bridge:0xc4203b0cc0 test-bridge:0xc4203b0d80]
192.168.10.2 /stupefied_pare map[bridge:0xc42049ecc0]

# confident_shockley,ping 192.168.10.2,网络已经通了
$ docker exec -it confident_shockley ping 192.168.10.2
PING 192.168.10.2 (192.168.10.2) 56(84) bytes of data.
64 bytes from 192.168.10.2: icmp_seq=1 ttl=64 time=0.084 ms
64 bytes from 192.168.10.2: icmp_seq=2 ttl=64 time=0.037 ms
^C
--- 192.168.10.2 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1000ms
rtt min/avg/max/mdev = 0.037/0.060/0.084/0.024 ms

前面我们提到:当我们将一个容器从bridge网络中connect,disconnect的时候,Docker会通过配置iptables来进行相关网络策略配置。那具体是如何实现的呢?后面我们会详细介绍

Port Mapping

Docker 的 Port Mapping(端口映射) 是容器网络的核心机制之一,用于将容器内部的网络端口映射到宿主机的端口,从而允许外部通过宿主机访问容器内的服务。其设计原理与容器网络隔离、NAT(网络地址转换)技术密切相关。通过Port Mapping可以做到:

  • 将容器内部端口映射到宿主机端口
  • 实现容器服务的外部访问
  • 解决容器网络隔离问题

使用格式如下:

1
$ docker run -p 8000:80 -d nginx

选项 -p 的语法格式为: HOST_PORT:CLIENT_PORT,含义为:绑定容器CLIENT_PORT到主机的HOST_PORT,主机的网卡信息可以忽略,默认是由前面介绍过的Docker Network的"com.docker.network.bridge.host_binding_ipv4"参数决定,默认是主机的所有IPV4/IPV6网卡。

如下是执行Port Mapping后的nginx的Container信息:

1
2
3
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
5f03a252f5e0 nginx "/docker-entrypoint.…" 6 seconds ago Up 5 seconds 0.0.0.0:8000->80/tcp, [::]:8000->80/tcp festive_haibt

如下是进行端口映射后,Docker通过iptables写入的映射规则:

主要关键点:

  • ADDRTYPE match dst-type LOCAL:作为扩展匹配条件,匹配目标地址为宿主机本地 IP时,跳转到DOCKER链进行匹配;
  • 跳入DOCKER链后,从非docker0网卡流入的流量,目标地址是8000 tcp端口的请求,全部进行DNAT转换,目标重定向到172.17.0.2:80 ,即nginx容器内的的80端口。

Docker 的 Port Mapping 通过 NAT 和网络命名空间隔离,实现了容器服务的灵活暴露。其设计平衡了隔离性、安全性与易用性,是容器化应用与外部通信的基石。理解这一机制有助于排查网络问题(如端口冲突、防火墙限制)和优化容器部署。

自定义bridge和默认bridge的差异

用户定义和默认 bridge network之间的差异如下:

  • 用户自定义的桥接网络提供容器之间的自动DNS解析。

运行在默认bridge network上的容器之间只能通过IP地址进行通信,除非使用 --link选项(已经被标记过时),而自定义的桥接网络容器之间可以通过名字或者别名进行相互解析并通信。

例如,有一个Web前端和一个数据库后端组成的应用程序,可以将容器命名为web和db,那么web容器就可以通过名称db连接到数据库容器,而不需要关心它们运行在哪个Docker主机上。如果在默认桥接网络上运行相同的应用程序堆栈,需要手动创建容器之间的链接(使用过时的 --link选项),并且需要在两个方向上创建,这样在有多个容器需要通信时就变得比较复杂。另一种方法是操纵容器内的/etc/hosts文件,但是这样会造成难以调试的问题。

  • 用户自定义的桥接网络提供更好的隔离性。

所有运行时没有设置--network选项的容器,都连接默认的bridge network,这就带来无关的服务之间都是可通信的潜在风险。而使用自定义的bridge network,只有显示连接到此bridge network的容器才可以相互通信。

  • 容器可以在运行时attach或者detach用户自定义的桥接网络,而如果detach默认的桥接网络,则需要停止容器。
  • 用户自定义的桥接网络是可配置的

默认的桥接网络,所有的容器共用相同的配置,如果修改默认的配置,所有的使用默认桥接网络的容器都会生效,例如MTU,iptables的规则。除此之外,修改默认的桥接网络配置,需要重启容器才可以生效。

用户自定义的桥接网络,通过docker network create来进行创建和配置,每个桥接网络都是独立的,可以根据需要进行创建和配置。

  • 连接到默认桥接网络的容器之间共享环境变量

最初,在两个容器之间共享环境变量的唯一方法是通过使用--link flag 参数,但是这种方式的变量共享无法使用在用户定义的桥接网络。然而,有更好的方法来共享环境变量。例如:

  1. 多个容器可以使用Docker卷挂载包含共享信息的文件或目录。
  2. 多个容器可以一起使用docker-compose启动,而compose文件可以定义共享变量。
  3. 可以使用swarm服务代替独立容器,并利用共享的secretsconfigs.

host网络

主机网络模式,如果Docker Container采用此种网络模式,Docker容器共享主机机器的网络命名空间,即共享网络协议栈。这意味着容器可以直接访问主机的网络接口和端口。此模式下Container不在单独分配IP地址,直接使用主机的IP地址进行通信,例如Container启动时绑定80端口,此时通过主机IP直接可以进行访问。

host网络模式下,端口映射将会失效,即 -p, --publish, -P, and --publish-all将会失效,并发出告警,如下:

1
2
3
$ docker run -d -it --network host -p 80:80 centos
WARNING: Published ports are discarded when using host network mode
cb73418ed0b43d2c202275af8e2d0a6e72aab8b0ede4a02723aafb29e2c96be1

可以直接通过主机IP进行Container的端口访问,不需要进行端口映射,如下:

1
2
3
4
5
# host1
$ docker exec -it cb73418ed0b4 nc -l 80

#host2
$nc 9.134.15.244 80

host网络模式在两种情况下比较有用:

  • 优化程序性能:Container应用程序直接访问主机的网络栈,对性能至关重要。相比容器网络,流量不需要在veth,bridge网卡进行转发,也不需要主机通过NAT来进行流量转换;这里能提高多少吞吐量?ChatGPT说30%,但是没有来源
  • Container应用程序需要绑定和处理大量范围的端口

host网络模式也有很大的缺点:

  • 安全风险:容器完全共享主机网络命名空间,没有网络层隔离;
  • 端口冲突:容器直接使用主机端口,多个容器不能绑定同一端口,需要手动协调端口分配;
  • 网络资源竞争:所有容器共享网络带宽,无法限制单个容器网络资源,容器可能相互影响网络性能;
  • 迁移和可移植性差:例如端口冲突;

host网络模式支持情况:Docker Desktop 4.34及以后的版本,以及Linux版本的Docker Engine;

none网络

none网络模式,是无网卡模式,会让Container的网络和主机网络彻底隔离,即Container不能访问外界任何网络;适用于当容器不需要网络访问或出于安全原因需要网络隔离时。

其实现方式,就是为Container创建独立的Network Namespace,但是不创建对应的bridgeveth进行网络的打通,如下:Container内的网卡只有lo网卡:

1
2
3
4
5
$ docker run -it --network none centos ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever

后面我们手动通过ip命令来实现跨Network Namespace通信后,就会知道none网卡模式就是ip netns add ns-1创建了一个隔离的ns,并没进行网络桥接。

跨Network Namespace的通信

前面我们知道默认的Container通过bridge网卡来进行网络管理,那Container是如何通过bridge网卡和主机上其他的进程进行通信的呢?

容器化技术之Linux Namespace一文中,我们知道Namespace是Container实现资源隔离的基础;Linux Namespace对系统的全局资源进行了抽象,让Namespace中的进程组可以认为他们拥有隔离的全局资源;对于Namespace拥有的全局资源进行修改,只有本Namespace中的进程可见,其他Namespace的进程是无感知的;

其中Network Namespace就是Linux针对全局资源Network Device按Namespace进行隔离,可以隔离出:网卡设备,堆栈,端口等网络资源。这样Container可以很方便的使用自己虚拟出的网卡资源,和其他Namespace项目隔离,互不影响;

我们前面知道:当我们创建,删除一个自定义的bridge网络,或者将一个容器从自定义的bridge网络中connect,disconnect的时候都发生了什么?Docker会通过操作系统的工具在Linux上添加和删除bridge设备,或配置iptables。那具体一个基于bridge网络的Container是如何和其他Container进行通信,以及如何通过Host和外界进行通信的呢?

我们来看一下创建bridge网络,然后运行Container,Container和主机上的其他进程通信的实现过程:

创建bridge network

如下:

1
$docker network create --driver=bridge --subnet=192.168.0.0/24 test-bridge

然后我们可以看到,系统创建了一个新的网卡:

1
2
3
4
5
6
7
8
$ ifconfig
br-e6705c4803fb: flags=4099<UP,BROADCAST,MULTICAST> mtu 1500
inet 192.168.0.1 netmask 255.255.255.0 broadcast 192.168.0.255
ether 02:42:5a:01:14:87 txqueuelen 0 (Ethernet)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 (0.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

然后Docker会自动创建对应的iptables规则,以控制网络流量并实现隔离。

FILTER表

如下是查看iptables默认filter表的所有Chain的格式化输出:

1
$ iptables -nvL |cut -f -9|column -t| sed 's/^Chain/\n&/g'|sed '/^Chain/ s/[ \t]\{1,\}/ /g'|sed '/^[0-9]/ s/[ \t]\{1,\}/ /10g'|sed -e 's/\(br-e6705c4803fb\|DOCKER-USER\|DOCKER-ISOLATION-STAGE-1\|DOCKER-ISOLATION-STAGE-2\)/\x1b[31m&\x1b[0m/g'

我们详细看一下filter表的所有关于新创建的br-e6705c4803fb虚拟网卡的规则:

首先是Chain级别,我们看到Docker创建了三个相关的Chain:

  • DOCKER-ISOLATION-STAGE-1:这是Docker创建的一个iptables链,用于处理Docker容器之间的网络隔离。它是流量隔离的第一阶段。
  • DOCKER-ISOLATION-STAGE-2:这是第二阶段的隔离链,用于进一步控制容器的流量。
  • DOCKER-USER:这是用户定义的链,Docker允许用户在此链上添加自定义的iptables规则。Docker会在此链的开头插入规则,以便用户可以在Docker创建的规则之前或之后添加自己的规则。

我们可以看到,所有需要经过转发的的流量经过FORWARD链时,按照从上至下的优先级:

  • 第一个规则,匹配所有数据包,流量都会跳转到DOCKER-USER链,目前看默认创建的此Chain包含的Rule的动作只是返回;
  • 第一个规则返回后,匹配上了第二个规则,同样匹配所有数据包,会自动跳转DOCKER-ISOLATION-STAGE-1 链,这是Docker流量管理的第一阶段

DOCKER-ISOLATION-STAGE-1 链里面有三条规则:

  • 规则1:入站数据包来自于br-e6705c4803fb(我上面刚刚通过docker network create创建的),出站数据包不非同一个网络桥(!br-e6705c4803fb)。也就是说从br-e6705c4803fb网卡发出的所有跨网络流量,都会转发到DOCKER-ISOLATION-STAGE-2链进行第二阶段处理;
  • 规则2:入站数据包来自于docker0(docker默认创建的的网卡),出站数据包非同一个网络桥(!docker0)。也就是说从docker0网卡发出的所有跨网络流量,都会转发到DOCKER-ISOLATION-STAGE-2链进行第二阶段处理;
  • 规则3:匹配所有数据包,没有命中上述规则的的流量,直接返回上一层链的规则调用处;

从上面知道,能跳转到DOCKER-ISOLATION-STAGE-2链的流量都是Docker创建的network的流量,并且是跨网卡的流量才会跳转到这里

DOCKER-ISOLATION-STAGE-2 链里面有三条规则:

  • 规则1:出站数据包流向br-e6705c4803fb,直接丢弃;
  • 规则2:出站数据包流向docker0,直接丢弃;
  • 规则3:匹配所有数据包,没有命中上述规则的的流量,直接返回上一层链的规则调用处;

基于DOCKER-ISOLATION-STAGE-2 链里面的三条规则,我们知道,Docker如何通过iptables实现不同network之间的网络隔离

有关Docker如何在Linux上操作iptables规则的信息,请参Packet filtering and firewalls.

NAT表

前面介绍docker network创建的bridge网络的默认网络选项的时候,我们知道enable_ip_masquerade,它控制 Docker 的 IP 伪装功能。一种 NAT(网络地址转换)技术,它允许容器通过宿主机的 IP 地址访问外部网络。如下,Docker在创建一个brdige网卡时,会默认创建如下iptables规则:

通过POSTROUTING(处理离开系统的数据包,在它们被路由选择后但在实际发送之前)可以实现容器的网络包在离开主机时进行NAT地址转换。

Docker 容器网络中的请求通过非 docker0 和非br接口(如 eth0)发送出去时,使用该出口接口的 IP 地址替换源 IP 地址。这允许 Docker 容器通过主机访问外部网络。

运行基于bridge的Container

如下:基于上面创建的test-bridge网卡,启动一个Container,如下:

1
$ docker run -d -it --network test-bridge centos

我们会发现,多了一个veth的网卡:

1
2
3
4
5
6
7
$ifconfig
veth5186333: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
ether 1e:3d:ce:45:66:99 txqueuelen 0 (Ethernet)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 (0.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

什么是veth网卡,为什么会新增这个网卡?

讨论veth之前,我们先看一下之前了解过Network Namespace,我们知道Linux为了隔离网络资源,提供了Network Namespace的功能,可以让每个Container拥有独立的网络栈,不同Namespace的Container之间/Container和主机进程之间是不能直接通信,如下我们看一下启动容器内所看到的网卡资源:

1
2
3
4
5
6
7
8
9
$ docker exec -it 0b14786e369b ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
29: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:c0:a8:00:02 brd ff:ff:ff:ff:ff:ff
inet 192.168.0.2/24 brd 192.168.0.255 scope global eth0
valid_lft forever preferred_lft forever

然后我们再看一下主机的网卡资源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
2: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP qlen 1000
link/ether 52:54:00:49:10:54 brd ff:ff:ff:ff:ff:ff
inet 9.134.15.244/20 brd 9.134.15.255 scope global eth1
valid_lft forever preferred_lft forever
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN
link/ether 02:42:20:8e:14:09 brd ff:ff:ff:ff:ff:ff
inet 192.168.10.1/24 brd 192.168.10.255 scope global docker0
valid_lft forever preferred_lft forever
28: br-e6705c4803fb: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP
link/ether 02:42:5a:01:14:87 brd ff:ff:ff:ff:ff:ff
inet 192.168.0.1/24 brd 192.168.0.255 scope global br-e6705c4803fb
valid_lft forever preferred_lft forever
30: veth5186333: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-e6705c4803fb state UP
link/ether 1e:3d:ce:45:66:99 brd ff:ff:ff:ff:ff:ff

可见,Container所在的Network Namespace和主机不是一个,也可以通过lsns命令来查看,这里不展开了。那Container中的进程如何和主机的进程进行通信呢,这里就回到veth网卡的作用。

Veth

Linux内核提供的虚拟设备之一:veth(Virtual Ethernet)虚拟以太网接口,通常用于连接不同Network Namespace,它是一对虚拟网络接口设备(类似双绞线),一个接口连接到一个网络命名空间,另一个接口连接到另一个网络命名空间。这种方式允许网络数据在同一台主机的不同网络命名空间之间传输,非常适合容器、虚拟机以及网络隔离的应用场景。

在上面我们看到,启动Container后,我们看到主机创建了一个veth5186333的虚拟网卡,网卡信息可以通过上面的ip addr查看,这里我们单独列出来veth5186333的信息:

1
2
30: veth5186333: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-e6705c4803fb state UP 
link/ether 1e:3d:ce:45:66:99 brd ff:ff:ff:ff:ff:ff

veth 网卡的 master 字段表示该接口所属的主设备(通常是一个桥接设备 bridge)。例如上面的veth5186333master字段为br-e6705c4803fb,我们看一下此veth的对端信息,这里可以通过ethtool查看对端网卡的索引号,如下:

1
2
3
$ ethtool -S veth5186333
NIC statistics:
peer_ifindex: 29 # 对端网卡的索引号

网卡的索引号Linux内核用来表示网络接口的唯一标识符,就是我们前面通过ip命令输出的网卡最前面的数字,该编号通常从 1 开始,随着系统中的接口数量增加而递增。Linux 系统中,网络接口的索引号可以通过以下方法查看:

1
$ cat /sys/class/net/<interface>/ifindex

/sys/class/net/ 是 Linux 系统的一个虚拟文件系统目录,用于存放系统中所有网络接口的信息。每个网络接口在该目录中对应一个子目录,这些子目录包含了该接口的各种属性和配置信息。它是 Linux sysfs 文件系统的一部分,提供了一种方便的方式来查看和操作内核中的设备信息。如下,目前我的主机中的网卡设备信息如下:

1
2
3
4
5
6
7
# ll /sys/class/net/
total 0
lrwxrwxrwx 1 root root 0 Nov 5 14:53 br-e6705c4803fb -> ../../devices/virtual/net/br-e6705c4803fb/
lrwxrwxrwx 1 root root 0 Oct 17 2022 docker0 -> ../../devices/virtual/net/docker0/
lrwxrwxrwx 1 root root 0 Oct 17 2022 eth1 -> ../../devices/pci0000:00/0000:00:05.0/virtio0/net/eth1/
lrwxrwxrwx 1 root root 0 Oct 17 2022 lo -> ../../devices/virtual/net/lo/
lrwxrwxrwx 1 root root 0 Nov 6 01:10 veth5186333 -> ../../devices/virtual/net/veth5186333/

每个网络接口的索引号存储在对应网卡目录的ifindex 文件中。你可以读取该文件查看特定接口的索引号。

1
2
$cat /sys/class/net/veth5186333/iflink 
30

通过上面我们知道,veth虚拟网卡连接了Container中的eth0和主机网络中的br-e6705c4803fb bridge网卡。

其实上面表述并不准确,创建veth的时候必须成对创建,也就是说veth网卡是一对,所以我们经常会看到veth pair的描述,那为什么我们主机上只看到一个veth5186333网卡呢?那是因为veth pair的另一端被移入了Container中,并命名为eth0了。如下:

1
2
3
$ docker exec -it 0b14786e369b ethtool -i eth0
driver: veth
...

所以比较准确的描述是:veth pair通过将veth一端置入Container中(作为容器内的默认eth网卡),另一段连接主机中的bridge网卡,实现了Container的Network NS和主机网络的Network NS的打通,以实现Container网络和主机网络以及其他Container网络的联通和隔离。如下示意图:

这里我们梳理一下 bridgeveth 的关系:

  • bridge网卡,本质是一个虚拟的二层交换机,主要负责转发数据包,维护MAC地址表,一个bridge可以连接多个网络设备(包括物理网卡和虚拟网卡)
  • veth网卡,只能成对创建的虚拟网卡,一端发送的数据会出现在另一端,主要用于连接不同的网络命名空间,就像一根网线的两端

所以:bridge像一个交换机提供交换功能,veth像一根网线连接容器到bridge,两者配合实现了容器的网络互联。

如下是测试数据在主机网络和Container网络如何经过 bridgeveth 转发的:

1
2
3
4
5
# 1. Container中启动监听8080 tcp端口
$docker exec -it 0b14786e369b nc -l 8080

# 2. 主机中直接探测Container中的8080端口
$ nc 192.168.0.2 8080

如下是TCP三次握手的数据在 bridgeveth pair 之间转发的过程,我按照数据包转发时间顺序标注了序号:从tcpdump的抓包时间可知,通过bridge和veth结构的跨NS的流量延迟在20us左右,即us级别

如下示意图:

可以简述为:

  1. nc进程的握手数据包进入主机的协议栈;
  2. 查找路由表,发现目标地址在br-e6705c4803fb网段,转发到br-e6705c4803fb网卡;
  3. br-e6705c4803fb网卡根据目标IP查找MAC地址,转发到对应的veth pair的主端:veth5186333
  4. veth5186333网卡将数据包转发到veth pair另一端的Container的eth0中,实现跨Network Namespace的通信;
  5. Container中的eth0网卡,将数据转发给本机的nc进程;

Container之间通信

我们再启动一个基于test-bridge的Container,如下:

1
$ docker run -d -it --network test-bridge centos

此时会自动创建另一个veth用来打通Container的Network NS和主机网络的Network NS,如下是最新的网卡信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
2: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP qlen 1000
link/ether 52:54:00:49:10:54 brd ff:ff:ff:ff:ff:ff
inet 9.134.15.244/20 brd 9.134.15.255 scope global eth1
valid_lft forever preferred_lft forever
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN
link/ether 02:42:20:8e:14:09 brd ff:ff:ff:ff:ff:ff
inet 192.168.10.1/24 brd 192.168.10.255 scope global docker0
valid_lft forever preferred_lft forever
28: br-e6705c4803fb: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP
link/ether 02:42:5a:01:14:87 brd ff:ff:ff:ff:ff:ff
inet 192.168.0.1/24 brd 192.168.0.255 scope global br-e6705c4803fb
valid_lft forever preferred_lft forever
30: veth5186333: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-e6705c4803fb state UP
link/ether 1e:3d:ce:45:66:99 brd ff:ff:ff:ff:ff:ff
38: veth4fd0b28: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-e6705c4803fb state UP
link/ether b6:5d:8a:c2:9c:17 brd ff:ff:ff:ff:ff:ff

新启动容器内网卡信息如下:

1
2
3
4
5
6
7
8
9
$ docker exec -it 41763f201e2d ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
37: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:c0:a8:00:03 brd ff:ff:ff:ff:ff:ff
inet 192.168.0.3/24 brd 192.168.0.255 scope global eth0
valid_lft forever preferred_lft forever

前面我们知道了通过 bridgeveth 是如何实现:主机网络和Container网络互通的,同理基于同一个 bridge的多个Container之间也是默认可以通信的,原理如下:

1
2
3
4
5
6
7
8
9
  容器1 namespace           主机 namespace           容器2 namespace
+----------------+ +---------------------+ +----------------+
| | | | | |
| eth0 | | docker0 | | eth0 |
| ↑ | | (bridge) | | ↑ |
| | | | ↗ ↖ | | | |
| | | | ↗ ↖ | | | |
| veth1的一端 | |veth1另一端 veth2另一端| | veth2的一端 |
+----------------+ +---------------------+ +----------------+

基于多个不同 bridge的多个Container之间网络是隔离的,在前面我们介绍了Docker在创建 bridge网卡的时候会配置iptables,让不同的 bridge网络天然的进行隔离。

如下是基于test-bridge的docker network创建的另一个Container是如何通过此bridgeveth 进行通信的:

如下,Container2和Container1通信的流量转发示意图:

整个转发大致流程如下:

  1. Container2 的nc进程的数据包进入容器的协议栈;
  2. 查找路由表,其实只有eth0网卡的路由表,所有流量都通过eth0转发;
  3. Container2容器内的eth0网卡其实是veth5186333的另一端,所以流量会直接转发到主机网卡veth5186333
  4. 进入主机协议栈后,查找路由表,目标192.168.0.2的流量的转发目标为br-e6705c4803fb网卡,所以会将流量转发到br-e6705c4803fb网卡;
  5. br-e6705c4803fb网卡根据目标IP查找MAC地址,转发到对应的veth pair的主端:veth5186333
  6. veth5186333网卡将数据包转发到veth pair另一端的Container1的eth0中,实现跨Network Namespace的通信;
  7. Container1中的eth0网卡,将数据转发给本机的nc进程;

其实,上面介绍的跨Container之间的通信实现,也被用在了Kubernetes中同一个Nodes中的跨Pods的Container之间的通信,如下是通过VethBridge进行通信的示意图:

手动实现跨NS的通信

前面我们在介绍veth和bridge网卡相关的概念的时候,都是通过Docker命令来管理这两个网卡的,Docker通过调用系统工具来进行网络的隔离和管理的相关操作,对用户屏蔽了底层虚拟化和网络的相关细节,那么为了更好的理解Docker是如何在Linux上进行网络管理的,我们接下来通过强大的ip命令来进行network namespace,veth和bridge的相关创建和管理。

ip 命令是 Linux 系统中用于配置网络接口、路由和网络相关设置的强大工具。它是 net-tools工具集的替代品,属于 iproute2 软件包。

  • net-tools工具集:包含:网络接口管理:ifconfig, 路由管理:router,ARP管理:arp,网络状态监控:netstat,自2001年起,Linux社区已经对其停止维护;
  • iproute2工具集:新一代的网络配置工具,是现代 Linux 网络管理的瑞士军刀,提供了强大、灵活的网络配置能力,主要包括:网络管理工具:ip,套接字管理:ss,流量管理:tc;网桥管理:bridge(包含在ip link);

如下,我们通过ip命令来创建network namespace,veth,bridge,然后进行网络的联通测试:

  1. 先通过ip工具在主机的当前ns下创建一个新的bridge网卡,并配置IP,然后启用;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 创建bridge网卡
$ip link add bridge-test-1 type bridge
# 给网卡配置IP地址
$ip addr add 192.168.20.1/24 dev bridge-test-1
# 启用
$ip link set bridge-test-1 up

# 查看刚创建的bridge网卡信息
$ip addr
...
41: bridge-test-1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN
link/ether be:7c:c6:74:2f:f1 brd ff:ff:ff:ff:ff:ff
inet 192.168.20.1/24 scope global bridge-test-1
valid_lft forever preferred_lft forever

# 主机上可以ping通bridge-test-1网卡
$ ping 192.168.20.1
PING 192.168.20.1 (192.168.20.1) 56(84) bytes of data.
64 bytes from 192.168.20.1: icmp_seq=1 ttl=64 time=0.044 ms
64 bytes from 192.168.20.1: icmp_seq=2 ttl=64 time=0.034 ms
  1. 通过ip工具创建一个新的network namespace,然后我们可以查看该network namespace下默认只有一个lo环回虚拟网卡;并测试新的ns下,和主机的bridge-test-1的网络连通性;
1
2
3
4
5
6
7
8
9
# 创建ns
$ip netns add netns-test-1
# 查看ns的信息
$ip netns exec netns-test-1 ip addr
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
# 测试和Docker创建的bridge网卡的连通
$ip netns exec netns-test-1 ping 192.168.20.1
connect: Network is unreachable
  1. 通过ip工具在主机ns中创建一对veth网卡:veth-test-1-1veth-test-1-2,并启用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 创建veth pair,必须成对创建,删除也是自动删除一对
$ip link add veth-test-1-1 type veth peer name veth-test-1-2

# 启用veth pair,两个都up后,它们的状态才会被置为up
$ip link set veth-test-1-1 up
$ip link set veth-test-1-2 up

# 查看创建的veth pair网卡信息
$ip link
39: veth-test-1-2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT qlen 1000
link/ether 16:c2:bc:5a:fd:84 brd ff:ff:ff:ff:ff:ff
40: veth-test-1-1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT qlen 1000
link/ether 92:53:d1:77:09:52 brd ff:ff:ff:ff:ff:ff

# 查看veth pair各自对端的网卡索引
$ ethtool -S veth-test-1-1
NIC statistics:
peer_ifindex: 39
$ ethtool -S veth-test-1-2
NIC statistics:
peer_ifindex: 40
  1. 通过ip工具将veth的一端绑定到主机的bridge-test-1网卡,另一端移入创建的netns-test-1的network namespace中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# veth一端绑定主机的bridge网卡
$ip link set veth-test-1-1 master bridge-test-1

# veth另一端移入新的ns中
$ip link set veth-test-1-2 netns netns-test-1

# 发现veth的另一端移入netns-test-1后,主机里的veth的状态变为DOWN了
$ip addr
40: veth-test-1-1: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc pfifo_fast master bridge-test-1 state DOWN qlen 1000
link/ether 92:53:d1:77:09:52 brd ff:ff:ff:ff:ff:ff

# netns-test-1中的veth状态也变为了DOWN
$ ip netns exec netns-test-1 ip addr
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
39: veth-test-1-2: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN qlen 1000
link/ether 16:c2:bc:5a:fd:84 brd ff:ff:ff:ff:ff:ff
  1. 通过ip工具重新配置移入到netns-test-1的network namespace中veth网卡的一端:veth-test-1-2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# netns-test-1中重新配置veth-test-1-2的名字
$ip netns exec netns-test-1 ip link set veth-test-1-2 name eth0
# 配置ip
$ ip netns exec netns-test-1 ip addr add 192.168.20.4/24 dev eth0
# 启用
$ip netns exec netns-test-1 ip link set eth0 up

# 查看netns-test-1这个ns中的网卡信息
$ ip netns exec netns-test-1 ip addr
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
39: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
link/ether 16:c2:bc:5a:fd:84 brd ff:ff:ff:ff:ff:ff
inet 192.168.20.4/24 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::14c2:bcff:fe5a:fd84/64 scope link
valid_lft forever preferred_lft forever
  1. 通过上面的配置,我们就打通了新创建的network namespace和主机的network namepsace,如下,可以ping通主机bridge-test-1了。
1
2
3
4
$ ip netns exec netns-test-1 ping 192.168.20.1
PING 192.168.0.1 (192.168.0.1) 56(84) bytes of data.
64 bytes from 192.168.0.1: icmp_seq=1 ttl=64 time=0.082 ms
64 bytes from 192.168.0.1: icmp_seq=2 ttl=64 time=0.059 ms
  1. 删除创建的ns和bridge网络
1
2
3
4
5
# 删除ns,会发现对应的veth pair也会全部删除,从主机网络中
$ip netns delete netns-test-1

# 删除bridge
$ip link delete bridge-test-1

上面这就是一个完整的手动配置过程。Docker实际上就是通过类似的操作,自动化地为每个容器创建和配置网络。主要区别是:

  • Docker使用自己的libnetwork库通过netlink系统调用来进行网络管理;
  • 自动分配IP地址和自动管理接口名称;
  • 提供DNS服务;
  • 自动管理网络生命周期;

overlay网络

overlay网络模式提供了在不同Docker daemon主机上的Container进行通信的能力。overlay网络,顾名思义,位于主机特定的网络之上,连接到此网络的容器之间能够进行安全的通信,Docker 透明地处理每个数据包的路由,确保数据包正确地在 Docker daemon 主机和目标容器之间传输。

这在多主机环境中特别有用,其中容器需要在不同物理或虚拟机之间进行通信。overlay网络使用VXLAN(虚拟可扩展局域网)技术封装和路由主机之间的网络流量。

和用户自定义的bridge网络一样,我们可以通过docker network create创建自定义的overlay网络,Service(Docker Swarm集群的服务,一组Container的抽象,类似K8S中的Service)和Container可以同时连接多个网络进行通信。

overlay网络常用于连接多个Docker Swarm Services,但是它也可以连接运行在不同Docker主机的独立的Container,此种standalone模式,也需要通过Swarm mode来进行主机的连接。下面会介绍standalone模式的overlay模式网络的使用,其他关于Swarm Services的使用,详细可以参考:Manage Swarm service networks

vxlan

介绍Docker的overlay网络之前,我们先看一下其实现的底层的网络技术:vxlan

vxlanVirtual Extensible LAN,虚拟局域网扩展,是一种虚拟网络技术,旨在解决传统局域网(LAN)在数据中心规模和灵活性上的局限性。它广泛应用于云计算和大规模数据中心环境中,用于支持虚拟机和容器的动态迁移和多租户隔离。

vxlan技术已经作为RFC7348正式标准,于2014年发布,它的核心是通过在一个Layer 3层网络上覆盖一个虚拟的Layer 2层网络来解决虚拟化技术发展带来的大规模虚拟网络设备通信的需求。

在介绍vxlan技术中有几个关键属于:

  • VXLAN Segment:一个VXLAN Layer 2 网络,有一组共同VNI的设备组成,类似于传统网络中的VLAN。每个VXLAN Segment都具有独立的广播域,可以隔离不同的租户或应用程序流量

  • VNI:VXLAN Network Identifier (or VXLAN Segment ID),vxlan网络标识符,24-bit长度,可以表示1600万个逻辑网络即Segment,

  • VTEP:VXLAN Tunnel End Point. VXLAN隧道的端点,用于在VXLAN网络中封装和解封装数据包,它是VXLAN架构中的关键组件,它可以存在于:物理交换机,虚拟交换机,主机中。

  • VXLAN Gateway:处理多个VXLANs之间的流量转发,连接 VXLAN 网络与传统网络。

VXLAN Frame格式

下面我们从协议结构看一下VXLAN是如何实现在主机的Layer 3层网络上实现虚拟的Layer 2层网络进行通信的:

首先看最底层以太网的头的格式,这里VXLAN并不会在这一层有任何标记,目的MAC地址可能是VTEP封装后的目标真实主机的MAC地址

1
2
3
4
5
6
7
8
9
10
11
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Outer Destination MAC Address |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Outer Destination MAC Address | Outer Source MAC Address |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Outer Source MAC Address |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|OptnlEthtype = C-Tag 802.1Q | Outer.VLAN Tag Information |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Ethertype = 0x0800 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

然后是IP网络层的协议头,是VTEP封装后,真实通信的主机的地址:

1
2
3
4
5
6
7
8
9
10
11
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Version| IHL |Type of Service| Total Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Identification |Flags| Fragment Offset |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Time to Live |Protocl=17(UDP)| Header Checksum |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Outer Source IPv4 Address |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Outer Destination IPv4 Address |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

然后是传输层的协议头,这里先划重点:VXLAN在Layer 3层采用UDP进行协议封装,以提供高效的的数据传输能力,且IANA为了VXLAN分配了4789熟知端口,用来VXLAN的隧道流量传输

1
2
3
4
5
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Port | Dest Port = VXLAN Port |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| UDP Length | UDP Checksum |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

这里按照RFC7348标准,VXLAN封装的UDP Checksum应该为0,这样应该也是为了提供转发性能;

接下来就是VXLAN的协议头了,如下:

1
2
3
4
5
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|R|R|R|R|I|R|R|R| Reserved |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| VXLAN Network Identifier (VNI) | Reserved |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

主要包括:

  1. Flag:8个bits,I位必须置为1,表示有效的VNI,其他7bits是预留字段;
  2. VNI:24个bits,唯一标识一个VXLAN Segment,前面介绍过;
  3. Reserved:24个bits和8bits,保留字段,传输过程中必须置为0;

接下来就是具体内部网络的完整包了同样包括:Inner Ethernet Header + Inner IP Header + Inner IP Payload,如下是经过VXLAN封装后的完整的以太网链路层的协议包格式:

手动创建VXLAN

下面我们通过ip命令来手动创建一个VXLAN网络进行简单的通信,如下:

  • 在Host1上,创建一个名为vxlan-test的VXLAN网卡,VNI为8888,隧道端点使用设备eth0进行传输,remote指定单播点对点的VXLAN隧道的目标,即远程VTEP的地址,local即VTEP的本端地址;
1
2
3
$ ip link add vxlan-test type vxlan id 8888 dev eth0 remote 10.206.0.12 local 10.206.0.37 dstport 4789
$ ip addr add 192.168.1.1/24 dev vxlan-test
$ ip link set up dev vxlan-test

启用vxlan-test后,可以看到对应网卡的信息如下:

1
2
3
4
5
6
7
$ ip add
19: vxlan-test: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN group default qlen 1000
link/ether fe:64:03:68:cc:cb brd ff:ff:ff:ff:ff:ff
inet 192.168.1.1/24 scope global vxlan-test
valid_lft forever preferred_lft forever
inet6 fe80::fc64:3ff:fe68:cccb/64 scope link
valid_lft forever preferred_lft forever
  • 在Host2上,同样创建名为vxlan-test的VXLAN网卡,所有参数和Host1一样,只是把VTEP两端的地址调换一下,即remotelocal调换,如下:
1
2
3
$ ip link add vxlan-test type vxlan id 8888 dev eth0 remote 10.206.0.37 local 10.206.0.12 dstport 4789
$ ip addr add 192.168.1.2/24 dev vxlan-test
$ ip link set up dev vxlan-test

启用vxlan-test后,同样可以看到对应网卡的信息如下:

1
2
3
4
5
6
24: vxlan-test: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN group default qlen 1000
link/ether ce:47:7f:57:20:98 brd ff:ff:ff:ff:ff:ff
inet 192.168.1.2/24 scope global vxlan-test
valid_lft forever preferred_lft forever
inet6 fe80::cc47:7fff:fe57:2098/64 scope link
valid_lft forever preferred_lft forever

接下来我们就可以在Host1上直接测试是否可以ping通基于VXLAN网络的Layer 2的内部网络,如下:

1
2
3
4
5
6
7
8
$ ping 192.168.1.2
PING 192.168.1.2 (192.168.1.2) 56(84) bytes of data.
64 bytes from 192.168.1.2: icmp_seq=1 ttl=64 time=0.211 ms
64 bytes from 192.168.1.2: icmp_seq=2 ttl=64 time=0.206 ms
^C
--- 192.168.1.2 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1002ms
rtt min/avg/max/mdev = 0.206/0.208/0.211/0.002 ms

如下,我们在Host2上进行抓包可以比较清楚的看到,VXLAN是如何在主机的Layer 3层网络上实现虚拟的Layer 2层网络通信的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ tcpdump -ni eth0 port 4789
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
00:41:58.021949 IP 10.206.0.37.34258 > 10.206.0.12.4789: VXLAN, flags [I] (0x08), vni 8888
IP 192.168.1.1 > 192.168.1.2: ICMP echo request, id 7, seq 1, length 64
00:41:58.022085 IP 10.206.0.12.52816 > 10.206.0.37.4789: VXLAN, flags [I] (0x08), vni 8888
IP 192.168.1.2 > 192.168.1.1: ICMP echo reply, id 7, seq 1, length 64
00:42:03.186250 IP 10.206.0.12.40462 > 10.206.0.37.4789: VXLAN, flags [I] (0x08), vni 8888
ARP, Request who-has 192.168.1.1 tell 192.168.1.2, length 28
00:42:03.186487 IP 10.206.0.37.47323 > 10.206.0.12.4789: VXLAN, flags [I] (0x08), vni 8888
ARP, Reply 192.168.1.1 is-at fe:64:03:68:cc:cb, length 28
00:42:03.230061 IP 10.206.0.37.47323 > 10.206.0.12.4789: VXLAN, flags [I] (0x08), vni 8888
ARP, Request who-has 192.168.1.2 tell 192.168.1.1, length 28
00:42:03.230147 IP 10.206.0.12.40462 > 10.206.0.37.4789: VXLAN, flags [I] (0x08), vni 8888
ARP, Reply 192.168.1.2 is-at ce:47:7f:57:20:98, length 28

在上面使用VXLAN进行跨机的通信的时候,我们在Host2上进行抓包可以清楚的看到ARP地址解析协议请求,但是我们在主机上是看不到ARP表的,因为VXLAN的ARP解析是在VTEP(虚拟隧道端点)层面进行的,所以主机上可能看不到传统的ARP表项。MAC地址表记录了远程主机的MAC地址和对应的VTEP信息,这样可以实现跨网段的通信。

Docker Swarm集群

使用overlay网络模式前,还是需要初始化docker swarm集群,可以参考官方:Swarm Mode,简单操作如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
# Host1上面初始化swarm集群,作为管理节点
$ docker swarm init --default-addr-pool 192.166.0.0/16 --force-new-cluster
Swarm initialized: current node (3yvp1wuoqx2nhqtksv3luwo5v) is now a manager.

To add a worker to this swarm, run the following command:

docker swarm join --token SWMTKN-1-4unwf5q31qkq4jvj9o5p4lfxx670j1cqsdv2e1ou0r090zh4w3-eoi2nv809n1e0ys3vkwewf82f 10.206.0.37:2377

To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

# Host2加入上面创建的swarm集群,作为工作节点
$ docker swarm join --token SWMTKN-1-4unwf5q31qkq4jvj9o5p4lfxx670j1cqsdv2e1ou0r090zh4w3-eoi2nv809n1e0ys3vkwewf82f 10.206.0.37:2377
This node joined a swarm as a worker.

上面初始化后,我们可以在Swarm的管理节点查看集群中的Node列表,如下:

1
2
3
4
$ docker node ls
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE
mrz8ofttzouczuq42wo7jvfcg VM-0-12-ubuntu Ready Active 24.0.7
3yvp1wuoqx2nhqtksv3luwo5v * VM-0-37-ubuntu Ready Active Leader 24.0.7

Swarm集群网络

Swarm集群初始化完成后,我们可以看到新创建了两个网络如下:

1
2
3
4
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
ca269ad68a60 docker_gwbridge bridge local
lqchwltv5w1c ingress overlay swarm

这两个网络是Swarm集群初始化自动创建的,这两个网络是Swarm集群的Service & Container通信的基础:

  • docker_gwbridge:此网络看名字就知道为bridge网络,专门用来连接overlay网络和Container的主机网络,Container最终通过该bridge和外部进行通信,和前面我们介绍bridge如何打通跨Namespace的功能是一致的。
  • ingress:此网络为overlay网络模式,是Swarm初始化是自动创建的特殊的overlay网络,用来在对Swarm集群中的Serivces进行服务发现负载均衡。当Swarm的任一Node收到一个到某Service暴露端口的请求后,会将请求交由IPVS模块,IPVS模块会根据目标Service配置的路由算法,选择其中一个IP,通过此ingress网络交由到对应的Node上。

我们看一下这两个新创建的Docker network的详细信息,这里先看一下docker_gwbridgebridge网络详细信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
$ docker inspect docker_gwbridge
[
{
"Name": "docker_gwbridge",
"Id": "ca269ad68a60fa39f9de71602679484f45d6eca8f630918f23d599dc88ea955d",
"Scope": "local",
"Driver": "bridge",
"IPAM": {
...
"Config": [
{
"Subnet": "172.18.0.0/16",
"Gateway": "172.18.0.1"
}
]
},
...
"Containers": {
"ingress-sbox": {
"Name": "gateway_ingress-sbox",
"EndpointID": "21c14a19f293bbf0d4937a5b6afb3a56ea79a61595e29fca526c24168c897f8e",
"MacAddress": "02:42:ac:12:00:02",
"IPv4Address": "172.18.0.2/16",
"IPv6Address": ""
}
},
}
]

再看一下名为ingressoverlay的网络信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
$ docker inspect ingress
[
{
"Name": "ingress",
"Id": "lqchwltv5w1ctjo9nt1cjrvjw",
"Scope": "swarm",
"Driver": "overlay",
"IPAM": {
"Config": [
{
"Subnet": "192.166.0.0/24",
"Gateway": "192.166.0.1"
}
]
},
...
"Containers": {
"ingress-sbox": {
"Name": "ingress-endpoint",
"EndpointID": "0aa396d7381de9b69273eb9e2ba50c5ccc4cf00818dd81be83f05f47ac5fe69f",
"MacAddress": "02:42:c0:a6:00:02",
"IPv4Address": "192.166.0.2/24",
"IPv6Address": ""
}
},
"Options": {
"com.docker.network.driver.overlay.vxlanid_list": "4096"
},
"Labels": {},
"Peers": [
{
"Name": "83472d9a8b9f",
"IP": "10.206.0.37"
},
{
"Name": "a118c4fd6ae8",
"IP": "10.206.0.12"
}
]
}
]

我们看一下ingress网络的信息:

  • vxlanid_list:是VXLAN的Segment ID,即VNI,唯一标识一个VXLAN网络的,用于标识该Swarm集群;
  • Peers:Swarm集群的Node列表信息,里面就是Node的物理IP地址,用于Service跨级路由;

除此以为,我们可以看到ingress网卡和docker_gwbridge网卡都已经Attach了一个名为:ingress-sbox的容器,这个容器是干什么的?但是我们在本机上为什么看不到该容器

带着疑问,那我们接下来在看一下,主机也多了两个网卡信息:

1
2
3
4
5
6
7
8
9
10
11
$ ip addr
7: docker_gwbridge: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:f1:3f:62:59 brd ff:ff:ff:ff:ff:ff
inet 172.18.0.1/16 brd 172.18.255.255 scope global docker_gwbridge
valid_lft forever preferred_lft forever
inet6 fe80::42:f1ff:fe3f:6259/64 scope link
valid_lft forever preferred_lft forever
24: vethe92990a@if23: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker_gwbridge state UP group default
link/ether 12:00:be:e1:f3:40 brd ff:ff:ff:ff:ff:ff link-netns ingress_sbox
inet6 fe80::1000:beff:fee1:f340/64 scope link
valid_lft forever preferred_lft forever

可以看到,Docker network对应的docker_gwbridge的主机网卡bridge网络,但是这里有两个疑问

  1. overlay网络对应的网卡去哪了?没有对应的overlay网络的网卡,只是额外多了一个veth虚拟网卡vethe92990a
  2. vethe92990a网卡干嘛的,另一端在哪?对应的veth pair的主端连接的是docker_gwbridge网卡,另一端的网卡索引为23,但是当前主机的Network Namespace并没有看到此网卡;

官方的手册Swarm Networking也没介绍相关的网络拓扑和说明,上面几个疑问,我们逐一解答:

Docker-tutorial这篇文章介绍了ingress-sbox的容器,是Docker创建并维护的特殊Network Namspace,目的是为了提供:服务发现流量路由负载均衡,它是 Swarm Service网络的大脑,它运行在所有的Swarm Node上,主要通过内核IPVS模块进行流量转发,IPVS模块会根据目标Service配置的路由算法,选择其中一个IP,通过ingress网络交由到对应的Node上。

当你创建一个Swarm服务并发布端口时,这些端口会被绑定到ingress-sbox,无论请求从哪个节点进入,ingress网络都能确保流量被正确路由到运行服务的容器上,保证负载均衡。

虽然在docker_gwbridgeingress网络信息的Containers中,ingress-sbox被归类为Container,但是技术上是一个Network Namespace,只包含网络配置,用于网络隔离和路由。由于是Docker创建的特殊Network Namspace,没有关联任何进程,是一个持久化的Namespace,所以我们通过lsns查看不到该Network Namespace:

lsns is not able to see persistent namespaces without processes where the namespace instance is held by a bind mount to /proc/pid/ns/type.

但是我们通过ip netns也查不到该Network Namespace的存在,是什么原因呢?通过ip netns手册发现它是查看/var/run/netns目录下的有名的Network Namespace。

By convention a named network namespace is an object at /var/run/netns/NAME that can be opened. The file descriptor resulting from opening /var/run/netns/NAME refers to the specified network namespace. Holding that file descriptor open keeps the network namespace alive. The file descriptor can be used with the setns(2) system call to change the network namespace associated with a task.

通过上面的lsnsip netns都查看不到ingress-sbox Network Namespace,那Docker Swarm创建的这个特殊Network Namspace到底在哪里?从Docker Swarm Container Networking我们知道ingress-sbox Network Namespace存在于/var/run/docker/netns目录下,由Docker daemon独立管理的Network Namespace:如下:

1
2
$ ls /var/run/docker/netns
1-lqchwltv5w ingress_sbox

为了方便通过ip netns进行管理,我们将Docker独立管理的这两个ns,链接到公共的netns目录下,如下:

1
2
3
4
$ ln -s /var/run/docker/netns/* /var/run/netns/
$ ip netns list
1-lqchwltv5w (id: 0)
ingress_sbox (id: 1)

然后我们看一下这两个Network Namespace里面的网卡信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
$ ip netns exec ingress_sbox ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
21: eth0@if22: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default
link/ether 02:42:c0:a6:00:02 brd ff:ff:ff:ff:ff:ff link-netns 1-lqchwltv5w
inet 192.166.0.2/24 brd 192.166.0.255 scope global eth0
valid_lft forever preferred_lft forever
23: eth1@if24: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:ac:12:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 1
inet 172.18.0.2/16 brd 172.18.255.255 scope global eth1
valid_lft forever preferred_lft forever

$ ip netns exec 1-lqchwltv5w ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
2: br0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default
link/ether 0e:9b:40:d0:9f:24 brd ff:ff:ff:ff:ff:ff
inet 192.166.0.1/24 brd 192.166.0.255 scope global br0
valid_lft forever preferred_lft forever
20: vxlan0@if20: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master br0 state UNKNOWN group default
link/ether fe:40:0b:1d:01:3c brd ff:ff:ff:ff:ff:ff link-netnsid 0
22: veth0@if21: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master br0 state UP group default
link/ether 0e:9b:40:d0:9f:24 brd ff:ff:ff:ff:ff:ff link-netns ingress_sbox

Standalone Containers基于overlay网络

由于Swarm Service的网络相对比较复杂,这里我们先通过在Swarm集群中运行基于overlayStandalone Container来观察overlay网络是如何工作的。Stanalone模式可以不创建swarm集群的情况下,使用overlay网络来实现不同容器跨Docker主机的通信能力。

首先:我们在Swarm集群的管理节点Host1(只能在manger node上创建overlay网络)上创建一个overlay的网络,如下:

1
2
$docker network create -d overlay --attachable overlay-test-1
bv8sznvlx2p1vs2lauds8zdj6

创建成功后我们可以看到有以下两个overlay的docker网卡资源,其实名为ingress的网络是docker swarm在初始化的时候创建的:

1
2
3
4
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
lqchwltv5w1c ingress overlay swarm
vfchbsa266tu overlay-test-1 overlay swarm

我们看一下overlay-test-1网络的信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
$ docker network inspect overlay-test-1
[
{
"Name": "overlay-test-1",
"Id": "bv8sznvlx2p1vs2lauds8zdj6",
"Created": "2024-11-29T04:17:57.123285722Z",
"Scope": "swarm",
"Driver": "overlay",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": null,
"Config": [
{
"Subnet": "192.166.1.0/24",
"Gateway": "192.166.1.1"
}
]
},
"Internal": false,
"Attachable": true,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": null,
"Options": {
"com.docker.network.driver.overlay.vxlanid_list": "4097"
},
"Labels": null
}
]

其中vxlanid_list:是VXLAN的Segment ID,即VNI,唯一标识一个VXLAN网络的,用于标识该Swarm集群;

接下来在Host1上启动一个Container连接到此overlay网络:

1
2
$ docker run -d -it --network overlay-test-1 --name container-host1 centos
15e42ce8fd6b3616af5f3a7a2c3a6479e54ae1d9c25b6ecf32035de9b5552a66

启动容器成功后,我们会发现Host1上多了一张网卡信息:

1
2
3
4
5
$ ip addr
45: veth602d3d0@if44: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker_gwbridge state UP group default
link/ether c2:cd:57:10:37:f3 brd ff:ff:ff:ff:ff:ff link-netns 519973c21f1e
inet6 fe80::c0cd:57ff:fe10:37f3/64 scope link
valid_lft forever preferred_lft forever

然后通过ip netns可以查看多出来三个Network Namespace:

1
2
3
4
5
6
$ ip netns
519973c21f1e (id: 4)
1-bv8sznvlx2 (id: 2)
lb_bv8sznvlx (id: 3)
1-lqchwltv5w (id: 0)
ingress_sbox (id: 1)

我们看一下新增的三个Network Namespace的网卡信息,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
$ ip netns exec 519973c21f1e ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
42: eth0@if43: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default
link/ether 02:42:c0:a6:02:04 brd ff:ff:ff:ff:ff:ff link-netns 1-bv8sznvlx2
inet 192.166.2.4/24 brd 192.166.2.255 scope global eth0
valid_lft forever preferred_lft forever
44: eth1@if45: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:ac:12:00:03 brd ff:ff:ff:ff:ff:ff link-netnsid 1
inet 172.18.0.3/16 brd 172.18.255.255 scope global eth1
valid_lft forever preferred_lft forever

$ ip netns exec 1-bv8sznvlx2 ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
2: br0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default
link/ether 62:86:34:cb:b2:7c brd ff:ff:ff:ff:ff:ff
inet 192.166.2.1/24 brd 192.166.2.255 scope global br0
valid_lft forever preferred_lft forever
39: vxlan0@if39: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master br0 state UNKNOWN group default
link/ether 62:86:34:cb:b2:7c brd ff:ff:ff:ff:ff:ff link-netnsid 0
41: veth0@if40: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master br0 state UP group default
link/ether 6a:0c:45:74:0c:80 brd ff:ff:ff:ff:ff:ff link-netns lb_bv8sznvlx
43: veth1@if42: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master br0 state UP group default
link/ether a6:54:1b:ed:66:26 brd ff:ff:ff:ff:ff:ff link-netns 519973c21f1e

$ ip netns exec lb_bv8sznvlx ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
40: eth0@if41: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default
link/ether 02:42:c0:a6:02:05 brd ff:ff:ff:ff:ff:ff link-netns 1-bv8sznvlx2
inet 192.166.2.5/24 brd 192.166.2.255 scope global eth0
valid_lft forever preferred_lft forever

新创建的container-host1容器,带来的网卡相关的拓扑如下:

新增的三个netns对应的功能为:

  • 519973c21f1e:容器自身的网络栈(eth0、路由、DNS),分配Overlay网络的IP地址;
  • 1-bv8sznvlx2:Overlay网络的核心组件,负责跨主机通信的底层实现。用于管理以下内容:
    • VXLAN隧道接口(如vxlan0):封装和解封装跨主机通信的数据包。
    • 路由表:定义如何将流量路由到其他主机或容器。
    • 网络网关:作为容器流量的出口,处理容器与外部网络的通信。
  • lb_bv8sznvlx:即使未使用Swarm模式,Docker Overlay网络也会预置负载均衡组件,用于未来可能的服务扩展(如Swarm服务)。

我们可以看到基于overlay-test-1overlay网络创建的standalone Container和普通基于bridge网络运行的Container有比较明显的区别:Container内有两张veth网卡:eth1eth0,它们的功能分别是:

  • eth1:和Docker Swarm集群外的网络通信,它和docker_gwbridge分配在一个网段;例如主机可以通过此网卡访问此Container;
  • eth0:和Docker Swarm集群内的网络通信,它分配的是overlay-test-1网卡的网段;所有基于此overlay网络的Container都通过此网卡来和此Container通信;

接着我们在Host2上查看Docker网络信息,此时并没有发现有Swarm集群中的Host1上创建的overl-test-1网络:但是我们此时在Host2上启动Container是可以加入``overl-test-1网络,如下:

1
2
$docker run -d -it --network overlay-test-1 --name container-host2 centos
da1d5347d581be0d404bdaedff93969a92536ae35d40d188e5cbcddcdb837faa

启动成功后,我们会发现Host2上的Docker网卡信息多了一个网卡,如下:

1
2
3
4
5
6
7
8
$docker network ls
NETWORK ID NAME DRIVER SCOPE
3c8b727d19cc bridge bridge local
53c59b41d257 docker_gwbridge bridge local
6d6a34280d8e host host local
lqchwltv5w1c ingress overlay swarm
f87b4f8c44ef none null local
vfchbsa266tu overlay-test-1 overlay swarm

且整体的Network Namespace和container-host-1的分布是一致的,如上图,下面我们在Host2上通过Container Name来ping Host1的容器网络,如下:

1
2
3
4
5
6
7
8
$ docker exec -it container-host2 ping container-host1
PING container-host1 (192.166.2.4) 56(84) bytes of data.
64 bytes from container-host1.overlay-test-1 (192.166.2.4): icmp_seq=1 ttl=64 time=0.437 ms
64 bytes from container-host1.overlay-test-1 (192.166.2.4): icmp_seq=2 ttl=64 time=0.365 ms
^C
--- container-host1 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1004ms
rtt min/avg/max/mdev = 0.365/0.401/0.437/0.036 ms

我们在container-host1的Host1上抓取对外的eth0网卡的VXLAN监听的默认4789端口信息,如下:

1
2
3
4
5
6
7
8
9
10
11
$ tcpdump -i eth0 -nn -v port 4789
tcpdump: listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes

16:34:03.398521 IP (tos 0x0, ttl 64, id 51392, offset 0, flags [none], proto UDP (17), length 134)
10.206.0.12.35998 > 10.206.0.37.4789: VXLAN, flags [I] (0x08), vni 4098
IP (tos 0x0, ttl 64, id 32603, offset 0, flags [DF], proto ICMP (1), length 84)
192.166.2.6 > 192.166.2.4: ICMP echo request, id 8, seq 1, length 64
16:34:03.398706 IP (tos 0x0, ttl 64, id 3343, offset 0, flags [none], proto UDP (17), length 134)
10.206.0.37.46162 > 10.206.0.12.4789: VXLAN, flags [I] (0x08), vni 4098
IP (tos 0x0, ttl 64, id 53624, offset 0, flags [none], proto ICMP (1), length 84)
192.166.2.4 > 192.166.2.6: ICMP echo reply, id 8, seq 1, length 64

然后经过针对上面各个网卡信息的监听,会发现,ICMP流量经过VXLAN的VTEP解封装后,交由1-bv8sznvlx2 Network Namespace的vxlan0 网卡,如下:

1
2
3
4
5
6
$ ip netns exec 1-bv8sznvlx2 tcpdump -i vxlan0 -nn -v
tcpdump: listening on vxlan0, link-type EN10MB (Ethernet), capture size 262144 bytes
16:34:03.398646 IP (tos 0x0, ttl 64, id 32603, offset 0, flags [DF], proto ICMP (1), length 84)
192.166.2.6 > 192.166.2.4: ICMP echo request, id 8, seq 1, length 64
16:34:03.398692 IP (tos 0x0, ttl 64, id 53624, offset 0, flags [none], proto ICMP (1), length 84)
192.166.2.4 > 192.166.2.6: ICMP echo reply, id 8, seq 1, length 64

然后流量经过1-bv8sznvlx2br0路由到veth1,然后直接转发到519973c21f1eveth pair的另一个端:eth0

1
2
3
4
5
6
$ ip netns exec 519973c21f1e tcpdump -i eth0 -nn -v
tcpdump: listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
16:34:03.398660 IP (tos 0x0, ttl 64, id 32603, offset 0, flags [DF], proto ICMP (1), length 84)
192.166.2.6 > 192.166.2.4: ICMP echo request, id 8, seq 1, length 64
16:34:03.398688 IP (tos 0x0, ttl 64, id 53624, offset 0, flags [none], proto ICMP (1), length 84)
192.166.2.4 > 192.166.2.6: ICMP echo reply, id 8, seq 1, length 64

Serivces网络

之前我们提过Swarm的Service,Service提供了服务发现、负载均衡和自动伸缩等功能,通过配置副本数、端口映射、环境变量等关键参数来管理服务的状态和行为,下面我们看一下Swarm如何管理Services的:

1
2
3
4
5
6
7
8
9
10
$ docker service create --name service-test-1 \
--replicas 3 \
--publish 8888:80 \
centos ping google.com
7dz61hopc61fbhksde0gnro8h
overall progress: 3 out of 3 tasks
1/3: running [==================================================>]
2/3: running [==================================================>]
3/3: running [==================================================>]
verify: Service converged

我们看一下Service的三个副本分布如下:Host1上启动了一个副本,Host2上启动了二个副本,

1
2
3
4
5
$ docker service ps service-test-1
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
ws8tn9zaichj service-test-1.1 centos:latest VM-0-12-ubuntu Running Running 10 minutes ago
wk499rom7wy2 service-test-1.2 centos:latest VM-0-37-ubuntu Running Running 10 minutes ago
99xfjf4gg5di service-test-1.3 centos:latest VM-0-12-ubuntu Running Running 10 minutes ago

我们先看一下Host1上的网卡和Network Namespace的变化:

1
2
3
4
5
6
7
8
9
10
11
$ ip addr
# 新增的veth网卡
2037: veth8245c0d@if2036: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker_gwbridge state UP group default
link/ether 96:3b:eb:7e:55:a0 brd ff:ff:ff:ff:ff:ff link-netns 48c24cf61ee4
inet6 fe80::943b:ebff:fe7e:55a0/64 scope link
valid_lft forever preferred_lft forever

$ ip netns
48c24cf61ee4 (id: 2) # 新增的network namespace
1-lqchwltv5w (id: 0)
ingress_sbox (id: 1)

Host2上的网卡和Network Namespace的变化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ ip addr
# 新增的两个网卡
1275: veth41c2673@if1274: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker_gwbridge state UP group default
link/ether 3a:e6:71:37:07:c2 brd ff:ff:ff:ff:ff:ff link-netns bbfe3164ce2a
inet6 fe80::38e6:71ff:fe37:7c2/64 scope link
valid_lft forever preferred_lft forever
1279: veth0f16c1f@if1278: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker_gwbridge state UP group default
link/ether 9a:9e:19:fe:9b:8c brd ff:ff:ff:ff:ff:ff link-netns 8181d9bbc1f4
inet6 fe80::989e:19ff:fefe:9b8c/64 scope link
valid_lft forever preferred_lft forever

$ ip netns
bbfe3164ce2a (id: 2) # 新增的network namespace
8181d9bbc1f4 (id: 3) # 新增的network namespace
ingress_sbox (id: 1)
1-lqchwltv5w (id: 0)

下面我们看一下Host1的Network Namespace及其网卡的结构图如下:

  • ingress-sbox Network Namespace:为了Swarm Serivce提供了服务发现流量路由负载均衡,主要通过内核IPVS模块进行流量转发,IPVS模块会根据目标Service配置的路由算法,选择其中一个IP,通过ingress网络交由到对应的Node上。
  • 1-lqchwltv5w Network Namespace:提供了VXLAN基于三层网络的内网通信能力,所有内网流量都经过此Namespace的网卡,要么经过VXLAN进行跨机转发,要么通过bridge转发至本机的Container。
  • 48c24cf61ee4 Network Namespace:新创建的Service在本机的一个副本即一个Container,链接了主机和1-lqchwltv5w 两个Network Namespace。

知道了上面的整体结构后,我们再看一下Host1上主机Network Namespace的iptables的变化,如下nat表的Chain和Rule变化:

我们看到PREROUTING 链和OUTPUT链都有自定义链DOCKER-INGRESS,用来过滤进入本机(入站路由前)的数据和出站(出站路由前)数据,匹配目标地址类型为 LOCAL 的所有协议数据包,且本地目标端口为8888的数据包,全部转发到172.18.0.2:8888

如下是filter表的Chain和Rule变化:

我们看FORWARD链(不是发给本机的数据包,需要通过本机进行路由转发的数据包)的自定义链DOCKER-INGRESS链的规则如下:

  1. 允许入站且目标端口是8888 端口流量;
  2. 允许已建立的、且来源的端口是 8888 端口的 TCP 连接;
1
$ ip netns exec ingress_sbox iptables -nvL -t mangle | cut -f -9|column -t| sed 's/^Chain/\n&/g'|sed '/^Chain/ s/[ \t]\{1,\}/ /g'|sed '/^[0-9]/ s/[ \t]\{1,\}/ /10g'|sed -e 's/\(192.166.0\|8888\)/\x1b[31m&\x1b[0m/g'

再看一下Host1上ingress_sbox的Network Namespace的iptables信息,如下nat表的Chain和Rule变化:

从上面的POSTROUTING链的SNAT规则可以得知,最终到Serivce对外端口的流量信息会通过IPVS模块进行SNAT转换后转发到192.166.0.21-lqchwltv5w Network Namespace),我们看一下IPVS的路由规则信息如下:

1
2
3
4
5
6
7
8
$ ip netns exec ingress_sbox ipvsadm --list --numeric
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
FWM 774 rr
-> 192.166.0.65:0 Masq 1 0 0
-> 192.166.0.66:0 Masq 1 0 0
-> 192.166.0.67:0 Masq 1 0 0

我们知道IPVS模块选择一个负载后,会交给1-lqchwltv5w Network Namespace,上面我们介绍了此NS提供了VXLAN基于三层网络的内网通信能力,所有内网流量都经过此Namespace的网卡,要么经过VXLAN进行跨机转发,要么通过bridge转发至本机的Container。

那VXLAN是如何通过overlay网络IP找到对应的主机的呢?

VXLAN 使用了一种封装机制,结合了ARP 和 FDB(Forwarding Database)表的功能来实现跨主机的通信。ARP 只解析内部 IP 到内部 MAC 地址,VXLAN 依赖 FDB 的额外信息来实现从内部 MAC 到远端主机(VTEP IP)的映射。

  • ARP的解析结果会存放在ARP表(IPV6中称之为邻居表:Neighbor Table)

  • FDB表的填充过程是基于数据包的动态学习的过程,每条记录为内部网络的MAC地址对应的远端VTEP的地址;

如下是1-lqchwltv5w Network Namespace的ARP表和FDB表的数据:

1
2
3
4
5
6
7
8
9
10
11
12
# 等价于ip netns exec 1-lqchwltv5w ip neigh show 
$ ip netns exec 1-lqchwltv5w arp
Address HWtype HWaddress Flags Mask Iface
192.166.0.65 ether 02:42:c0:a6:00:41 CM vxlan0
192.166.0.3 ether 02:42:c0:a6:00:03 CM vxlan0
192.166.0.67 ether 02:42:c0:a6:00:43 CM vxlan0

$ ip netns exec 1-lqchwltv5w bridge fdb show dev vxlan0
fe:40:0b:1d:01:3c master br0 permanent
02:42:c0:a6:00:41 dst 10.206.0.12 link-netnsid 0 self permanent
02:42:c0:a6:00:03 dst 10.206.0.12 link-netnsid 0 self permanent
02:42:c0:a6:00:43 dst 10.206.0.12 link-netnsid 0 self permanent

启动的Service的Container的Network Namespace的iptables信息如下:

如下是整个Docker Service网络的拓扑结构,包含了数据流入后,如何通过主机网络流入Service的ingress_sbox的Namespace,然后通过IPVS模块进行路由选择,并最终通过OVERLAY网络进行流量转发的示意图:

总结

本文主要介绍了docker的bridgeoverlay网络相关基础,其中了解了Linux内核如何进行相关网络设备的支持。主要有两个方面的收获:

  • 同主机之间,容器是如何通过bridge和iptables进行网络隔离,跨容器又是如何通过bridgeveth进行打通的。
  • 跨主机之间,容器是如何借助现有的underlay的物理网络,通过VXLAN技术构建overlay的网络通信能力。

本文主要是研究Docker的network基础,希望通过这个学习,对后面深入浅出Kubernetes的网络有基础的铺垫。

参考

https://ammarsuhail155.medium.com/kubernetes-networking-798c06b2a3ca

https://network-insight.net/2016/03/19/kubernetes-network-namespace/

https://logingood.github.io/kubernetes/cni/2016/05/14/netns-and-cni.html

https://labs.iximiuz.com/challenges/connect-multiple-network-namespaces

https://github.com/nehalineogi/azure-cross-solution-network-architectures/blob/main/advanced-linux-networking/linux-vxlan.md

https://skminhaj.wordpress.com/2016/02/15/vxlan-encapsulation-and-packet-format/

https://ops.tips/blog/blocking-ingress-traffic-to-docker-swarm-worker-machines/

https://docker-tutorial.schoolofdevops.com/swarm-networking-deepdive/

https://www.suse.com/c/docker-swarm-container-networking/

https://stackoverflow.com/questions/42804541/docker-swarm-how-to-inspect-ingress-endpoint-container

https://docs.docker.com/engine/swarm/ingress/