hexiaodai

单节点容器网络动手实验

本文会在单节点上实验 bridge 网络模型,揭示 docker 项目网络的实现原理。

我们按照下图创建网络拓扑,让容器之间网络互通,从容器内部可以访问外部资源,同时,容器内可以暴露服务让外部访问。

单节点容器网络

容器的网络资源是被隔离在 Network Namespace 中的。拥有自己的网卡、路由表、iptables 规则。对于一个进程来说,这些设备就构成了它发起和响应网络请求的基本环境。

单节点容器网络通信是通过 Veth Pairs、Bridge、路由表、iptables 规则实现的。

Veth Pairs 是成对出现的两张虚拟网卡,从一端发送的数据包,总会在另一端接收到。容器网络正是利用 Veth Pairs 的特性,将一端的虚拟网卡放入容器内,另一端接入虚拟交换机(Bridge)。这样,接入同一个虚拟交换机(Bridge)的容器之间就实现了网络互通。

Bridge 是 Linux 中的虚拟交换机,连接在同一个虚拟交换机上的容器组成局域网。

路由表 打通了容器间 IP 数据包的流向。在容器内有一条默认路由,指向 Veth 虚拟网卡,这张虚拟网卡正是连接在虚拟交换机(Bridge)上的一端。而在宿主机上有一条容器网段的路由,指向虚拟交换机(Bridge)。

iptables 规则 容器能访问外部网络,需要在 POSTROUTING 链中添加一条 SNAT 规则,将数据包的源 IP 修改为宿主机的 IP,然后经过宿主机的网卡发出去。宿主机能访问容器内暴露的服务,需要在 PREROUTING 链中添加一条 DNAT 规则,将请求转发到容器内。

PREROUTING:数据包进入路由表之前,插入 hook 函数

FORWARDING:通过路由表后,目的地不为本机,则转发到其它设备处理

POSTROUTIONG:发送到网卡接口之前,插入 hook 函数

实验目的

掌握单节点容器网络通信原理,理解容器网络通信过程。

实验环境

注意,请在虚拟机内折腾,以免干扰工作环境。

OS 用户 主机网卡 主机 IP 容器网段
Ubuntu 22.04 root ens33 192.168.245.168 172.17.0.0/24

安装依赖

apt update
apt install bridge-utils

动手实验

下文中,“容器”指的是 Network Namespace。

