一文精通Docker网络

512 阅读15分钟

在深入探讨Docker网络之前,我们需要先理解以下几个关键的概念,它们是整个网络结构的基石:

  • 网络命名空间(Network Namespace):这是Linux内核的一项功能,可以为每一个进程提供独立的网络视图。
  • 网络桥接(Net Bridge):这是一个重要的工具,可以在不同的网络设备或网络段之间转发数据包。
  • iptables:这是Linux系统中常见的防火墙工具,负责处理数据包的路由和转发。
  • Veth Pair: 这是Linux不同的网络命名空间中进行通信的工具。

接下来,我们将深入探讨每一个部分,并详细讨论它们在Docker网络中的作用。

Network Namespace

在Linux操作系统中,"Namespace"是一种技术,可以用于将系统资源隔离到不同的命名空间(Namespaces),使每个命名空间内的进程只能看到同一个命名空间内的资源。这对于构建轻量级虚拟化(如容器)非常有用。

"Network Namespace"是这些命名空间类型中的一种,主要用于隔离网络接口。当你创建一个新的Network Namespace时,它会拥有自己的网络设备、IP地址、路由表等网络栈环境,而且和其它Network Namespace之间是完全隔离的。

这意味着你可以在不同的Network Namespace中配置相同的网络环境(例如,相同的IP地址和路由规则),而它们不会互相干扰。这对于创建独立运行的容器或虚拟机非常有用,因为它们可以在各自的环境中运行,就好像在物理上被分割开一样。

image.png

docker的网络隔离正是利用了Network Namespace特性,如果一个容器在使用时指定 -net=host, 那么就不会建立自己docker进程私有的Network Namespace, 这样直接使用宿主机网络栈的方式虽然简单但是有可能造成端口冲突等网络资源冲突的问题。

所以在一般情况下,我们使用docker都创建独立的Network Namespace, 即这个容器拥有自己的IP和端口,那么问题来了,被隔离的容器进程相互之间怎么进行网络通信的?

Net Bridge

我们都知道,如果两台主机需要通信,那么它们需要通过网线连接到交换机的不同端口上。而在Linux系统中,不同的Network Namespace中的进程可以利用Bridge(相当于虚拟的交换机)进行网络通信。Bridge可以根据目的mac地址转发到网桥的不同端口上,Docker默认在宿主机创建一个docker0的网桥,连接这个网桥的不同容器进程可以进行网络通信。

Net Bridge是一个二层的虚拟网络设备,需要维护mac地址和端口的映射关系。

  1. 当一个数据包到达网桥的一个接口时,网桥首先会查看该数据包的源MAC地址。这个MAC地址来自于发送这个数据包的设备。
  2. 网桥将此源MAC地址与接收到此数据包的其物理端口(即接口)进行关联,并在它的地址表(有时被称为转发表或MAC地址表)中存储这个关联信息。
  3. 当网桥需要转发一个数据包时,它会查看数据包的目标MAC地址,然后查找其地址表以确定应该通过哪个端口发送此数据包。如果目标MAC地址在地址表中,则网桥知道应该通过哪个端口转发此数据包。如果目标MAC地址不在地址表中,那么网桥会通过所有的端口(除了接收到这个数据包的端口)广播这个数据包。
  4. 为了保持地址表的准确性,在一段时间内没有看到任何来自特定MAC地址的数据包之后,网桥会从其地址表中删除该MAC地址的条目。这种超时机制可以帮助网桥适应网络的变化,例如设备被移动到另一端口或完全从网络中移除。

至于发送方是怎么知道接收方的mac地址的,需要借助ARP广播,具体可以参考请求www.baidu.com到底发生了什么

