IPv6

家庭网络改造(二)——旁路由openWRT | polar-bear~Blog

距前文已经过去了一坤年(两年半),运营商的公网IPv4不断收紧,超管密码动态刷新,IPv6已经全面普及大势所趋的情况下,这一天终究是到来了吗。

美味新玩具

全屋FTTR套餐,运营商新套餐赠送了中兴ZXHN G7611V2和从机G1612。

装维师傅会按照千兆有线,配置主从机mesh组网。因为电脑的房间里没有预留墙内网线接口,于是网上购入成品隐形光纤,连接主从机PON接入,即可享受极简的走线,1250Mbps的上行,2500Mbps的下行,<1ms的延迟,无感的FTTR Roaming切换。。。(其实这些体验和1000M有线没有区别,正常基本碰不到瓶颈隐形光纤更好看而已,可以贴着墙缝和门缝)

网络拓扑

设备或接口 IPv4 地址 用途
光猫 / 主路由 192.168.1.1 普通设备的默认网关
OneCloud 的 eth0 192.168.1.45 宿主机、应用和公网服务入口
OpenWrt 容器 192.168.1.30 手动旁路由及 LuCI
宿主机 macvlan0 192.168.1.46 宿主机访问 macvlan 容器
1
2
3
4
5
6
7
8
9
10
11
光纤入户
│
FTTR 主机 / 主路由 192.168.1.1
├── FTTR 从机/其他设备
└── OneCloud eth0 192.168.1.45
├── 1Panel
├── Home Assistant
├── OpenList
├── EasyTier、Tailscale、Cloudflare Tunnel
├── DDNS、EdgeTier 状态上报
└── Docker macvlan → OpenWrt 192.168.1.30

消失的公网IPv4

路由器获取的ip地址与公网ip不同,并且 属于专为运营商级 NAT 部署而设计的专用空间100.64.0.0/10 ,RFC 6598,是CGNAT。

询问客服,明确现在不再提供公网IPv4 。

IPv6

任意设备打开Test your IPv6 connectivity,测试通过即可。

在 OneCloud 上查看:

1
2
ip -6 addr show dev eth0
ip -6 route

一张网卡可能同时有几个 IPv6 地址:

  • fe80:: 开头的链路本地地址,常用于邻居和网关通信。
  • ULA 地址,家庭网络中常见 fd 开头的地址。
  • 可用于公网路由的全局单播地址。
  • 标有 temporary 的临时地址。

将防火墙设置为低

关闭IPv6Spi

挑选一台设备启动监听服务,并且使用手机流量访问其ipv6地址,若能正确访问即配置正常。

直接在浏览器填写 IPv6 时,需要加方括号:

1
http://[设备的全局IPv6]:8123/

OpenWrt

IPv4 部分继续保持:

1
2
3
OpenWrt:192.168.1.30/24
网关:192.168.1.1
LAN DHCP:关闭

配置 wan6

从 OneCloud 进入容器:

1
docker exec -it openwrt sh

先备份:

1
2
3
backup_tag=$(date +%Y%m%d-%H%M%S)
cp /etc/config/network "/etc/config/network.$backup_tag.bak"
cp /etc/config/dhcp "/etc/config/dhcp.$backup_tag.bak"

这台 OpenWrt 的 LAN 逻辑接口使用 eth0。wan6 复用 LAN 所在设备,作为 DHCPv6 客户端:

1
2
3
4
5
6
uci set network.wan6='interface'
uci set network.wan6.proto='dhcpv6'
uci set network.wan6.device='@lan'
uci set network.wan6.reqaddress='try'
uci set network.wan6.reqprefix='no'
uci set network.wan6.peerdns='1'

这里使用上游所在的同一局域网,不向下游划分新子网,所以没有向上游请求新的前缀委派。需要另外划分 IPv6 子网时,再按照 PD 的拓扑配置。

接着关闭 LAN 的服务发布:

1
2
3
4
5
6
7
uci set dhcp.lan.ignore='1'
uci set dhcp.lan.ra='disabled'
uci set dhcp.lan.dhcpv6='disabled'
uci set dhcp.lan.ndp='disabled'

uci commit network
uci commit dhcp

这里关闭的是 LAN 上的服务器 / 发布功能,wan6 的 DHCPv6 客户端仍然工作。RA、DHCPv6、NDP 可以分别设置,选项含义见 OpenWrt odhcpd 说明。

容器里 IPv6 被禁用的情况

Docker 的网络配置与容器内部接口状态,要分别看。

1
2
3
sysctl net.ipv6.conf.all.disable_ipv6
sysctl net.ipv6.conf.default.disable_ipv6
sysctl net.ipv6.conf.eth0.disable_ipv6

