macvlan + 固定 ULA + OpenWrt NAT66
环境:群晖 DSM 7.4(Container Manager 24.0.2-1706)|主路由:京东云 RE-CS-02(ImmortalWrt SNAPSHOT,qualcommax,fw4/nftables)|宽带:电信,动态 IPv6 前缀(240e:xxxx:xxxx:xxxx::/64)
目标:容器拥有固定不变的 IPv6 地址,可出公网 IPv6、可被外网 IPv6 连接(BT 场景),且电信前缀变化后无需任何维护。
〇、方案选型(为什么走这条路)
踩坑:Docker 原生 IPv6 NAT 方案不可行
网上通用做法是修改 dockerd.json 开 ipv6 + ip6tables + fixed-cidr-v6,但在这台群晖上启动失败:
# 配置文件位置(DSM 7.2+ 套件名 ContainerManager)
vi /var/packages/ContainerManager/etc/dockerd.json
# 加入以下字段后 synopkg restart ContainerManager 报错:
"ipv6": true,
"fixed-cidr-v6": "fd00:db8:1::/64",
"ip6tables": true,
"experimental": true错误日志(journalctl -b -u pkg-ContainerManager-dockerd.service 可查):
failed to start daemon: Error initializing network controller:
failed to create NAT chain DOCKER: iptables failed:
ip6tables --wait -t nat -N DOCKER: ip6tables v1.8.3 (legacy):
can't initialize ip6tables table `nat': Table does not exist (do you need to insmod?)根因:DSM 内核裁剪掉了 IPv6 NAT 模块(/lib/modules/ 下只有 ip6table_filter.ko、ip6table_mangle.ko,没有 ip6table_nat.ko、nf_nat_ipv6.ko)。DSM 的内核不动为妙,故放弃此路。
最终架构
Internet(电信动态 IPv6 前缀,随时会变)
│ WAN
┌──┴──────────────────┐
│ OpenWrt RE-CS-02 │ ① LAN 别名 fd00:db8:1::1/128(容器网关,固定)
│ │ ② 静态路由 fd00:db8:1::/64 → 群晖 fe80(下一跳不变)
│ │ ③ nat66-out:fd00:db8:1::/64 出 WAN 走 MASQUERADE
│ │ ④ DNAT:5xxxx → fd00:db8:1::20(BT 入站)
└──┬──────────────────┘
│ LAN(192.168.68.0/24)
┌──┴──────────────────┐
│ 群晖 DSM 7.4 │ ⑤ IPv6 转发=1 + 宿主机 macvlan 垫片 mv-shim(::254)
│ eth0 │ 开机计划任务自动恢复(见 三-3)
└──┬──────────────────┘
│ macvlan lan_net(fd00:db8:1::/64)
qB / TR / iyuup → 192.168.68.20 + fd00:db8:1::20核心思想:动态的电信前缀全部由路由器消化(NAT66 + DNAT),容器侧只用固定的 ULA 地址(fd 开头私有段),从此前缀随便变,配置永不动。
一、OpenWrt 侧(SSH 登录路由器)
1. LAN 上加一个固定 IPv6 别名(作为容器网关)
uci add_list network.lan.ip6addr='fd00:db8:1::1/128'
uci commit network
/etc/init.d/network restart2. 静态路由:告诉 OpenWrt,fd00:db8:1::/64 的包都交给群晖
先拿到群晖的链路本地地址(在群晖上执行 ip -6 addr show eth0 | grep fe80,形如 fe80::xxxx:xxxx:xxxx:xxxx/64),然后在 OpenWrt 上:
uci add network route6
uci set network.@route6[-1].interface='lan'
uci set network.@route6[-1].target='fd00:db8:1::/64'
uci set network.@route6[-1].gateway='<群晖的fe80地址>'
uci commit network
/etc/init.d/network restart用 fe80 地址做下一跳是刻意的:它只跟群晖的 MAC 绑定,不受电信前缀变化影响。
或者在路由 Web 页面配置(网络 → 路由 → 静态 IPv6 路由 → 添加):
| 字段 | 填写内容 |
|---|---|
| 接口 | lan |
| 路由类型 | unicast(单播) ← 关键!选成 local 是"本机地址"语义,必错 |
| 目标 | fd00:db8:1::/64 |
| 网关 | 群晖的 fe80 链路本地地址,填纯地址不带 /64,如 fe80::2e0:22ff:fe29:919c |
| 距离值/表 | 留空 |
查找群晖的 IPv6 链路本地地址:
# 群晖 SSH 上执行
ip -6 addr show eth0 | grep fe80
# 输出示例:inet6 fe80::2e0:22ff:fe29:919c/64 scope link配置结果:

验证路由生效(OpenWrt SSH):
ip -6 route show | grep fd00
# 期望:fd00:db8:1::/64 via fe80::2e0:22ff:fe29:919c dev br-lan proto static3. NAT66 出站(fw4 规则)
uci add firewall nat
uci set firewall.@nat[-1].name='nat66-out'
uci set firewall.@nat[-1].family='ipv6'
uci set firewall.@nat[-1].src='lan'
uci set firewall.@nat[-1].src_ip='fd00:db8:1::/64'
uci set firewall.@nat[-1].dest='wan'
uci set firewall.@nat[-1].target='MASQUERADE'
uci commit firewall
/etc/init.d/firewall restart注意:如果你的 ImmortalWrt wan 区已经有masq6='1'(uci show firewall | grep -i masq查看),这步可以跳过。配置后验证:uci show firewall.@nat[0]应能看到name='nat66-out'。
4. BT 入站放行(IPv6 DNAT)
# qBittorrent 55678
uci add firewall redirect
uci set firewall.@redirect[-1].name='qb-bt-v6'
uci set firewall.@redirect[-1].family='ipv6'
uci set firewall.@redirect[-1].proto='tcp udp'
uci set firewall.@redirect[-1].src='wan'
uci set firewall.@redirect[-1].src_dport='55678'
uci set firewall.@redirect[-1].dest_ip='fd00:db8:1::20'
uci set firewall.@redirect[-1].target='DNAT'
# Transmission 51413
uci add firewall redirect
uci set firewall.@redirect[-1].name='tr-bt-v6'
uci set firewall.@redirect[-1].family='ipv6'
uci set firewall.@redirect[-1].proto='tcp udp'
uci set firewall.@redirect[-1].src='wan'
uci set firewall.@redirect[-1].src_dport='51413'
uci set firewall.@redirect[-1].dest_ip='fd00:db8:1::20'
uci set firewall.@redirect[-1].target='DNAT'
uci commit firewall
/etc/init.d/firewall restart二、群晖侧
1. 创建 macvlan 双栈网络(固定 ULA 段)
docker network rm lan_net # 若之前建过先删
docker network create -d macvlan \
--subnet=192.168.68.0/24 --gateway=192.168.68.1 \
--ipv6 --subnet=fd00:db8:1::/64 --gateway=fd00:db8:1::1 \
-o parent=eth0 lan_net网关 fd00:db8:1::1 就是我们在 OpenWrt LAN 上加的别名地址。验证网络与容器 IP:
docker network inspect lan_net --format '{{range .Containers}}{{.Name}} {{.IPv4Address}} {{.IPv6Address}}{{"\n"}}{{end}}'
# 期望:qbittorrent 192.168.68.20/24 fd00:db8:1::20/642. compose 改造(三容器共用一个 IP)
关键写法:只有 qbittorrent 挂 lan_net 持有 IP;transmission 和 iyuuplus 用 network_mode: "service:qbittorrent" 共享 qB 的网络栈(IPv4、IPv6 全部公用,靠端口区分)。macvlan 下 ports: 端口映射无效,全部删除。
services:
qbittorrent:
image: linuxserver/qbittorrent:4.5.2
container_name: qbittorrent
environment:
- PUID=0
- PGID=0
- TZ=Asia/Shanghai
- UMASK_SET=022
- WEBUI_PORT=8080
volumes:
- ./qbittorrent/config:/config
- /volume5/temp:/temp
- /volume3/media:/media
networks:
lan_net:
ipv4_address: 192.168.68.20
ipv6_address: fd00:db8:1::20
restart: always
transmission:
image: chisbread/transmission:latest
container_name: transmission
depends_on:
- qbittorrent
environment:
- PUID=0
- USER=<你的用户名>
- PASS=<你的密码>
- RPCPORT=9091
volumes:
- ./transmission/config:/config
- ./transmission/watch:/watch
- ./transmission/transmission-web-control:/transmission-web-control
- /volume5/temp:/temp
- /volume3/media:/media
network_mode: "service:qbittorrent"
restart: always
iyuuplus:
image: iyuucn/iyuuplus-dev:latest
container_name: iyuuplus-dev
depends_on:
- qbittorrent
stdin_open: true
tty: true
volumes:
- ./iyuuplus/iyuu:/iyuu
- ./iyuuplus/data:/data
- ./iyuuplus/log:/var/log/nginx
- ./qbittorrent/config/qBittorrent/BT_backup:/BT_backup
- ./transmission/config/torrents:/torrents
network_mode: "service:qbittorrent"
restart: always
networks:
lan_net:
external: true改完后:
docker-compose down && docker-compose up -diyuuplus 里下载器地址改成 127.0.0.1:8080(qB)和 127.0.0.1:9091(TR)——三者共享网络栈,走 localhost 即可。