虽然网桥(bridge)主要工作在数据链路层(第二层),进行MAC地址的学习和转发数据包,但是在某些情况下可以作为一个三层设备(网桥本身也有ip):

  1. 主机管理:如果你需要远程登录到运行网桥的主机上进行管理(例如,通过SSH),那么接收的数据包将需要被送至网络层并进一步传递给传输层和应用层。
  2. 服务提供:如果网桥所在的主机同时也运行了某种网络服务,如Web服务器或者数据库服务器,那么来自客户端的请求会直接发送到这个主机的IP地址。这些数据包会在网桥中被处理,并向上传递给相应的服务处理程序。
  3. 防火墙和路由:在有些配置下,Linux桥接也可以与iptables (或nftables)一起工作,以提供更复杂的数据包过滤、修改或路由功能。这种情况下,数据包不仅在二层进行处理,还可能涉及到第三层(IP层)甚至更高层次。

有了Bridge之后,对于上层应用来说,只看到Bridge,数据包发给Bridge,由Bridge根据自己的代码判断转发到哪个端口或者哪些端口。反之从网桥连接的端口收到的报文会经过Bridge的代码处理判断应该被转发、丢弃或者提交到协议栈上层处理。

网桥常用操作如下:

  1. 创建网桥:使用brctl addbr命令创建一个新的网桥。

    sudo brctl addbr br0
    
  2. 添加接口到网桥:使用brctl addif命令将网络接口添加到网桥中。

    sudo brctl addif br0 eth0
    
  3. 删除接口从网桥:使用brctl delif命令从网桥中移除网络接口。

    sudo brctl delif br0 eth0
    
  4. 删除网桥:使用brctl delbr命令删除网桥。

    sudo brctl delbr br0
    
  5. 显示网桥信息:使用brctl show命令查看现有的网桥及其相关接口的信息。

    brctl show
    

此外,近年来,ip命令已经开始取代brctl命令。例如,你可以使用下面的命令创建一个网桥和添加接口:

# 创建一个新的网桥br0
sudo ip link add name br0 type bridge

# 将eth0添加到br0
sudo ip link set dev eth0 master br0

# 启动br0网桥
sudo ip link set dev br0 up

Veth Pair

veth设备通常以配对形式存在,因此被称为"veth pair"。这两个设备可以互相转发数据,使其中一个设备发送的数据能够直接出现在另一个设备上。你可以将其视为一个管道:数据从一端输入,就会从另一端输出。这对虚拟网络卡(Veth Peer)在创建时总是同时出现,并且即使它们位于不同的Network Namespace中,一个设备的输出也会立即出现在另一个设备上。因此,veth pair常被视为不同Network Namespace之间的"直连网线"。

image.png

当启动Docker容器时,系统会创建一个名为veth pair的接口对。这可以视作系统内部的一个数据传输"隧道"。当一端发送数据时,另一端也会接收到相同的数据。这对接口的一端,被称为eth0,位于容器内部;另一端则保持在主机上,并连接到名为docker0的网桥,通常其名称以veth开头。通过这种配置,主机和容器之间就像建立了一个直接的"隧道",使得数据可以互相流通。而各个容器则可以通过主机的docker0网桥间接实现通信。

使用ip命令来创建和管理veth pair。以下是一些基本的操作:

  1. 创建veth pair:使用ip link add命令可以创建一个新的veth pair。例如,创建一个veth pair: veth0 & veth1。

    sudo ip link add veth0 type veth peer name veth1
    
  2. 设置veth接口的状态:使用ip link set命令可以改变veth接口的状态,比如将其设置为up(启动)或down(关闭)。例如,启动veth0接口。

    sudo ip link set veth0 up
    
  3. 移动veth接口到其他网络命名空间:你还可以使用ip link set命令将veth接口移动到其他网络命名空间。例如,将veth1接口移动到名为netns0的网络命名空间。

    sudo ip link set veth1 netns netns0
    
  4. 在namespace下查看veth设备

    ip netns exec netns0 ip link show
    
  5. 删除veth pair:使用ip link delete命令可以删除一个veth pair。注意,只需要删除其中一个接口就可以了,因为删除任何一个接口都会自动删除整个veth pair。例如,删除veth0接口。

    sudo ip link delete veth0
    