场景一:容器间的网络互通

  1. 创建容器:

    ip netns add docker0
    ip netns add docker1
    

    查看创建出来的容器:

    ls -l /var/run/netns
    -r--r--r-- 1 root root 0 Dec  4 08:42 docker0
    -r--r--r-- 1 root root 0 Dec  4 08:45 docker1
    
  2. 创建虚拟网卡(Veth Pairs):

    # 给 docker0 容器使用
    ip link add veth0 type veth peer name veth1
    # 给 docker1 容器使用
    ip link add veth2 type veth peer name veth3
    

    查看创建出来的 veth pairs 设备:

    ip a
    5: veth1@veth0: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN group default qlen 1000
     link/ether e2:11:d7:8f:91:f8 brd ff:ff:ff:ff:ff:ff
    6: veth0@veth1: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN group default qlen 1000
     link/ether 0a:b4:a6:75:62:d8 brd ff:ff:ff:ff:ff:ff
    7: veth3@veth2: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN group default qlen 1000
     link/ether d6:ff:ba:b7:84:83 brd ff:ff:ff:ff:ff:ff
    8: veth2@veth3: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN group default qlen 1000
     link/ether 1e:47:4d:57:ab:b8 brd ff:ff:ff:ff:ff:ff
    
  3. 将 veth pairs 设备的一端放入容器:

    ip link set veth0 netns docker0
    ip link set veth2 netns docker1
    

    查看 docker0 容器的虚拟网卡:

    ip netns exec docker0 ip a
    6: veth0@if5: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
     link/ether 0a:b4:a6:75:62:d8 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    
  4. 创建虚拟交换机(Bridge):

    brctl addbr br0
    
  5. 将 veth pairs 设备另一端加入虚拟交换机:

    # docker0 容器虚拟网卡的另一端
    brctl addif br0 veth1
    # docker1 容器虚拟网卡的另一端
    brctl addif br0 veth3
    

    查看插在虚拟交换机上的虚拟网卡:

    brctl show
    bridge name	bridge id		STP enabled	interfaces
    br0		8000.4e7511db0f1e	no		veth1
                             veth3
    

    两张虚拟网卡 veth1 和 veth3 已经插在虚拟交换机 br0 上。并且 veth1 的另一旦端 veth0 已经加入 docker0 容器,veth3 的另一端 veth2 已经加入 docker1 容器。

  6. 为容器内的虚拟网卡分配 IP 地址,并且激活虚拟网卡:

    # docker0 容器
    ip netns exec docker0 ip addr add 172.17.0.2/24 dev veth0
    ip netns exec docker0 ip link set veth0 up
    # docker1 容器
    ip netns exec docker1 ip addr add 172.17.0.3/24 dev veth2
    ip netns exec docker1 ip link set veth2 up
    

    查看 docker0 和 docker1 容器的网卡和路由表:

    ip netns exec docker0 ip a
    5: veth0@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
     link/ether 0a:b4:a6:75:62:d8 brd ff:ff:ff:ff:ff:ff link-netnsid 0
     inet 172.17.0.2/24 scope global veth0
        valid_lft forever preferred_lft forever
     inet6 fe80::8b4:a6ff:fe75:62d8/64 scope link
        valid_lft forever preferred_lft forever
        
    ip netns exec docker1 ip a
    7: veth2@if6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
     link/ether 1e:47:4d:57:ab:b8 brd ff:ff:ff:ff:ff:ff link-netnsid 0
     inet 172.17.0.3/24 scope global veth2
        valid_lft forever preferred_lft forever
     inet6 fe80::1c47:4dff:fe57:abb8/64 scope link
        valid_lft forever preferred_lft forever
    
    ip netns exec docker0 route -n
    Kernel IP routing table
    Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
    172.17.0.0      0.0.0.0         255.255.255.0   U     0      0        0 veth0
       
    ip netns exec docker1 route -n
    Kernel IP routing table
    Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
    172.17.0.0      0.0.0.0         255.255.255.0   U     0      0        0 veth2
    

    显然,docker0 和 docker1 容器内,有容器网段 172.17.0.0/24 的路由信息,并且连接的是 veth 虚拟网卡。

  7. 激活插在虚拟交换机上的虚拟网卡:

    ip link set veth1 up
    ip link set veth3 up
    
  8. 为虚拟交换机分配 IP 地址,并且激活虚拟交换机:

    ip addr add 172.17.0.1/24 dev br0
    ip link set br0 up
    
  9. 测试 docker0 和 docker1 容器间的连通性

    从 docker0 ping docker1:

    ip netns exec docker0 ping -w 3 172.17.0.3
    PING 172.17.0.3 (172.17.0.3) 56(84) bytes of data.
    64 bytes from 172.17.0.3: icmp_seq=1 ttl=64 time=0.030 ms
    

    从 docker1 ping docker0:

    ip netns exec docker1 ping -w 3 172.17.0.2
    PING 172.17.0.2 (172.17.0.2) 56(84) bytes of data.
    64 bytes from 172.17.0.2: icmp_seq=1 ttl=64 time=0.159 ms
    

小结:

通过 Veth Pairs 设备和虚拟交换机(Bridge),打通了单节点容器间的网络。

场景二:从宿主机访问容器内网络

  1. 查看宿主机的网卡和路由表:

    ip a
    2: ens33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
     link/ether 00:0c:29:d0:08:1e brd ff:ff:ff:ff:ff:ff
     altname enp2s1
     inet 192.168.245.168/24 metric 100 brd 192.168.245.255 scope global dynamic ens33
        valid_lft 910sec preferred_lft 910sec
     inet6 fe80::20c:29ff:fed0:81e/64 scope link
        valid_lft forever preferred_lft forever
    8: br0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
     link/ether 4e:75:11:db:0f:1e brd ff:ff:ff:ff:ff:ff
     inet 172.17.0.1/24 scope global br0
        valid_lft forever preferred_lft forever
     inet6 fe80::4c75:11ff:fedb:f1e/64 scope link
        valid_lft forever preferred_lft forever
    
    route -n
    Kernel IP routing table
    Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
    0.0.0.0         192.168.245.2   0.0.0.0         UG    100    0        0 ens33
    172.17.0.0      0.0.0.0         255.255.255.0   U     0      0        0 br0
    
  2. 进入到 docker0 容器,并且监听 80 端口:

    ip netns exec docker0 nc -lp 80
    
  3. 打开一个新的终端,在宿主机上访问 docker0 容器的 80 端口:

    telnet 172.17.0.2 80
    Trying 172.17.0.2...
    Connected to 172.17.0.2.
    Escape character is '^]'.
    hello
    world
    