这台设备上三个值都应该是 0。如果为 1,需要启用对应接口。当前容器通过 sysctl 文件持久化这些设置,下面给出同样用途的配置示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mkdir -p /etc/sysctl.d

cat > /etc/sysctl.d/60-onecloud-ipv6.conf <<'EOF'
net.ipv6.conf.all.disable_ipv6=0
net.ipv6.conf.default.disable_ipv6=0
net.ipv6.conf.eth0.disable_ipv6=0
net.ipv6.conf.default.forwarding=1
net.ipv6.conf.all.forwarding=1
net.ipv6.conf.all.accept_redirects=0
net.ipv6.conf.default.accept_redirects=0
net.ipv6.conf.eth0.accept_redirects=0
EOF

/etc/init.d/sysctl restart
/etc/init.d/network restart

接口名不同的话,文件里的 eth0 也要修改。修改后直接查路由和连通性,不要只凭某一个 sysctl 值判断 wan6 成功与否,OpenWrt 还有 netifd / odhcp6c 在管理网络。

1
2
3
4
ifstatus wan6
ip -6 addr show dev eth0
ip -6 route
curl -6 --noproxy '*' --max-time 15 https://www.cloudflare.com/cdn-cgi/trace

已经有 IPv6 地址并能出站后,再检查主机上的 Docker 网络:

1
2
docker network inspect macnet
docker exec openwrt ip -6 addr

本次 macnet 的 EnableIPv6 为 false,但容器内部已经通过上游取得 IPv6。Docker IPAM 管理 IPv6 子网,与容器接口启用后从上游获取 RA/SLAAC 地址,是不同的配置方式;接口能否接收 IPv6 仍要检查。Docker macvlan 文档

手动终端还要看 IPv6 路由

Linux 终端:

1
2
ip route
ip -6 route

Windows 可以查看:

1
Get-NetRoute -AddressFamily IPv6 -DestinationPrefix "::/0"

如果 IPv6 的下一跳还是光猫,这个终端的 IPv6 流量仍会使用光猫。

先在容器里查 OpenWrt 的链路本地地址:

1
ip -6 addr show dev eth0 scope link

需要手动经过 OpenWrt 的 Linux 终端,可用下面的方法临时验证,变量均替换为该终端的实际值:

1
2
3
4
5
6
7
CLIENT_IFACE='替换为终端网卡名'
OPENWRT_GW6='替换为OpenWrt链路本地IPv6'

sudo ip -6 route replace default \
via "$OPENWRT_GW6" dev "$CLIENT_IFACE" metric 50

ip -6 route

同时确认没有更优先的默认路由。临时路由验证成功后,通过终端的网络管理工具保存配置;IPv6 DNS 的选择也单独检查。

同网卡收发的旁路由还可能产生 IPv6 Redirect。当前 OpenWrt 使用下面的规则,避免手动终端收到重定向后改变路径:

1
2
3
4
5
6
7
8
9
10
uci set firewall.onecloud_no_v6_redirect='rule'
uci set firewall.onecloud_no_v6_redirect.name='Keep-manual-IPv6-gateway'
uci set firewall.onecloud_no_v6_redirect.dest='*'
uci set firewall.onecloud_no_v6_redirect.family='ipv6'
uci set firewall.onecloud_no_v6_redirect.proto='icmp'
uci set firewall.onecloud_no_v6_redirect.icmp_type='redirect'
uci set firewall.onecloud_no_v6_redirect.target='DROP'

uci commit firewall
/etc/init.d/firewall reload

普通设备保持原来的自动配置即可。完成容器中的配置后,用 exit 回到 OneCloud 宿主机;前面修改终端默认路由的命令,应在对应终端执行。

1
exit

DDNS

现在需要把域名指向承载公网入口的设备全局 IPv6,统一入口使用 OneCloud的IPv6 。

1
ip.example.com 的 AAAA → OneCloud 全局 IPv6

onecloud-ddns.py

它会从指定网卡选择地址、排除 temporary 等不可用状态、做 IPv6 出站确认、更新 Cloudflare 并读回记录。地址没变时也确认当前 DNS 内容,结果保存在本地状态文件中。

在 OneCloud 宿主机执行:

1
2
3
4
5
6
7
8
9
10
11
command -v python3 curl ip

sudo install -d -m 755 /usr/local/libexec
sudo install -d -m 700 /etc/onecloud-ddns

curl --noproxy '*' -fsSL \
https://raw.githubusercontent.com/51hhh/EdgeTier/v0.2.1/deploy/onecloud/onecloud-ddns.py \
-o /tmp/onecloud-ddns.py

sudo install -m 755 /tmp/onecloud-ddns.py \
/usr/local/libexec/onecloud-ddns

配置 Cloudflare 记录

先在 Cloudflare 建好当前入口的 AAAA,内容填经过验证的 OneCloud IPv6,关闭代理。