如何在两个不同的网络命名空间(一个在Docker容器内,另一个在主机上)之间创建一对veth设备,并进行通信

  1. 根据 容器ID($container_id)获取容器进程的pid:
sudo docker inspect -f '{{.State.Pid}}' $container_id
  1. 在主机网络命名空间中创建一对veth设备:

    sudo ip link add veth0 type veth peer name veth1
    
  2. veth1移到容器的网络命名空间:

    sudo ip link set veth1 netns $pid
    
  3. 在主机上配置veth0设备:

    sudo ip addr add 10.1.1.2/24 dev veth0
    sudo ip link set veth0 up
    
  4. 在容器网络命名空间内配置veth1设备:

    sudo nsenter -t $pid -n ip addr add 10.1.1.1/24 dev veth1
    sudo nsenter -t $pid -n ip link set veth1 up
    
  5. 在容器中测试连接到主机:

    docker exec $container_id ping -c 3 10.1.1.2
    

注意,这只是为了演示如何手动设置veth设备并在Docker容器和主机之间进行通信。实际上,Docker会自动处理所有的这些网络设置。

set netns命令后面跟的是PID(Process ID),而不是namespace。这是因为每个进程在Linux系统中都有自己的一套名称空间,该命令通过指定PID实际上是引用了对应进程的网络名称空间。所以,当执行sudo ip link set veth1 netns $pid这条命令时,你正在把 veth1 接口移到 PID 为 $pid 的进程所在的网络命名空间

Iptables/Netfilter

Netfilter和Iptables是Linux系统网络包处理过程中的关键技术。

Netfilter 是内核级别的一种框架,它在网络堆栈中提供了一系列的钩子函数,允许特定的内核模块在数据包的处理过程中被调用并执行操作(例如修改、丢弃等)。而iptables则是用户级别的工具,它使用Netfilter提供的功能来定义和应用规则,从而控制网络数据包如何在系统中流动。这些规则可以包括源和目标IP地址、传输协议类型(TCP、UDP等)、端口号等参数的过滤。

以一个实际例子来说,如果你想要阻止所有来自特定IP地址的流量,你可以使用Iptables创建一个规则,当一个从该IP地址发出的数据包到达时,Netfilter将拦截并丢弃该数据包,使其无法进一步在网络堆栈中进行传输。

总的来说,Netfilter/Iptables机制为Linux提供了强大的网络流量控制和包过滤能力。这种灵活性使得Linux成为路由器、防火墙等网络设备的理想选择。

在iptables中,表(tables)和链(chains)是其核心概念。表是一组具有特定目标的规则的集合,每个表中又包含了多个预定义的链,这些链代表了数据包处理流程中的各个点。在iptables中,有以下四种主要的规则表:

  1. Filter:这是默认的或最常见的表。它主要用于过滤数据包。此表有三个预设链:INPUT, OUTPUT, and FORWARD。
  2. NAT:这个表主要用于网络地址转换(Network Address Translation)。它含有三个预设链:PREROUTING, POSTROUTING, and OUTPUT。
  3. Mangle:这个表主要用于特殊类型的数据包处理 – 如更改IP头部像TTL、TOS等字段。它包含五个预设链:PREROUTING, POSTROUTING, INPUT, OUTPUT, and FORWARD。
  4. Raw:这个表主要用于配置 exemptions from connection tracking 在特定情况下。它含有两个预设链:PREROUTING and OUTPUT。

每个表都存储了一系列规则Rule,Rule包括一个条件和一个目标,满足条件则执行目标,不满足就跳到下一条Rule匹配。