小结:

通过 Veth Pairs 设备和虚拟交换机(Bridge),打通了宿主机和容器间的网络。

场景三:从容器内访问外网

  1. 配置 Linux 内核参数,允许 IP forward:

    IP forward 允许在网络设备上将接收到的数据包从一个网络接口转发到另一个网络接口(从网桥转发到宿主机网卡)。

    sysctl net.ipv4.conf.all.forwarding=1
    
  2. 配置 iptables FORWARD 规则:

    iptables -P FORWARD ACCEPT
    
  3. 将 docker0 和 docker1 的默认网关设置成虚拟交换机的 IP:

    ip netns exec docker0 route add default gw 172.17.0.1 veth0
    ip netns exec docker1 route add default gw 172.17.0.1 veth2
    

    查看 docker0 和 docker1 容器的路由表:

    ip netns exec docker0 route -n
    Kernel IP routing table
    Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
    0.0.0.0         172.17.0.1      0.0.0.0         UG    0      0        0 veth0
    172.17.0.0      0.0.0.0         255.255.255.0   U     0      0        0 veth0
    
    ip netns exec docker1 route -n
    Kernel IP routing table
    Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
    0.0.0.0         172.17.0.1      0.0.0.0         UG    0      0        0 veth2
    172.17.0.0      0.0.0.0         255.255.255.0   U     0      0        0 veth2
    

    显然,docker0 和 docker1 容器内,有一条默认路由信息,Gateway 是虚拟交换机(Bridge)的 IP,所有网段的数据包会经过 veth 虚拟网卡,进入到 br0 网桥(进入到虚拟交换机)。

  4. 首先尝试从容器内访问外部地址:

    180.101.50.242 是 baidu 的 IP 地址

    ip netns exec docker0 ping 180.101.50.242
    

    然后使用 tcpdump 分别抓 docker0 容器内的 veth 虚拟网卡、br0 网桥、宿主机 ens33 网卡的数据包:

    # tcpdump: docker0 容器内的 veth 虚拟网卡
    ip netns exec docker0 tcpdump -i veth0 -n
    tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
    listening on veth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
    09:53:22.619581 IP 172.17.0.2 > 180.101.50.242: ICMP echo request, id 10678, seq 176, length 64
    09:53:23.643672 IP 172.17.0.2 > 180.101.50.242: ICMP echo request, id 10678, seq 177, length 64
       
    # tcpdump: br0 网桥
    tcpdump -i br0 -n
    tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
    listening on br0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
    09:55:14.235818 IP 172.17.0.2 > 180.101.50.242: ICMP echo request, id 10678, seq 285, length 64
    09:55:15.259379 IP 172.17.0.2 > 180.101.50.242: ICMP echo request, id 10678, seq 286, length 64
    
    # tcpdump: ens33 宿主机网卡
    tcpdump -i ens33 -n | grep 180.101.50.242
    tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
    listening on ens33, link-type EN10MB (Ethernet), snapshot length 262144 bytes
    09:56:13.627599 IP 172.17.0.2 > 180.101.50.242: ICMP echo request, id 10678, seq 343, length 64
    09:56:14.651685 IP 172.17.0.2 > 180.101.50.242: ICMP echo request, id 10678, seq 344, length 64
    

    显然,docker0 容器没有收到 baidu 回复的数据包。

    这是因为容器的 IP 地址外部并不认识(外部指的是宿主机连接在公网的路由器),如果它要访问外网,需要在数据包离开前将源地址替换为宿主机的 IP,这样外部主机才能用宿主机的 IP 作为目的地址响应。

    这里的“外部主机”指的是宿主机连接在公网的路由器,不是 baidu 的服务器。

  5. 配置 iptables 的 SNAT 规则:

    iptables -t nat -A POSTROUTING -s 172.17.0.0/24 ! -o br0 -j MASQUERADE
    

    这条规则的作用是:当数据包的源地址为 172.17.0.0/24 网段(容器网段地址),出口设备不是 br0 时(不是虚拟交换机),就执行 MASQUERADE 动作。MASQUERADE 是一种源地址转换动作,它会动态选择宿主机的一个 IP 做源地址转换,而 SNAT 动作必须在命令中指定固定的 IP 地址。

  6. 从容器内访问外部地址:

    ip netns exec docker0 ping 180.101.50.242
    

    然后使用 tcpdump 分别抓 docker0 容器内的 veth 虚拟网卡、br0 网桥、宿主机 ens33 网卡的数据包:

    # tcpdump: docker0 容器内的 veth 虚拟网卡
    ip netns exec docker0 tcpdump -i veth0 -n
    tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
    listening on veth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
    10:13:06.483429 IP 172.17.0.2 > 180.101.50.242: ICMP echo request, id 53878, seq 4, length 64
    10:13:06.497865 IP 180.101.50.242 > 172.17.0.2: ICMP echo reply, id 53878, seq 4, length 64
    
    # tcpdump: br0 网桥
    tcpdump -i br0 -n
    tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
    listening on br0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
    10:13:48.624988 IP 172.17.0.2 > 180.101.50.242: ICMP echo request, id 3366, seq 8, length 64
    10:13:48.644290 IP 180.101.50.242 > 172.17.0.2: ICMP echo reply, id 3366, seq 8, length 64
    
    # tcpdump: ens33 宿主机网卡
    tcpdump -i ens33 -n | grep 180.101.50.242
    tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
    listening on ens33, link-type EN10MB (Ethernet), snapshot length 262144 bytes
    10:15:20.643947 IP 192.168.245.168 > 180.101.50.242: ICMP echo request, id 45166, seq 18, length 64
    10:15:20.675225 IP 180.101.50.242 > 192.168.245.168: ICMP echo reply, id 45166, seq 18, length 64
    

    显然,docker0 容器收到了 baidu 回复的数据包。

    通过 tcpdump 分别监听:docker0 容器内的 veth 虚拟网卡、br0 网桥、ens33 宿主机网卡。

    发现 docker0 容器的 veth 虚拟网卡和 br0 网桥,docker0 容器发出的数据包源 IP 地址是 docker0 容器 veth 虚拟网卡的 IP,目的地址是 baidu 服务器的 IP。

    ens33 宿主机网卡,docker0 容器发出的数据包源 IP 地址是宿主机 ens33 网卡的 IP,目的地址是 baidu 服务器的 IP。

    这正是 iptables SNAT(MASQUERADE)规则起作用了。

