Docker 使用 macvlan 网络容器与宿主机的通信过程

一. 摘要

因为 macvlan 的限制,使用 manvlan 虚拟网口的容器不能与宿主机直接互通(准确的说是不能与父网卡互通)。
为了让容器与宿主机互通,可以通过在宿主机上添加一个额外的 macvlan 虚拟网口来实现。
这里涉及到的一些知识:

  1. 同子网下多网卡 arp 应答机制(可以参考我的这篇文章来了解 ARP Flux
  2. 同主机网卡之间的数据报转发(控制参数 ip_forward
  3. 网络协议是分层的。源地址 ip 和源 mac 可以不在同一张网卡上;同理目的地址 ip 和目的 mac 也可以不在同一张网卡上
  4. 路由控制(tips: 没有配置 ip 的网卡依然可以转发数据包)

二. 快速搭建测试环境

  1. 基础信息:

    网口 ip MAC
    ens33(宿主机) 192.168.136.130 00:0c:29:80:61:8b
    eth0(容器) 192.168.136.240 02:42:c0:a8:88:f0
    macvlan0-host(宿主机) 9e:63:b4:5c:c8:93

    macvlan0-host 网口是为了打通宿主机与容器网络额外添加的

  2. 开启网卡混杂模式

    1
    ip link set ens33 promisc on
  3. 使用 docker network create 创建 docker macvlan 网络

    1
    docker network create -d macvlan --subnet=192.168.136.0/24 --gateway=192.168.136.2 --ip-range=192.168.136.240/28 -o parent=ens33 macvlan0
    1
    2
    3
    4
    5
    6
    7
    [root@centos7-01 ~]# docker network ls
    NETWORK ID NAME DRIVER SCOPE
    93e753322579 awxcompose_default bridge local
    1597fe9b77cc bridge bridge local
    18efa0daec31 host host local
    da129fb7ee57 macvlan0 macvlan local
    679be2fe09c2 none null local
  4. 启动使用 macvlan 网络的容器,并指定容器 ip 为 192.168.136.240

    1
    docker run -d -it --network=macvlan0 --ip=192.168.136.240 centos:8 /bin/bash

三. 第一阶段测试

  1. 在宿主机 ping 容器 ip 192.168.136.240;发现 ping 不通
    1
    2
    3
    [root@centos7-01 ~]# ping 192.168.136.240
    PING 192.168.136.240 (192.168.136.240) 56(84) bytes of data.
    From 192.168.136.130 icmp_seq=1 Destination Host Unreachable
  2. 在容器里 ping 宿主机 ip 192.168.136.130;同样也 ping 不通
    1
    2
    3
    4
    5
    [root@da15dc23e3d2 /]# ping 192.168.136.130
    PING 192.168.136.130 (192.168.136.130) 56(84) bytes of data.
    ^C
    --- 192.168.136.130 ping statistics ---
    2 packets transmitted, 0 received, 100% packet loss, time 3ms
  3. 在容器里 arping 宿主机 ip 192.168.136.130;也没有响应
    1
    2
    3
    4
    [root@da15dc23e3d2 /]# arping 192.168.136.130
    ARPING 192.168.136.130 from 192.168.136.240 eth0
    ^CSent 3 probes (3 broadcast(s))
    Received 0 response(s)

四. 打通容器与宿主机之间的网络

  1. 通过 macvlan 添加一块虚拟网口macvlan0-host来实现容器与宿主机互通
    1
    2
    ip link add link ens33 macvlan0-host type macvlan mode bridge
    ip link set dev macvlan0-host up
  2. 检查macvlan0-host 网口,确保是 UP 状态
    1
    2
    3
    [root@centos7-01 ~]# ip link show | grep macvlan0-host
    7: macvlan0-host@ens33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
    link/ether 9e:63:b4:5c:c8:93 brd ff:ff:ff:ff:ff:ff
  3. 添加宿主机到容器的专有路由(在宿主机上执行)
    1
    ip route add 192.168.136.140 dev macvlan0-host
    tips: 容器与宿主机在同一网段的情况下,macvlan0-host 不需要配置 ip

五. 再次测试验证

  1. 从宿主机 ping 容器 192.168.136.240 并抓包观察
    ①. 先分析宿主机发出去的包,可以看到源 ip 是 192.168.136.130(ens33 网口上的),源 mac 是 9e:63:b4:5c:c8:93 (macvlan0-host 网口上的)。这是因为我们上面加的那条路由指定了发给 192.168.136.140 的数据包要从 macvlan0-host 出;同时网络协议是分层的,源地址 ip 和源 mac 可以不在同一张网卡上。
    ②. 再分析宿主机收到的包,可以看到目的 mac 地址是 macvlan0-host 网口上的,目的 ip 是 192.168.136.130。然后数据包再由 macvlan0-host 转交给 ens33

    1
    2
    3
    [root@centos7-01 ~]# ping 192.168.136.240
    PING 192.168.136.240 (192.168.136.240) 56(84) bytes of data.
    64 bytes from 192.168.136.240: icmp_seq=1 ttl=64 time=0.160 ms
    1
    2
    3
    4
    5
    6
    [root@centos7-01 ~]# tcpdump -nn -e -i ens33 icmp -vvv
    tcpdump: listening on ens33, link-type EN10MB (Ethernet), capture size 262144 bytes
    21:03:47.362614 9e:63:b4:5c:c8:93 > 02:42:c0:a8:88:f0, ethertype IPv4 (0x0800), length 98: (tos 0x0, ttl 64, id 7281, offset 0, flags [DF], proto ICMP (1), length 84)
    192.168.136.130 > 192.168.136.240: ICMP echo request, id 2135, seq 1, length 64
    21:03:47.362637 02:42:c0:a8:88:f0 > 9e:63:b4:5c:c8:93, ethertype IPv4 (0x0800), length 98: (tos 0x0, ttl 64, id 17007, offset 0, flags [none], proto ICMP (1), length 84)
    192.168.136.240 > 192.168.136.130: ICMP echo reply, id 2135, seq 1, length 64
    1
    2
    3
    4
    5
    6
    [root@centos7-01 ~]# tcpdump -nn -e -i macvlan0-host icmp -vvv
    tcpdump: listening on macvlan0-host, link-type EN10MB (Ethernet), capture size 262144 bytes
    21:03:47.362590 9e:63:b4:5c:c8:93 > 02:42:c0:a8:88:f0, ethertype IPv4 (0x0800), length 98: (tos 0x0, ttl 64, id 7281, offset 0, flags [DF], proto ICMP (1), length 84)
    192.168.136.130 > 192.168.136.240: ICMP echo request, id 2135, seq 1, length 64
    21:03:47.362637 02:42:c0:a8:88:f0 > 9e:63:b4:5c:c8:93, ethertype IPv4 (0x0800), length 98: (tos 0x0, ttl 64, id 17007, offset 0, flags [none], proto ICMP (1), length 84)
    192.168.136.240 > 192.168.136.130: ICMP echo reply, id 2135, seq 1, length 64
  2. 从容器抓包也可以观察到回包是先发给了 macvlan0-host 网口

    1
    2
    3
    4
    5
    6
    7
    [root@fd6aa698a558 /]# tcpdump -nn -e -i eth0 icmp -vvv
    dropped privs to tcpdump
    tcpdump: listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
    13:03:47.362614 9e:63:b4:5c:c8:93 > 02:42:c0:a8:88:f0, ethertype IPv4 (0x0800), length 98: (tos 0x0, ttl 64, id 7281, offset 0, flags [DF], proto ICMP (1), length 84)
    192.168.136.130 > 192.168.136.240: ICMP echo request, id 2135, seq 1, length 64
    13:03:47.362636 02:42:c0:a8:88:f0 > 9e:63:b4:5c:c8:93, ethertype IPv4 (0x0800), length 98: (tos 0x0, ttl 64, id 17007, offset 0, flags [none], proto ICMP (1), length 84)
    192.168.136.240 > 192.168.136.130: ICMP echo reply, id 2135, seq 1, length 64
  3. 从容器 arping 宿主机 ens33 网口 ip 192.168.136.130
    可以观察到应答的 mac 地址是 macvlan0-host 网口上的, 原因在这里( ARP Flux

    1
    2
    3
    [root@fd6aa698a558 /]# arping 192.168.136.130
    ARPING 192.168.136.130 from 192.168.136.240 eth0
    Unicast reply from 192.168.136.130 [9E:63:B4:5C:C8:93] 0.600ms

六. 总结

mavlan 虚拟出来的网口不能和父网口直通,但是可以和同一子网的其他网口互通(包括其他虚拟出来的网口)。
通过在宿主机上用 macvlan 添加一个虚拟网口 macvlan0-host ,并在宿主机上配置路由『到容器的数据包由 macvlan0-host 发出』,就架起了宿主机与容器的通信桥梁。
宿主机发往容器的数据包,由 macvlan0-host 发给容器;容器发往宿主机的数据包先发给 macvlan0-host,再由 macvlan0-host 转交给 ens33

tips: 如果服务器上有额外的和容器同网段的网口,可以直接添加路由来实现容器与宿主机互通。

参考文档:

[Bridge] [PATCH 0/3] macvlan: add vepa and bridge mode
Linux 虚拟网卡技术:Macvlan
Docker 网络模型之 macvlan 详解,图解,实验完整