通常,数据包的流转顺序如下:

  1. PREROUTING:数据包首先达到系统时会进入PREROUTING链。这个阶段主要用于处理目标地址转换(即DNAT),相关规则定义在NAT表中,这时候去查NAT表就行。
  2. INPUT:如果数据包的目标是本机,那么它将被送往INPUT链进行处理。这里的规则通常定义在Filter表和Mangle表中,分别对应基本的包过滤和特殊的包处理功能。
  3. FORWARD:如果数据包需要被路由(即本机并非其最终目标),那么数据包将被送往FORWARD链。同样,这里的规则通常定义在Filter表和Mangle表中, 在Filter表中进行过滤决策,Mangle表进行必要修改。
  4. OUTPUT:如果数据包是由本机产生的,那么在发送前会被送往OUTPUT链。规则位置与INPUT链相同,也在Filter表和Mangle表中。
  5. POSTROUTING:数据包离开系统前会经过POSTROUTING链。这个阶段主要用于处理源地址转换(即SNAT),相关规则定义在NAT表中。

查看各表中的规则命令

# 查看 (policy ACCEPT) 表示默认规则是接收,在没有匹配到任何一条rule就用默认规则  
iptables -t filter -L  
Chain INPUT (policy ACCEPT)  
target     prot opt source               destination  
  
Chain FORWARD (policy ACCEPT)  
target     prot opt source               destination  
  
Chain OUTPUT (policy ACCEPT)  
target     prot opt source               destination

image.png

容器网络

Linux容器的网络是被隔离在它自己的Network Namespace中,其中就包括:网卡(Network Interface)、回环设备(Loopback Device)、路由表(Routing Table)和iptables规则。对于一个进程来说,这些要素,就构成了它发起和响应网络请求的基本环境

我们在执行 docker run -d --name xxx   之后,进入容器内部执行ifconfig可以看到eth0的地址:

/ # ifconfig
eth0      Link encap:Ethernet  HWaddr 02:42:AC:3E:00:0C  
          inet addr:172.62.0.12  Bcast:172.62.255.255  Mask:255.255.0.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:1325 errors:0 dropped:0 overruns:0 frame:0
          TX packets:1315 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0 
          RX bytes:132224 (129.1 KiB)  TX bytes:129400 (126.3 KiB)

lo        Link encap:Local Loopback  
          inet addr:127.0.0.1  Mask:255.0.0.0
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:14 errors:0 dropped:0 overruns:0 frame:0
          TX packets:14 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000 
          RX bytes:898 (898.0 B)  TX bytes:898 (898.0 B)

eth0网卡正是一个Veth Pair设备在容器的这一端,而Veth Pair 设备的另一端,则在宿主机上, 可以通过brctl show看到Veth Pair的一端就插在docker0上。

(base) guwanhua@guwanhua-PC:~/Desktop$ sudo docker inspect -f '{{.State.Pid}}' mysql
3520
(base) guwanhua@guwanhua-PC:~/Desktop$ sudo nsenter -t 3520 -n ip link show type veth
16: eth0@if17: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group defaul

接口名后面的@if17表示该接口的另一端ifindex为17, 我们通过在宿主机上执行下面命令:

(base) guwanhua@guwanhua-PC:~/Desktop$ sudo ip link show type veth
13: veth5a59d43@if12: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-ce97d61136f8 state UP mode DEFAULT group default 
    link/ether 22:da:a1:6d:64:e1 brd ff:ff:ff:ff:ff:ff link-netnsid 1
15: vethb89f316@if14: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-5eab437f1759 state UP mode DEFAULT group default 
    link/ether ce:39:ea:41:d6:15 brd ff:ff:ff:ff:ff:ff link-netnsid 2
17: vethff5bbd9@if16: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-0ae75e12de2c state UP mode DEFAULT group default 
    link/ether a2:f1:e7:d9:3b:aa brd ff:ff:ff:ff:ff:ff link-netnsid 0

17对应的是vethff5bbd9,vethff5bbd9后面的@16对应的编号是容器内的16,所以容器的eth0和宿主机的vethff5bbd9是一对veth接口,并且还能看到vethff5bbd9信息中有br-0ae75e12de2c,说明vethff5bbd9是属于br-0ae75e12de2c接口的。

自定义网络