小结:

通过 iptables POSTROUTING 链增加 SNAT 规则,在数据包出网卡前执行 SNAT 源地址转换,将数据包源 IP 地址修改为宿主机的 IP。打通了容器访问外部网络的限制。

场景四:从外部访问容器内暴露的服务

常见的场景:把 docker 容器作为服务,暴露给外部主机访问(通过宿主机的 IP:Port 访问)。

  1. 配置 iptables 的 DNAT 规则:

    # 暴露 docker0 容器监听在 80 端口的服务
    iptables -t nat -A PREROUTING  ! -i br0 -p tcp -m tcp --dport 80 -j DNAT --to-destination 172.17.0.2:80
    

    这条规则的作用是:当输入设备不是 br0(虚拟交换机),目的端口为 80 时,做目的地址转换,将宿主机 IP 和端口替换为容器 IP 和端口。

  2. 从远程访问容器内暴露的服务:

    下面演示从 MAC OS 物理机,访问 VMware 虚拟主机中的 docker0 容器内监听在 80 端口的服务。

    整条链路:物理机 -> 虚拟机 -> docker0 容器监听在 80 端口的服务。

    其中,物理机跟虚拟机之间的网络是通过 VMware 虚拟机网桥 bridge102 连接的。

    ➜ ip a
    bridge102: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
    	ether d2:81:7a:d9:fc:66
    	inet 192.168.245.1/24 brd 192.168.245.255 bridge102
    	inet6 fe80::10e8:9150:2740:3eb3/64 secured scopeid 0x15
    

    在 docker0 容器内监听 80 端口的服务:

    ip netns exec docker0 nc -lp 80
    hello world
    你好,世界
    

    在 MAC OS 物理机上,访问 VMware 虚拟机 192.168.245.168:80 暴露的服务:

    ➜ telnet 192.168.245.168 80
    Trying 192.168.245.168...
    Connected to 192.168.245.168.
    Escape character is '^]'.
    hello world
    你好,世界
    

    然后使用 tcpdump 分别抓 docker0 容器内的 veth 虚拟网卡、br0 网桥、宿主机 ens33 网卡的数据包:

    # tcpdump: docker0 容器内的 veth 虚拟网卡
    ip netns exec docker0 tcpdump -i veth0 -n
    tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
    listening on veth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
    10:43:21.899740 IP 192.168.245.1.49622 > 172.17.0.2.80: Flags [P.], seq 600752655:600752668, ack 1742415664, win 2058, options [nop,nop,TS val 766307193 ecr 3027406862], length 13: HTTP
    10:43:21.899808 IP 172.17.0.2.80 > 192.168.245.1.49622: Flags [.], ack 13, win 509, options [nop,nop,TS val 3027421984 ecr 766307193], length 0
    10:43:32.062495 IP 172.17.0.2.80 > 192.168.245.1.49622: Flags [P.], seq 1:17, ack 13, win 509, options [nop,nop,TS val 3027432147 ecr 766307193], length 16: HTTP
    10:43:32.063116 IP 192.168.245.1.49622 > 172.17.0.2.80: Flags [.], ack 17, win 2058, options [nop,nop,TS val 766317311 ecr 3027432147], length 0
    
    # tcpdump: br0 网桥
    tcpdump -i br0 -n
    listening on br0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
    10:46:39.256663 IP 192.168.245.1.49622 > 172.17.0.2.80: Flags [P.], seq 600752668:600752681, ack 1742415680, win 2058, options [nop,nop,TS val 766504040 ecr 3027432147], length 13: HTTP
    10:46:39.256795 IP 172.17.0.2.80 > 192.168.245.1.49622: Flags [.], ack 13, win 509, options [nop,nop,TS val 3027619341 ecr 766504040], length 0
    10:46:46.115069 IP 172.17.0.2.80 > 192.168.245.1.49622: Flags [P.], seq 1:17, ack 13, win 509, options [nop,nop,TS val 3027626199 ecr 766504040], length 16: HTTP
    10:46:46.115454 IP 192.168.245.1.49622 > 172.17.0.2.80: Flags [.], ack 17, win 2058, options [nop,nop,TS val 766510873 ecr 3027626199], length 0
    
    # tcpdump: ens33 宿主机网卡
    tcpdump -i ens33 -n | grep 192.168.245.1
    01:43:58.746916 IP 192.168.245.168.80 > 192.168.245.1.56066: Flags [P.], seq 3486567723:3486567734, ack 1240222501, win 510, options [nop,nop,TS val 3081458831 ecr 820082130], length 11: HTTP
    01:43:58.747644 IP 192.168.245.1.56066 > 192.168.245.168.80: Flags [.], ack 11, win 2057, options [nop,nop,TS val 820254637 ecr 3081458831], length 0
    01:44:02.638193 IP 192.168.245.1.56066 > 192.168.245.168.80: Flags [P.], seq 1:18, ack 11, win 2057, options [nop,nop,TS val 820258522 ecr 3081458831], length 17: HTTP
    01:44:02.638329 IP 192.168.245.168.80 > 192.168.245.1.56066: Flags [.], ack 18, win 510, options [nop,nop,TS val 3081462723 ecr 820258522], length 0
    

    通过 tcpdump 分别监听:docker0 容器内的 veth 虚拟网卡、br0 网桥、ens33 宿主机网卡。

    发现 docker0 容器的 veth 虚拟网卡和 br0 网桥,物理机向 WMware 虚拟机发出的数据包源 IP 地址是物理机 bridge102 网桥的 IP,目的地址是 docker0 容器 veth 虚拟网卡的 IP。

    ens33 宿主机网卡,物理机向 WMware 虚拟机发出的数据包源 IP 地址是物理机 bridge102 网桥的 IP,目的地址是 WMware 虚拟机 ens33 网卡的 IP。

    这正是 iptables DNAT 规则起作用了。

小结:

通过 iptables PREROUTING 链增加 DNAT 规则,在数据包进网卡前执行 DNAT 目的地址转化,将宿主机 IP 转换为容器 IP。打通了外部主机访问容器暴露服务的限制。

总结

通过学习了 Veth PairsBridge路由表iptables 规则等概念后,亲自动手模拟出了 Docker Bridge 网络模型,并且分别测试了容器间的网络互通、从宿主机访问容器内网络、从容器内访问外网等场景的网络互通。实际上 Docker Network 就是使用了上述技术,帮我们创建和维护网络。

参考资料:

极客时间 - Kubernetes 容器网络