家里用一台 N95 迷你主机当 homelab host,Docker 里跑 mihomo,用 macvlan 给它分一个独立的局域网 IP(10.0.0.6),让它当局域网的旁路由——设备把网关指过去,流量就经过 mihomo 分流代理,不用动主路由的整体路由结构。

容器起来只是第一步,实际会撞上两件事:宿主机和自己衍生出的 macvlan 容器默认互相不通,以及局域网设备到底怎么切到这个网关。这篇把两个都记下来。

为什么用 macvlan

docker-compose 配置:

services:
  mihomo:
    image: metacubex/mihomo:latest
    container_name: mihomo
    cap_add:
      - NET_ADMIN
    devices:
      - /dev/net/tun:/dev/net/tun
    volumes:
      - ./data:/root/.config/mihomo
    networks:
      macvlan_net:
        ipv4_address: 10.0.0.6
        # 需要 IPv6 就把下面带 # 的行全解开(含 sysctls 里那两行)
        # driver_opts:
        #   com.docker.network.endpoint.sysctls: "net.ipv6.conf.IFNAME.disable_ipv6=0,net.ipv6.conf.IFNAME.accept_ra=2"
    sysctls:
      net.ipv4.ip_forward: 1
      # net.ipv6.conf.all.disable_ipv6: 0
      # net.ipv6.conf.all.forwarding: 1

networks:
  macvlan_net:
    driver: macvlan
    driver_opts:
      parent: enp1s0
    ipam:
      config:
        - subnet: 10.0.0.0/24
          gateway: 10.0.0.1

用 macvlan 而不是普通 bridge,是因为要给容器一个和局域网其他设备平级的真实 IP,这样它才能被其他设备当网关直接访问、转发流量。bridge 模式下容器藏在宿主机的 NAT 后面,别的设备根本没法把它当网关。

注释掉的几行是 IPv6:只要 v4 就保持注释,容器起来是纯 v4 的;解开注释,容器会跟着路由器的 RA 自动 SLAAC 拿到全局地址和默认路由,运营商换前缀也不用改配置。这里为什么用 IFNAME 占位、为什么 accept_ra 必须是 2(写 1 会被静默忽略),展开在这篇:让 macvlan 容器拿到 IPv6。

第一步:让宿主机和 macvlan 容器互通

宿主机自己要访问这个容器(管理、健康检查),或者容器要反过来访问宿主机上跑的其他服务,都需要额外配置——这是 macvlan 的一个已知限制:宿主机和它自己衍生出来的 macvlan 子接口容器之间默认无法直接通信,而且是双向的。

原因在内核层面:宿主机物理网卡发出的包和 macvlan 子接口发出的包共享同一个底层设备,内核不会在这两者之间做转发/环回。容器和局域网内其他真实主机通信没问题,因为流量走的是物理交换机再绕回来;但容器和宿主机本身之间没有这条路径。

解决办法是在宿主机上再建一个 macvlan 子接口(俗称 shim),让宿主机通过这个影子接口访问 macvlan 网段:

ip link add macvlan-shim link enp1s0 type macvlan mode bridge
ip addr add 10.0.0.9/32 dev macvlan-shim   # 网段内挑一个空闲 IP
ip link set macvlan-shim up
ip route add 10.0.0.6/32 dev macvlan-shim  # 精确路由到 mihomo 容器

宿主机访问 10.0.0.6 的流量就会走 macvlan-shim 这条路由。反过来,如果容器需要访问宿主机上某个服务(比如我这边容器要连宿主机的一个 SOCKS5 代理,监听在 1080 端口),只要那个服务是用 docker run -p 1080:1080 发布的,docker-proxy 默认监听 0.0.0.0,也会监听在 shim 的 10.0.0.9 上,容器里直接连 10.0.0.9:1080 即可。

持久化,写个 systemd oneshot service 开机自动跑一遍:

# /etc/systemd/system/macvlan-shim.service
[Unit]
Description=macvlan shim for host-container communication
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/sbin/ip link add macvlan-shim link enp1s0 type macvlan mode bridge
ExecStart=/sbin/ip addr add 10.0.0.9/32 dev macvlan-shim
ExecStart=/sbin/ip link set macvlan-shim up
ExecStart=/sbin/ip route add 10.0.0.6/32 dev macvlan-shim
ExecStop=/sbin/ip link del macvlan-shim