在Docker的桥接模式下,不能直接指定容器IP,因为Docker自动管理内部IP池并分配给新容器, 比如使用下面的命令会报错:

docker run -d --name nginx --net bridge --ip 192.168.0.19 nginx 

但我们可以间接控制,通过创建自定义桥接网络并设定子网和网关,从而影响Docker分配的IP地址范围。

docker network create --driver bridge --subnet 192.168.0.0/16 --opt "com.docker.network.bridge.name "="mybridge" --gateway 192.168.0.1 my_custom_network

在未通过--opt参数指定名字时,执行ifconfig -a命令会显示类似于br-110eb56a0b22的网卡名称,这样的名称记忆起来并不方便。而my_custom_network则是使用docker network list命令时展示出的bridge网络模式的名称。

一旦你创建了自定义网络,就可以在该网络上启动新的 Docker 容器。例如:

docker run -d --network=my_custom_network --ip 192.168.0.19 nginx

同一主机容器互通

image.png

查看路由表:

/ # route
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
default         172.17.0.1      0.0.0.0         UG    0      0        0 eth0
172.17.0.0      *               255.255.0.0     U     0      0        0 eth0

上面的路由表包含两条规则:

  1. default 172.17.0.1:这是默认路由,表示如果数据包的目的地不符合任何其他已知网络,则将其发送至此地址。"UG"代表:

    • U (Up):该路由当前是启用的。
    • G (Gateway):目标可以通过一个网关(即172.62.0.1)到达。 这条规则指出所有未能匹配其他规则的流量,应当被发送到IP地址为172.62.0.1的网关。
  2. 172.17.0.0:这条规则适用于目标IP地址在172.17.0.0/16子网内的所有数据包。* 表示没有特定的下一跳或网关,数据包将会直接发送至本地连接的网络。"U"代表该路由是活动的,并且可以直接使用。这条规则指出所有目标为172.17.0.0/16子网的流量,应直接通过eth0设备进行处理。

总结来说,这个容器的网络流量会被以下方式处理:对于发往172.17.0.0/16子网的流量,它会直接通过eth0设备发送;对于所有其他的流量,它会被送至172.17.0.1这个网关。

当在容器1里访问容器2的地址,这个时候目的IP地址会匹配到容器1的第二条路由规则,这条路由规则的Gateway是*(0.0.0.0),意味着这是一条直连规则,也就是说凡是匹配到这个路由规则的请求,会直接通过eth0网卡,通过二层网络发往目的主机。而要通过二层网络到达容器2,就需要127.17.0.3对应的MAC地址。容器1的网络协议栈就需要通过eth0网卡来发送一个ARP广播,通过IP找到MAC地址。这里说到的eth0,就是Veth Pair的一端,另一端则插在了宿主机的docker0网桥上。eth0这样的虚拟网卡插在docker0上,也就意味着eth0变成docker0网桥的"从设备"。从设备会降级成docker0设备的端口,而调用网络协议栈处理数据包的资格全部交给docker0网桥。

所以,在收到ARP请求之后,docker0就会扮演二层交换机的角色,把ARP广播发给其它插在docker0网桥的虚拟网卡上,这样,127.17.0.3就会收到这个广播,并把其MAC地址返回给容器1。有了这个MAC地址,容器1的eth0的网卡就可以把数据包发送出去。

这个数据包会经过Veth Pair在宿主机的另一端veth26cf2cc,直接交给docker0。docker0转发的过程,就是继续扮演二层交换机,docker0根据数据包的目标MAC地址,在CAM表查到对应的端口为veth8762ad2,然后把数据包发往这个端口。而这个端口,就是容器2的Veth Pair在宿主机的另一端,这样,数据包就进入了容器2的Network Namespace,最终容器2将响应(Pong)返回给容器1。

容器访问外网