3. IPv6 转发 + 宿主机垫片(⚠️ 最容易漏的一步)
配完上面后,容器能出公网(macvlan 二层直通 OpenWrt),但内网设备 ping 不通容器——检查会发现:
sysctl net.ipv6.conf.all.forwarding
# 输出 net.ipv6.conf.all.forwarding = 0 ← 转发没开
ip -6 route show | grep fd00
# 无输出 ← 宿主机不知道 fd00:db8:1::/64 该往哪转(macvlan 隔离,天生没有)修复(两步缺一不可):
# 1. 开启 IPv6 转发
sysctl -w net.ipv6.conf.all.forwarding=1
# 2. 宿主机创建 macvlan 垫片接口,作为转发出口
ip link add mv-shim link eth0 type macvlan mode bridge
ip addr add fd00:db8:1::254/64 dev mv-shim # ::254 避开 ::1(路由器) 和 ::20(容器)
ip link set mv-shim up执行后验证:
ping -c3 fd00:db8:1::20 # 群晖宿主机 ping 容器,此前 macvlan 隔离时是不通的4. 开机持久化(计划任务,防重启丢失)
sysctl 和 ip link 都是临时生效,群晖重启会丢。控制面板 → 任务计划 → 新增 → 触发的任务 → 用户定义的脚本:
- 任务名称:
docker-macvlan-ipv6 - 用户账号:root(必须)
- 事件:开机
- 运行命令(幂等,重复执行不报错):
#!/bin/sh
# Docker macvlan IPv6 转发与垫片(fd00:db8:1::/64 方案)
sysctl -w net.ipv6.conf.all.forwarding=1
if ! ip link show mv-shim >/dev/null 2>&1; then
ip link add mv-shim link eth0 type macvlan mode bridge
fi
ip addr replace fd00:db8:1::254/64 dev mv-shim
ip link set mv-shim up保存后右键任务 → 运行,验证脚本无误。

三、验证清单(自上而下逐级测)
| 层级 | 命令/方法 | 期望 |
|---|---|---|
| 容器 → OpenWrt 网关 | docker exec qbittorrent ping -c2 fd00:db8:1::1 | 通 |
| 容器 → 公网 IPv6 | docker exec qbittorrent ping -6 -c4 2400:3200::1 | 通(NAT66 生效) |
| 路由器 → 容器 | OpenWrt 上 ping -6 -c3 fd00:db8:1::20 | 通(转发+垫片生效) |
| 内网电脑 → 容器 | Windows ping fd00:db8:1::20 | 通 |
| WebUI | 浏览器 http://192.168.68.20:8080 / :9091 / :8780 | 正常打开 |
| 外网 BT 端口 | 手机关 WiFi 走流量,ipv6.ping.pe 测 55678/51413 | 端口开放 |
四、注意事项与常见问题
- IP 冲突:
192.168.68.20~22和fd00:db8:1::20必须避开路由器 DHCP 分配池,最稳做法是在路由器上做静态绑定(容器 MAC 用docker inspect qbittorrent | grep MacAddress查)。 - 电信前缀动态变化:GUA 前缀(240e:…)重拨会变,但本方案容器全走固定 ULA,无感;变化的只有外网访问时要用的"当前 WAN 地址"(BT tracker 上报的地址是被动告知的,无需配置)。
- macvlan 隔离:容器与群晖宿主机二层互相隔离,宿主机访问容器必须走 mv-shim 垫片路由,这是 macvlan 的内核机制,不是故障。
ports:映射无效:macvlan 下端口映射不工作,直接访问容器 IP:端口;群晖防火墙若开启,注意放行相应端口。- Docker 套件重启/群晖升级后:计划任务会自动恢复转发与垫片;若手动
docker network rm过lan_net,需重新执行 二-1 的 create 命令再 up 容器。 排查工具箱:
- dockerd 起不来:
journalctl -b -u pkg-ContainerManager-dockerd.service | tail -50 - 内核模块检查:
ls /lib/modules/ | grep -i ip6 - OpenWrt 路由/防火墙:
ip -6 route show | grep fd00、uci show firewall | grep -iE "nat66|bt-v6"
- dockerd 起不来:
