macvlan(ipvlan 同理)容器默认没有 IPv6:只有 link-local 地址,没有默认路由。原因不是网络不通,而是 Docker 在网络的 EnableIPv6 为 false 时,会主动把容器接口的 disable_ipv6 置 1。
下面是家庭宽带(前缀由运营商动态下发)场景下的正确配置。
先确认是这个问题
$ docker exec <容器> ip -6 addr show eth0
inet6 fe80::ef5:b3ae:a64e:e782/64 scope link # 只有 link-local
$ docker exec <容器> cat /proc/sys/net/ipv6/conf/eth0/disable_ipv6
1 # 被管理性关闭
不要给 docker 网络声明 v6 子网
看着「正规」的写法是这样,但对家庭宽带是错的:
networks:
macvlan_net:
enable_ipv6: true
ipam:
config:
- subnet: 2408:xxxx:xxxx:xxx::/64 # ← 别这么写
这个 /64 是运营商通过 DHCPv6-PD 下发的,它会变。前缀一换,容器还抱着旧前缀里切出来的地址,而那个前缀已经不再路由到你家了——表现是 IPv6 静默失效,配置文件里却白纸黑字写着一个看起来没问题的地址。
只有前缀是静态的场景(自己申请的 PI 段、机房固定分配、或者打算用 ULA 再做 NPT)才适合写死。
正确做法是不声明任何 v6 子网,只把 disable_ipv6 关掉,让内核听路由器的 RA 自己做 SLAAC。前缀变了内核跟着变,配置一个字不用改。
正确的写法
services:
myapp:
networks:
macvlan_net:
ipv4_address: 10.0.0.6
driver_opts:
com.docker.network.endpoint.sysctls: "net.ipv6.conf.IFNAME.disable_ipv6=0,net.ipv6.conf.IFNAME.accept_ra=2"
sysctls:
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
四个容易踩的点:
接口级 sysctl 不能写在
sysctls:里。新版 Docker 会直接拒绝:Error response from daemon: interface specific sysctl setting "net.ipv6.conf.eth0.accept_ra" must be supplied using driver option 'com.docker.network.endpoint.sysctls'因为容器创建时接口还不一定存在,而且一个容器可以接多个网络,写死
eth0语义上是含糊的。这类参数必须挂到网络端点上。IFNAME是字面量占位符,不要替换成eth0。Docker 把接口塞进容器时会用真实网卡名替换它——这正是这套 API 存在的意义。多个参数用逗号分隔,写在同一个字符串里。
端点选项挂在
services.<名>.networks.<网络>.driver_opts下,不是顶层networks:里那个driver_opts。这两个是不同的东西:顶层那个是网络驱动的选项(比如parent),这里这个是端点的。
不带接口名的 net.ipv6.conf.all.*、net.ipv4.ip_forward 照旧写在 sysctls: 里,不受新规矩约束。
为什么 accept_ra 必须是 2
漏了这一步就前功尽弃,而且没有任何报错。
Linux 的逻辑是:一旦这个 netns 开启了 IPv6 转发,就认为自己是路由器,而路由器不该听别人的 RA——于是 accept_ra=1 被静默忽略。做旁路由/透明代理的容器几乎一定开着转发,两者一撞,结果就是 disable_ipv6 确实变成了 0、accept_ra 确实是 1,但地址就是配不上。
accept_ra=2 的含义正是「即使在转发状态下也接受 RA」。转发和 SLAAC 要同时要,就必须是 2。
如果容器不开转发,accept_ra=1 也能工作;但写 2 没有副作用,统一写 2 更省心。
验证
$ docker exec <容器> ip -6 addr show eth0 | grep global
inet6 2408:xxxx:xxxx:xxx:a4be:81ff:fead:d100/64 scope global dynamic
$ docker exec <容器> ip -6 route | grep default
default via fe80::xxxx:xxxx:xxxx:xxxx dev eth0 metric 1024 expires 0sec
$ docker exec <容器> ping -c4 <目标的 v6>
4 packets transmitted, 4 packets received, 0% packet loss
拿到全局地址、有默认路由、能 ping 通,就成了。
那个吓人的 expires 不用管
inet6 2408:xxxx:xxxx:xxx:a4be:81ff:fead:d100/64 scope global dynamic
valid_lft 3061sec preferred_lft 2469sec
第一次看会以为地址 50 分钟后就没了。不是。这是 RA 携带的生命周期,每收到一次 RA 就重置,路由器的广播间隔远小于这个值,所以计数器一直在被顶回去。容器和宿主机的数值一模一样,因为它俩听的是同一份 RA。
前缀变了会怎样
这是选 SLAAC 的回报。运营商换前缀时:拨号重连 → 路由器拿到新 PD → 广播新前缀、同时把旧前缀 lifetime 置 0 → 容器内核弃用旧地址、自动配上新地址。配置不用改,容器不用重启。代价只是切换瞬间已建立的连接会断一次重连。
写死子网的话,这一步就是手工改 compose、重建网络、重建容器,而且得先意识到问题出在这儿。
小结
- macvlan 容器没 IPv6,通常是 Docker 设的
disable_ipv6=1,不是网络不通 - 家宽前缀是动态的,不要把 v6 子网写进 docker 网络,让容器 SLAAC
- 接口级 sysctl 必须走
com.docker.network.endpoint.sysctls,用IFNAME占位 - 开了转发就必须
accept_ra=2,否则 SLAAC 静默失效 valid_lft的倒计时会被 RA 刷新,不用管
具体到 mihomo 旁路由怎么搭,在这篇:用 Docker + macvlan 跑 mihomo 旁路由。