在Docker使用桥接模式的情况下,当容器A执行curl www.baidu.com之后,它首先会解析baidu的IP地址,然后创建一个目标IP为baidu.com的数据包, 数据包将通过容器的网络堆栈,然后通过veth设备发送到docker0网桥。然后,数据包将从docker0传输到主机的IP堆栈,并最终通过物理网络接口(如eth0)传出到外部网络。在数据包离开主机之前,iptables的POSTROUTING链上的SNAT规则会被应用,以将源IP更改为主机的公共IP。本机的网络协议栈收到报文后怎么访问baidu可以参考请求www.baidu.com到底发生了什么

容器要想访问外部网络,需要本地系统的转发支持。在Linux 系统中,检查转发是否打开。

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

如果为 0,说明没有开启转发,则需要手动打开。

$sysctl -w net.ipv4.ip_forward=1

net.ipv4.ip_forward = 1 的设置不是专门针对容器环境的。在任何Linux系统中,无论是否使用容器技术,都可以启用或禁用这个设置。 net.ipv4.ip_forward = 1 这条指令告诉操作系统,允许它将接收到的、并非发往自身的IP数据包转发(forward)出去。这就意味着你的机器可以作为一个路由器,处理并转发网络流量。 在某些场景下,这是非常有用的。例如,如果你的机器连接了多个网络,你可能需要它在这些网络之间进行路由。或者,如果你正在运行虚拟机或容器,并且希望它们能够访问外部网络,那么你也需要开启IP转发。

容器端口映射原理(外部访问容器)

当你运行一个Docker命令并用-p或者-P标志指定端口映射时,例如docker run -p 80:8080 some_image,Docker将在宿主机的网络堆栈和容器之间创建一条路径,以便数据包能从宿主机转发到容器。 这其中就涉及到了iptables的规则。iptables是Linux系统下的一个命令行工具,可以设定并管理操作系统内核的防火墙规则,控制进出该主机的网络流量。Docker在后台自动设置iptables规则以完成端口转发。

使用 -p 80:8080 时会自动添加DNAT规则

$ iptables -t nat -nL
Chain DOCKER (2 references)
target     prot opt source               destination
DNAT       tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:80 to:172.17.0.2:8080

这里的规则映射了 0.0.0.0,意味着将接受主机来自所有接口的流量。

除了添加DNAT规则之后,在容器的数据包离开主机的时候,也会把数据包的源地址改成宿主机地址,其实就是在nat表的POSTROUTING增加一条规则,类似下面这种:

iptables -t nat -A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE

将所有来自docker的包172.17.0.0/16的ip替换为本机ip并发送,以达到docker访问外网的目的

跨主通信

bridge桥接器并不支持跨多台电脑(或者说,多个宿主机)的Docker容器之间的通信, 要实现跨主通信,下面是一些粗略的想法:

  1. 公共网桥:是否可以创建一个公共的网桥,让集群中的所有容器都可以通过这个公共网桥进行连接。理论上讲,如果你能控制整个网络环境,并且能设置合适的专门路由规则,这是可能的。但是实际操作起来可能比较困难,需要更复杂的网络配置。
  2. NAT和IP冲突问题:NAT,或者称为网络地址转换,是让内部网络与外部网络进行通信的一种方式。然而,如果你在容器中运行应用,并向注册中心注册服务时,这可能会导致问题。因为每个Docker容器都有自己的IP地址,这可能会导致多个容器有同样的IP地址,从而产生冲突。为了避免这种冲突,应用可能需要使用宿主机的IP和映射端口进行注册。但是这就要求应用需要知道自己运行在一个容器内,并且需要知道宿主机的IP。这并不是一个好的设计,因为这需要应用去配合特定的环境配置。

image.png

所以,基于此,我们肯定要寻找其他的容器网络解决方案。在上图这种容器网络中,我们需要在我们已有的主机网络上,通过软件构建一个覆盖在多个主机之上,且能把所有容器连通的虚拟网络。这种就是Overlay Network(覆盖网络)。
关于这些具体的网络解决方案,有例如Flannel、Calico等。