Zone ID 从域名概览取得。脚本支持 API Token,也兼容旧的 API Key + email 配置。下面给 Token 形式的示例,权限需要覆盖对应 zone 的 DNS 读取与编辑。

Record ID 按准确的 hostname 和类型查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
CF_ZONE_ID='替换为Zone ID'
DDNS_NAME='ip.example.com'

read -r -s -p 'Cloudflare API Token: ' CF_API_TOKEN
echo

curl --noproxy '*' --fail -sS \
"https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/dns_records?type=AAAA&name=$DDNS_NAME" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H 'Content-Type: application/json' \
-o /tmp/ddns-record.json

python3 -c '
import json
from pathlib import Path
data = json.loads(Path("/tmp/ddns-record.json").read_text())
records = data.get("result", [])
if not data.get("success") or len(records) != 1:
raise SystemExit("需要准确匹配一条 AAAA 记录")
print(records[0]["id"])
'
unset CF_API_TOKEN

编辑配置:

1
sudo nano /etc/onecloud-ddns/config.json
1
2
3
4
5
6
7
8
{
"interface": "eth0",
"name": "ip.example.com",
"zone_id": "<ZONE_ID>",
"record_id": "<RECORD_ID>",
"api_token": "<API_TOKEN>",
"ttl": 120
}

修改成自己的内容并保存:

1
sudo chmod 600 /etc/onecloud-ddns/config.json

已有 API Key 配置的设备可以继续使用脚本支持的 email 和 api_key 字段。配置文件与脚本分开保存,文章中的值都是占位值。

第一次还没有启用定时器时,先运行一次:

1
2
sudo /usr/local/libexec/onecloud-ddns
sudo /usr/local/libexec/onecloud-ddns --check

能看到 updated、unchanged 或检查通过的结果,再配置定时器。若出现多地址或多记录不明确的错误,先解决选址或记录配置,不随意取列表里的第一个。

用 systemd 定时运行

创建 /etc/systemd/system/onecloud-ddns.service:

1
2
3
4
5
6
7
8
9
10
11
12
13
[Unit]
Description=OneCloud verified IPv6 Cloudflare DDNS
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/libexec/onecloud-ddns
TimeoutStartSec=240
UMask=0077
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full

创建 /etc/systemd/system/onecloud-ddns.timer:

1
2
3
4
5
6
7
8
9
10
11
[Unit]
Description=Refresh OneCloud AAAA every two minutes

[Timer]
OnBootSec=30s
OnUnitActiveSec=2min
RandomizedDelaySec=10s
Unit=onecloud-ddns.service

[Install]
WantedBy=timers.target

启动:

1
2
3
4
5
6
7
sudo systemctl daemon-reload
sudo systemctl enable --now onecloud-ddns.timer

systemctl status onecloud-ddns.timer
systemctl list-timers onecloud-ddns.timer
sudo journalctl -u onecloud-ddns.service -n 20 --no-pager
sudo cat /var/lib/onecloud-ddns/status.json

启用定时器后,手动刷新使用:

1
sudo systemctl start onecloud-ddns.service

这样由 systemd 处理同一个服务的并发启动。不要在定时任务还在执行时直接另起一个脚本进程,脚本的锁会阻止重叠执行。

这是一次性任务,执行完出现 inactive 属于正常情况。可以查看:

1
2
systemctl show onecloud-ddns.service \
-p Result -p ExecMainStatus

判断最近一次是否成功,再看定时器是否仍在等待下一轮。

配置IPv6服务

Home Assistant 8123

Home Assistant 容器使用 host 网络,服务共享 OneCloud 的网络空间。

当前有:

1
2
0.0.0.0:8123
[::]:8123

它已经双协议监听。IPv6 入站路径通了以后,8123 就能访问,不需要为了它重建一套 Docker IPv6 网络。

1Panel 3000

1Panel 原先只有:

1
0.0.0.0:3000

0.0.0.0 是 IPv4 的通配监听地址。域名只有 AAAA 时,客户端尝试的是 IPv6,服务没有对应 socket

我这里是 1Panel v1.10.26-lts,使用其原生命令:

1
sudo 1pctl listen-ip ipv6

命令会切换监听并重启 1Panel。CLI 的说明见 1Panel v1 文档。

切换后分别测试:

1
2
3
4
sudo ss -lntp 'sport = :3000'

curl --noproxy '*' --max-time 10 http://127.0.0.1:3000/
curl --noproxy '*' --max-time 10 'http://[::1]:3000/'

这台主机切换后,IPv4 与 IPv6 均能返回 HTTP 200,局域网电脑的双协议测试也通过。

OpenList 5244

OpenList 使用 bridge 网络,宿主机发布 5244:

1
2
0.0.0.0:5244 -> 容器5244/tcp
[::]:5244 -> 容器5244/tcp