[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable --now macvlan-shim.service

第二步:让局域网设备走这个旁路由

容器起来之后,局域网设备还得知道要把出口指向 10.0.0.6。这里有两种模式:

模式改哪里适合
改网关和 DNS客户端逐台手动,或路由器按 MAC 批量下发设备固定、想精确控制哪几台走代理
开一个专用 WiFi只改路由器(新网段 + 新 SSID + 策略路由)想随时切换、家人设备不方便动手脚

模式一:把网关和 DNS 指向 10.0.0.6

网关和 DNS 要一起指过去,只改网关是不够的。mihomo 跑的是 fake-ip:客户端的 DNS 查询交给它,它才能把域名和后续连接对上号,域名规则(DOMAIN-SUFFIX、GEOSITE 之类)才有东西可匹配。如果 DNS 还指着路由器,mihomo 只能看到一个裸 IP,绝大部分规则匹配不上,最后落到兜底策略——被墙的站照样打不开。

另外建议关掉这些设备的 IPv6。家里宽带有公网 IPv6 的话,设备一旦拿到 v6 地址就会优先走 v6 直连出去,代理形同虚设。

逐台手动改

每台设备的网络设置里,把默认网关从路由器 IP(比如 10.0.0.1)改成 10.0.0.6,DNS 一并填 10.0.0.6。适合只想测试个别设备、不想动路由器配置的场景。缺点是要改成静态 IP,设备一多就没法管了。

OpenWrt 批量打 tag

设备一多,逐台改就太麻烦了。路由器跑 OpenWrt 的话,可以用 dnsmasq 的 tag 机制,按 MAC 地址分组,给不同分组下发不同的网关和 DNS,不用碰任何客户端设置。

思路是:先给需要走旁路由的设备打上一个 tag(按 MAC 匹配),再用 dhcp-option-force 针对这个 tag 强制下发网关(option 3)和 DNS(option 6)。

在 /etc/config/dhcp 里加:

config host
    option name 'my-device'
    option mac 'AA:BB:CC:DD:EE:FF'
    option tag 'bypass'
    option ip '10.0.0.100'

config tag 'bypass'
    list dhcp_option '3,10.0.0.6'
    list dhcp_option '6,10.0.0.6'

或者直接改 dnsmasq.conf 风格:

dhcp-host=AA:BB:CC:DD:EE:FF,set:bypass,10.0.0.100
dhcp-option-force=tag:bypass,3,10.0.0.6
dhcp-option-force=tag:bypass,6,10.0.0.6

set:bypass 给匹配到的设备打上 bypass 标签,dhcp-option-force 只对带这个标签的设备生效,强制下发网关和 DNS 为 10.0.0.6。没打标签的设备继续走路由器默认网关,两者互不影响。想让某台设备走旁路由,只要把它的 MAC 加进 bypass 分组即可。

改完重启 dnsmasq,让设备重新获取一次 DHCP(或者等租期到期自动刷新)就生效了:

/etc/init.d/dnsmasq restart

这种方式下,设备和 mihomo 在同一个网段(都是 10 段),DNS 查询是二层直达的,不经过路由器转发——这一点在下一节的方案里会变成一个大坑。

模式二:开一个专用 WiFi,连上就走代理

按 MAC 打 tag 仍然是「静态分组」:一台设备今天想走代理、明天想直连,还是要回后台改配置、再让它重新拿一次租约。更顺手的做法是在路由器上单独开一个网段,绑一个独立的 SSID,整段流量用策略路由指向 mihomo——连上这个 WiFi 就全走代理,切回主网 WiFi 就直连,客户端零配置。

因为 mihomo 在 10 段而新 WiFi 在另一个网段,不能简单地把网关填成 10.0.0.6(跨网段 ARP 不到),要在 OpenWrt 上做源地址策略路由;中间还有 ImmortalWRT 的 DNS 劫持和 mihomo 上游 DNS 竞速两个挺隐蔽的坑。这部分内容不少,单独写了一篇:

→ OpenWrt 独立 WiFi 网段指向 mihomo 旁路由:连上即自动代理

小结

macvlan + shim 接口解决宿主机和容器之间的通信问题,让 mihomo 容器像一台真实的旁路由设备一样存在于局域网里;接入侧则按需要选:想精确控制到具体设备就用 dnsmasq 的 tag 按 MAC 下发网关和 DNS,想随手切换就上独立 WiFi 网段。两种方式可以并存,互不影响。