在 Docker 或 Kubernetes 网络场景中,macvlan 是一种让容器直接拥有局域网二层可达 IP 的驱动。它把宿主的物理网卡当作父设备,虚拟出多个带独立 MAC 地址的子接口,每个子接口可以配一个与宿主机同网段的地址。这样一来,容器就像一台接在交换机上的真实机器,局域网里的其他设备能直接访问它,不需要做端口映射。但很多人在配完之后遇到一个怪现象:同网段里的其他电脑能 ping 通容器,宿主机自己却 ping 不通,或者宿主机发给容器的包根本出不去。

macvlan 同网段通信的底层原理
要理解宿主机为什么默认不能和 macvlan 容器互通,得先看 macvlan 的工作方式。macvlan 是 Linux 内核网桥之外的另一种虚拟网卡技术,它在数据链路层把父设备收到的帧按目标 MAC 地址做分流。当物理网卡 eth0 收到一个目标 MAC 是容器虚拟 MAC 的包,内核会直接把它交给对应的 macvlan 子接口,而不会经过宿主的协议栈。反过来,宿主机如果想主动发一个包给容器 IP,它默认会从 eth0 的 IP 走,而 eth0 的 MAC 和容器 MAC 不同,发出的 ARP 请求或单播帧不会被 macvlan 子接口接收,于是通信失败。
这种隔离是 macvlan 的设计特性,不是 bug。它的好处是性能高、延迟低,容器网络和外界几乎无差别;代价就是宿主和容器在二层被天然隔开。如果业务要求宿主机必须能访问同网段里的容器,比如监控代理跑在宿主上要探活容器服务,就必须在宿主机侧也创建一个 macvlan 接口并配上同网段 IP,或者使用 ipvlan 这类允许宿主互通的变体。搞清楚这个边界,才能选对配置方案。
从 ARP 和行为看,容器在 macvlan 下会用自己的 MAC 回应 ARP,局域网交换机学到的是虚拟 MAC 与交换机端口的映射。宿主机由于和容器共享父网卡,它的 ARP 表若没有专门条目,就会认为容器 IP 应该在 eth0 本身,而不是某个子设备。因此即便手动加静态路由,也得配合一个宿主侧的 macvlan 接口,让回包能从正确的虚拟设备进来,否则会出现请求能出、回包丢失的单向通现象。
宿主机侧创建 macvlan 接口实现互通的配置
最常见的解决办法是在宿主机上再建一个 macvlan 接口,模式和容器用的保持一致,比如都是 bridge 模式,然后给它分配同网段的一个空闲 IP。这样宿主机就多了一张虚拟网卡,它和容器子接口处于同一个 macvlan 域,二层互通没有问题。下面是在 Ubuntu 宿主机上的操作步骤,假设父设备是 eth0,网段是 192.168.1.0/24,网关是 192.168.1.1。
先加载模块并创建接口,注意 macvlan 的 bridge 模式允许同父设备下的多个子接口互通,但不与父设备互通,所以宿主的 eth0 仍然不能和容器通,必须靠新接口。创建后配 IP 并 up,然后加一条到容器子网的路由走新接口。示例脚本如下:
# 创建宿主侧 macvlan 接口 sudo ip link add mvl_host link eth0 type macvlan mode bridge sudo ip addr add 192.168.1.250/24 dev mvl_host sudo ip link set mvl_host up # 让宿主访问容器网段时走 mvl_host sudo ip route add 192.168.1.0/24 dev mvl_host # 如果默认网关在 eth0,可保留 eth0 的默认路由不变 # 验证:从宿主 ping 容器 IP ping 192.168.1.100
上面的做法里,mvl_host 占用了网段里一个真实 IP,需要和容器 IP 规划好避免冲突。如果宿主本身不需要对外提供服务,只是管理用途,也可以只给 mvl_host 配一个 /32 的地址并做精确路由。另一个容易踩的坑是:有些云厂商的网卡驱动不支持混杂模式或 macvlan 卸载,会导致子接口收不到包,这时用 ip link show 看状态是 up 但抓包为空,需要换用 ipvlan 或检查驱动。
对于 Docker 环境,可以在 daemon.json 里定义 macvlan 网络,并明确指定网关和子网,然后启动容器时连到这个网络。宿主侧接口需手动建,因为 Docker 不会替宿主建互通口。示例的 Docker 网络创建命令和容器启动如下:
# 创建 docker macvlan 网络 docker network create -d macvlan --subnet=192.168.1.0/24 --gateway=192.168.1.1 -o parent=eth0 my_macvlan # 启动容器使用同网段 IP docker run -itd --name c1 --network my_macvlan --ip=192.168.1.100 alpine sh
排错思路与 ipvlan 替代方案
当同网段通信不通时,第一步应该用 tcpdump 在父设备 eth0 和宿主侧 mvl_host 上同时抓包,看 ARP 是否发出、容器是否回应。如果 eth0 上能看到宿主发往容器 IP 的 ARP 请求,但 mvl_host 上没有,说明路由没走对;如果 mvl_host 有请求但容器没回,可能是容器侧 macvlan 模式和宿主不一致,或者交换机做了 MAC 安全限制。通过分层抓包能快速定位是二层隔离还是三层配置问题。
另一个经常被忽略的点是反向过滤和 rp_filter。Linux 默认会做反向路径校验,如果包从 mvl_host 进来但源 IP 的出口不是它,可能被丢。可以临时把相关接口的 rp_filter 设为 0 做验证:sysctl -w net.ipv4.conf.mvl_host.rp_filter=0。此外,若宿主有多个同网段接口,要小心路由优先级,避免默认流量被引到 mvl_host 导致宿主上不了网。
如果觉得给宿主单独占 IP 太浪费,或者网卡驱动不支持 macvlan,可以改用 ipvlan 的 l2 模式。ipvlan 和 macvlan 类似,但多个子接口共享父设备 MAC,并且内核提供了 hairpin 类的宿主互通能力,配置更简单。下面是用 ipvlan 创建宿主互通网络的示例,注意 ipvlan 要求内核版本较新且父设备支持:
# 创建 ipvlan 接口(l2 模式,允许宿主互通需额外设置) sudo ip link add ipv_host link eth0 type ipvlan mode l2 sudo ip addr add 192.168.1.251/24 dev ipv_host sudo ip link set ipv_host up # 在 Docker 中使用 ipvlan docker network create -d ipvlan --subnet=192.168.1.0/24 --gateway=192.168.1.1 -o parent=eth0 -o ipvlan_mode=l2 my_ipvlan
ipvlan 的缺点是某些旧网络环境对同 MAC 多 IP 的识别不如 macvlan 清晰,但在纯二层互通和宿主访问场景里更省心。无论选哪种,核心原则都是:宿主若想和同网段容器通信,必须自身也处于那个虚拟二层域里,而不是仅靠父网卡。理清这一点,规划 IP 和路由就不会再踩坑。