检查时本机和局域网 IPv4 访问正常,IPv6 连接超时,UFW 中没有 5244 的放行规则。

恢复 OpenWrt 的 8000 入口

第二篇曾把 OpenWrt 页面改到 8000,再通过公网 IPv4 的 DMZ 或端口映射进入旁路由。

这次查询后发现,当前 OpenWrt 由 nginx 提供 LuCI,实际监听的是:

1
2
3
192.168.1.30:80
192.168.1.30:443
IPv6的80与443

OneCloud 没有监听 8000,容器也没有监听 8000。

AAAA 又指向 OneCloud,所以浏览器连接 ip.example.com:8000 时,先找到的是宿主机。必须在宿主机上安排一个真正的入口。

最终使用下面的路径:

1
2
3
4
5
6
7
8
9
外部客户端
│ IPv6 / TCP 8000
▼
OneCloud 的 systemd socket
│ systemd-socket-proxyd
▼
192.168.1.30:80
│
OpenWrt nginx / LuCI

宿主机先能访问后端

macvlan 容器与宿主机不能直接通过父接口互相通信。第二篇创建的 macvlan0 就是用来处理这个问题的,Docker 文档也说明了这一限制和宿主机添加同父接口 macvlan 的方法。Docker macvlan 文档

先检查现有接口和服务:

1
2
3
4
5
ip addr show macvlan0
systemctl status macvlan-setup.service

curl --noproxy '*' --connect-timeout 3 --max-time 10 \
http://192.168.1.30/

这里要能得到后端响应,再继续设置 8000。

如果是刚按第二篇部署的新设备,可以用以下命令建立接口;已经有 macvlan0 的设备不要重复创建:

1
2
3
sudo ip link add link eth0 name macvlan0 type macvlan mode bridge
sudo ip addr add 192.168.1.46/24 dev macvlan0
sudo ip link set macvlan0 up

.46 要使用未被占用的地址。接口持久化继续使用第二篇的 macvlan-setup.service。如果接口已存在但后端仍不可达,还要检查到 .30 的路由是否确实使用 macvlan0:

1
ip route get 192.168.1.30

如果查询结果没有经过 macvlan0,可先为这个后端增加定向路由:

1
sudo ip route replace 192.168.1.30/32 dev macvlan0 src 192.168.1.46

验证后把相应路由保存进网络初始化服务,避免仅有临时配置。

用 systemd-socket-proxyd 转发

OneCloud 上已经有这个程序:

1
ls -l /lib/systemd/systemd-socket-proxyd

它接收 systemd 传入的监听 socket,再建立到后端的连接,双向转发 TCP 数据。Debian 11 对应的说明

创建 /etc/systemd/system/onecloud-openwrt.socket:

1
2
3
4
5
6
7
8
9
10
[Unit]
Description=OneCloud public OpenWrt TCP entry on port 8000

[Socket]
ListenStream=[::]:8000
BindIPv6Only=both
Accept=no

[Install]
WantedBy=sockets.target
  • ListenStream 指定 TCP 监听地址和端口。
  • BindIPv6Only=both 让这个 IPv6 socket 同时接受 IPv4 映射连接。
  • Accept=no 把监听 socket 交给一个常驻代理服务处理。

创建 /etc/systemd/system/onecloud-openwrt.service:

1
2
3
4
5
6
7
8
9
10
11
[Unit]
Description=Forward OneCloud port 8000 to OpenWrt LuCI
Requires=onecloud-openwrt.socket
After=onecloud-openwrt.socket network-online.target docker.service
Wants=network-online.target

[Service]
Type=simple
ExecStart=/lib/systemd/systemd-socket-proxyd 192.168.1.30:80
Restart=on-failure
RestartSec=2s

语法检查并启动:

1
2
3
4
5
6
7
8
9
sudo systemd-analyze verify \
/etc/systemd/system/onecloud-openwrt.socket \
/etc/systemd/system/onecloud-openwrt.service

sudo systemctl daemon-reload
sudo systemctl enable --now onecloud-openwrt.socket

curl --noproxy '*' --max-time 10 http://127.0.0.1:8000/
curl --noproxy '*' --max-time 10 'http://[::1]:8000/'

socket 设置为开机自启,service 会在有连接时被激活。service 不需要另外 enable。

最后放行主机入口:

1
2
sudo ufw allow 8000/tcp
sudo ufw status

再检查:

1
2
3
4
systemctl status onecloud-openwrt.socket onecloud-openwrt.service
systemctl is-enabled onecloud-openwrt.socket
sudo ss -lntp 'sport = :8000'
sudo journalctl -u onecloud-openwrt.service -n 20 --no-pager

本次这两个 unit 已经部署,socket 为 enabled / active,代理服务正常运行。