OpenWrt + IPv6 DDNS + wiregurad 全 VPN 组网落地手册

5小时前

OpenWrt + IPv6 DDNS + wiregurad 全 VPN 组网落地手册

WireGuard 为核心 · 公网零服务暴露 · 多 OS 客户端 · 双向访问 · 多站点互联

环境前提:OpenWrt 上 DNSPod(腾讯云)IPv6 DDNS 已配好,即存在一条 AAAA 记录(如 router.example.com)能稳定解析到路由器当前的公网 IPv6。 本手册的核心思路:既然 DDNS 已经让”路由器”在公网可被稳定寻址,我们就只把 WireGuard 的 UDP 51820 端口暴露在公网(经 IPv6),其余一切服务(NAS、HA、Jellyfin、SSH、RDP…)都不暴露公网,全部通过 VPN 隧道按内网域名/地址访问。这样:
  • 外部访问内网 Web 服务 → 拨入 VPN 后用 nas.lan 访问
  • 外部管理内网设备(SSH/RDP)→ 拨入 VPN 后直连内网 IP
  • 多站点互联(家↔父母家↔办公室)→ 两个 OpenWrt 之间站点对站点 WG
  • 内网设备主动连外(反向)→ 隧道天然双向,家里也能主动连到已拨入的手机/笔记本上的服务

0. 约定与地址规划(全文统一,照抄前先改成你自己的)

为避免歧义,全文使用以下地址段。实施前请替换为你自己的,且多站点时各站点 LAN 段不能重叠

用途 IPv4 IPv6 (ULA) 说明
家里 LAN 192.168.1.0/24 fd00:cafe:1::/64 路由器 = .1 / ::1
站点 B(父母家)LAN 192.168.2.0/24 fd00:cafe:2::/64 路由器 = .1 / ::1
WireGuard 隧道 10.200.0.0/24 fd00:cafe:9::/64 服务端 = .1 / ::1
DDNS 域名 router.example.com(站点 B 用 router-b.example.com)
内部域名后缀 .lan(由路由器 dnsmasq 解析)
为什么 VPN 隧道同时配 v4 和 v6 地址? 很多客户端所在网络只有 v4,隧道走 v4 更兼容;而家里内网又有 v6 服务。双栈隧道让两种资源都能到达。

1. 前置确认:DNSPod DDNS 端点可用性

WireGuard 客户端的 Endpoint 会写成 router.example.com:51820,所以这条 AAAA 必须始终指向路由器当前的公网 GUA。先验证:

# 在任意公网主机(或手机蜂窝网络)上
dig AAAA router.example.com +short
# 应输出类似 2408:xxxx:xxxx:xxxx::1

# 验证它正是路由器 WAN 当前的 GUA(在 OpenWrt 上)
ip -6 addr show pppoe-wan 2>/dev/null || ip -6 addr show wan 2>/dev/null
# 或
ifstatus wan | jsonfilter -e '@["ipv6-address"][0].address'

若两者一致 → DDNS 正常。若不一致 → 先修 DDNS(见附录 C 的 DNSPod 更新脚本与排查)。

重要提醒:客户端所在网络必须有 IPv6才能连到这个 v6 端点。若你的客户端常出现在纯 v4 环境(很多公司 WiFi、部分蜂窝),建议二选一:
  1. 向运营商要公网 IPv4,DDNS 同时维护 A 记录,WG 端点用域名(自动选 v4/v6);
  2. 用一台便宜 VPS 做 WG 中继(VPS 有双栈),客户端连 VPS,VPS 再连家里。本手册主流程按”IPv6 端点”写,VPS 中继见 §8。

2. OpenWrt WireGuard 服务端配置(家里路由器)

2.1 安装

⚠️ 先确认你的包管理器。OpenWrt 24.10+ / 25.x 已从 opkg 切换到 apk;旧版(23.05 及更早)仍是 opkg。下面给出两套命令,只跑你那套。判断方法:能跑 apk 就是新固件。

OpenWrt 25.x / 24.10(apk):

apk update
apk add wireguard-tools luci-proto-wireguard qrencode

OpenWrt 23.05 及更早(opkg):

opkg update
opkg install wireguard-tools luci-proto-wireguard qrencode
luci-proto-wireguard 让你能在 LuCI 里管理(否则 LuCI 显示”不支持的协议类型”);qrencode 用于给手机生成二维码。 装完 LuCI 扩展后,若 LuCI 仍显示”不支持的协议”,或新装的协议接口 ifup 静默失败,执行 /etc/init.d/network restart(不是 reload)重启 netifd,让它重新扫描协议脚本(/lib/netifd/proto/wireguard.sh)。详见 §2.7。 存储空间提示:WireGuard 相关包总计约 350–500 KB,但 64MB flash 设备(如新路由3)装完包后务必清缓存:apk cache cleanrm -f /var/opkg-lists/*(opkg)。OK: 31.1 MiB in 216 packages 这类输出是全系统已装总量,不是本次安装大小,别被吓到。空间紧张就挂 extroot。

2.2 生成服务端密钥对

mkdir -p /etc/wireguard                       # 目录默认不存在,必须先建
wg genkey | tee /etc/wireguard/server.key | wg pubkey > /etc/wireguard/server.pub
chmod 600 /etc/wireguard/server.key
cat /etc/wireguard/server.pub                 # 记下公钥,客户端 [Peer] PublicKey 用
绝对路径生成,别 cd /etc/wireguard 后再生成——该目录可能不存在导致 cd 失败,后续命令就在错误目录(/root)执行,文件位置对不上。 私钥随后会写入 /etc/config/networkwg0.private_key 字段(由 uci 完成),server.key 文件只是中转与备份,删掉不影响运行(建议离线备份,别上传云端)。

2.3 配置接口 /etc/config/network

uci 或直接编辑。下面用 uci 批量写入(可整体粘贴):

uci -q delete network.wg0
uci set network.wg0='interface'
uci set network.wg0.proto='wireguard'
uci set network.wg0.private_key="$(cat /etc/wireguard/server.key)"
uci set network.wg0.listen_port='51820'
uci add_list network.wg0.addresses='10.200.0.1/24'
uci add_list network.wg0.addresses='fd00:cafe:9::1/64'
uci commit network
⚠️ 必须 uci commituci show 看到的是 uci 内存状态,netifd 读的是 /etc/config/network 文件。漏 commit 会导致 netifd 根本不知道有 wg0,ifup wg0 静默失败。验证:grep -A10 "config interface 'wg0'" /etc/config/network 能看到才算真写进去了。 隧道地址为什么用 ULA(fd00:cafe:9::/64)而不是公网 GUA? 运营商下发的 IPv6 前缀是动态变化的(如 2409:8a62:... 每次拨号都变)。若隧道地址用 GUA,前缀一变整条隧道地址失效、所有客户端配置作废。ULA 永远不变,适合做隧道寻址。代价是 ULA 公网不可路由 → WG 客户端 IPv6 不走家里出公网(v4 靠 NAT 出,v6 访问内网走 ULA 互通即可,这是有意取舍)。详见 §6 与 §9 排错。

等会儿在 §2.6 加 peer(客户端)。先做防火墙。

2.4 防火墙:放行 51820 + 建立 wg 区域

端口可自定义:若 51820 被运营商/光猫封禁,改用其他端口(如 10520)。改端口时三处必须同步:uci set network.wg0.listen_port='10520' + 防火墙放行 10520 + 客户端 Endpoint 用 10520。改完 network restart(非 reload)让监听端口生效。下方示例用 51820,按需替换。

编辑 /etc/config/firewall,增加:

# 1) 放行 WAN 侧 UDP 51820(双栈,一条规则 family=any 即可)
uci add firewall rule
uci set firewall.@rule[-1].name='Allow-WG-51820'
uci set firewall.@rule[-1].src='wan'
uci set firewall.@rule[-1].dest_port='51820'
uci set firewall.@rule[-1].proto='udp'
uci set firewall.@rule[-1].target='ACCEPT'

# 2) 新建 wg 区域
uci add firewall zone
uci set firewall.@zone[-1].name='wg'
uci set firewall.@zone[-1].input='ACCEPT'
uci set firewall.@zone[-1].output='ACCEPT'
uci set firewall.@zone[-1].forward='ACCEPT'
uci add_list firewall.@zone[-1].network='wg0'

# 3) wg <-> lan 互通(双向访问的关键)
uci add firewall forwarding
uci set firewall.@forwarding[-1].src='wg'
uci set firewall.@forwarding[-1].dest='lan'
uci add firewall forwarding
uci set firewall.@forwarding[-1].src='lan'
uci set firewall.@forwarding[-1].dest='wg'

# 4) (可选)让 VPN 客户端走家里出口上网(v4)——靠 wan 区域的 masq,不要单独写 MASQUERADE 规则
uci add firewall forwarding
uci set firewall.@forwarding[-1].src='wg'
uci set firewall.@forwarding[-1].dest='wan'
# 给 wan 区域开 masq:这样所有转发到 wan 的 v4 流量自动 SNAT,WG 客户端即可 v4 出公网
#   先查 wan 是第几个 zone:uci show firewall | grep "name='wan'"  (假设 @zone[1])
uci set firewall.@zone[1].masq='1'          # 索引按你实际查到的改!别照抄 [1]

uci commit firewall
/etc/init.d/firewall restart
⚠️ 千万不要单独写 target='MASQUERADE' 的 rule。MASQUERADE 目标会跳到 masquerade_to_wan 链,而该链只有当 wan zone 启用 masq='1' 时才生成。若 wan 没开 masq 却写了 MASQUERADE rule,firewall restart 会报错:
Error: No such file or directory; did you mean chain 'forward_wan' ...?
  meta nfproto ipv4 counter jump masquerade_to_wan comment "!fw4: ..."
  The rendered ruleset contains errors, not doing firewall restart.
此时防火墙不会重启、保持旧规则。修复:删掉那条 MASQUERADE rule(uci delete firewall.@rule[N],N 用 uci show firewall | grep MASQ 查真实索引,别照抄文档里的索引),改为给 wan zone 开 masq='1'删 rule 时索引必须现查:uci show firewall | grep WG-MASQ 会显示如 firewall.@rule[13].name='WG-MASQ-v4',就用 13。文档里的 [1]/[3] 只是示例,每台机器不同。 masq 只管 IPv4。IPv6 是真实公网地址,不要开 masq6、不要做 NAT66(反模式,且 fw4 支持有限)。WG 客户端 v6 出公网用客户端本地网络的 v6,不绕回家;v6 访问内网走 ULA 互通。详见 §6。

2.4.1 确保 wan6 纳入 wan 区域

OpenWrt 里 IPv6 通常走单独的 wan6 接口。防火墙 wan 区域的 network 列表必须同时包含 wan 和 wan6,否则 v6 流量的 zone 归属会错。

uci show firewall.@zone[1]                   # 看 network 字段
# 应类似:firewall.@zone[1].network='wan wan6'
# 若没有 wan6,补上:
uci add_list firewall.@zone[1].network='wan6'   # 索引按实际
uci commit firewall
/etc/init.d/firewall restart
解释:wg↔lan 双向 forwarding 是”双向访问”的防火墙基础。lan→wg 让家里设备能主动连到已拨入的手机(反向访问)。若你不想要”v4 全流量走家”,省略第 4 步的 wg→wan forwarding 即可(纯 split tunnel,只访问内网)。

2.5 让 dnsmasq 在 wg 接口上提供 DNS(让客户端解析 .lan)

VPN 客户端会把 DNS 指向路由器隧道地址,需要 dnsmasq 监听 wg0。

⚠️ 血泪坑:千万别设 dnsmasq 的 interface 字段! 早期版本曾写 uci add_list dhcp.@dnsmasq[0].interface='wg0',这是错的。dnsmasq 的 interface/interfaces排他监听列表——一旦设置,dnsmasq 只监听列表里的接口,lan/loopback 等全被排除 → LAN 设备无法解析 DNS → 全屋断网。add_list 在这里不是”追加监听”,而是”限定只听这几个”。 正确做法:不设 interface 字段。 dnsmasq 默认监听所有接口(含 wg0、lan、loopback),只需用 notinterface 排除 WAN(防止 DNS 暴露公网)即可。OpenWrt 默认已排除 wan,通常什么都不用配,wg0 自动被监听。
# 1) 如果之前误设过 interface,必须清掉(恢复监听全部接口)
uci -q del_list dhcp.@dnsmasq[0].interface='wg0'
uci -q delete dhcp.@dnsmasq[0].interface          # 清空整个 interface 列表
# 确认 notinterface 含 wan(默认有,防 DNS 暴露公网):
#   uci show dhcp.@dnsmasq[0] | grep notinterface
#   没有则:uci add_list dhcp.@dnsmasq[0].notinterface='wan'

# 2) 给 wg0 建一个 dhcp 段但禁用 DHCP(只保留 DNS 解析能力,不发地址租约)
uci set dhcp.wg='dhcp'
uci set dhcp.wg.interface='wg0'
uci set dhcp.wg.ignore='1'

uci commit dhcp
/etc/init.d/dnsmasq restart

验证 dnsmasq 监听了 wg0 且没排除 lan:

# 看 dnsmasq 启动参数,应能看到监听 wg0 和 br-lan,且 --bind-dynamic 或监听 0.0.0.0
pgrep -a dnsmasq
# 测试:从路由器自身解析(应正常)
nslookup nas.lan 127.0.0.1
# 从 LAN 设备也能解析 = 没误伤 lan
误设 interface 后的症状与修复:全屋设备无法上网、nslookup 超时、LuCI 能开但网页打不开。修复就是上面第 1 步:清空 dhcp.@dnsmasq[0].interface 列表,restart dnsmasq。清空后 dnsmasq 回到”监听所有接口(除 notinterface 里的 wan)”,lan/wg0 都正常。

并定义内网域名(示例,按你设备改):

uci -q delete dhcp.nas_domain
uci add dhcp domain
uci set dhcp.@domain[-1].name='nas.lan'
uci set dhcp.@domain[-1].ip='192.168.1.10'        # 或 'fd00:cafe:1::10'
uci add dhcp domain
uci set dhcp.@domain[-1].name='ha.lan'
uci set dhcp.@domain[-1].ip='192.168.1.12'
uci add dhcp domain
uci set dhcp.@domain[-1].name='win.lan'
uci set dhcp.@domain[-1].ip='192.168.1.20'         # RDP 目标机
uci commit dhcp
/etc/init.d/dnsmasq restart

2.6 启动接口并添加第一个客户端 peer

# 装包后第一次启动,用 restart 而非 reload,确保 netifd 重新加载协议脚本
/etc/init.d/network restart
sleep 3
wg show
⚠️ 常见坑:ifup wg0 / network reloadwg0 不存在
  • 症状:ip -6 addr show wg0Device "wg0" does not exist.,wg show 空输出,ifup wg0 无任何回显,且 netifd 日志里完全没有 wg0 记录
  • 根因:reload 只重载已注册接口,不重新扫描协议脚本。刚装完 wireguard-tools 后协议才注册,netifd 仍持有旧的协议列表,不认识 wireguard 协议。
  • 修复:/etc/init.d/network restart(重启整个 netifd,重新扫描 /lib/netifd/proto/wireguard.sh)。验证该文件存在:ls -l /lib/netifd/proto/wireguard.sh
  • 其他可能:uci commit network 漏了(配置没写进 /etc/config/network,netifd 读不到)→ grep -A10 "config interface 'wg0'" /etc/config/network 确认。
  • 兜底排查:logread -e netifd | tail -30 看 netifd 报什么;which wg 确认命令在 PATH。

peer 的添加在 §3 为每个客户端做(因为要先有客户端公钥)。

2.7 验证服务端

wg show              # 应看到 wg0 listening on 51820 + public key(此时无 peer,正常)
ip -6 addr show wg0  # 应有 10.200.0.1/24 和 fd00:cafe:9::1/64
以命令行 wg show 为准,别迷信 LuCI。LuCI 显示可能滞后或受协议包加载时机影响;只要 wg showlistening port: 51820 就是真正起来了。装了 luci-proto-wireguard 后强刷 LuCI(/etc/init.d/rpcd restart 后浏览器 Ctrl+F5)即正常。
wg0 没有 MAC 地址是正常的,不是配置错。 WireGuard 是 L3(网络层)隧道接口,直接封装 IP 包、没有以太网帧头,而 MAC 是 L2(数据链路层)概念,只有承载以太网帧的接口(eth0/br-lan/wlan0)才有。所以 ip link show wg0 输出里没有 link/ether 那一行是预期行为(与 lotun 同类)。判断 wg0 是否正常只看 IP 地址和 wg show,不要用有无 MAC 来判断。

2.7.1 怎么算”连接成功”(接口层 vs 握手层)

接口起来 ≠ 隧道打通。分两层判定:

层 1:接口起没起(服务端/客户端通用)

ip -6 addr show wg0     # 有隧道地址 = 接口层 OK
wg show                 # 能列出接口 + peer = 接口层 OK

层 2:握手成没成功(隧道是否真通)—— 这才是硬指标

wg show

看 peer 下这两行:

  • latest handshake: 几秒/几十秒前 = 握手成功,密钥协商完成,隧道建立 ✅
  • 同时 transfer: ... received, ... sent 计数在涨 = 真的在收发包 ✅
  • 没有 latest handshake 这行transfer: 0 B received = 握手失败,没真正连上(查公钥是否贴反、endpoint 解析、防火墙是否挡 51820、客户端是否残留 ListenPort,见 §10)。

握手成功后用实际流量验证端到端:

ping 10.200.0.1         # 通 = 隧道对端可达
ping nas.lan            # 通 = DNS 解析 + 内网都通
ping 192.168.1.10       # 通 = 路由/forwarding 都对
状态 wg show 表现 结论
接口没起 空输出 / Device does not exist 配置或 netifd 问题(§2.6 坑)
接口起了、没握手 有 peer,但无 latest handshake,rx=0 握手失败,查 endpoint/公钥/防火墙
握手成功 latest handshake: 几秒前 + rx/tx 在涨 连接成功
端到端通 上面 + ping 10.200.0.1 / ping nas.lan 全链路 OK ✅✅

从公网(手机蜂窝)测试 UDP 可达:

nc -6 -u -v router.example.com 51820   # 不通也没响应是正常的(WG 不握手不回包),主要看防火墙没被 drop

更可靠的验证:配好客户端后看 wg show 出现最新握手时间。


3. 客户端配置(四类 OS)

每个客户端都需要:自己的密钥对 + 一份 .conf。下面先给通用 .conf 模板,再讲各 OS 怎么导入。

3.1 通用客户端配置模板

把下面存为 client.conf,按注释替换:

[Interface]
PrivateKey = <客户端私钥>
Address = 10.200.0.2/24, fd00:cafe:9::2/64
DNS = 10.200.0.1, fd00:cafe:9::1
MTU = 1420

[Peer]
PublicKey = <服务端公钥,即 /etc/wireguard/server.pub>
PresharedKey = <可选,见 3.2;不用就删掉这行>
Endpoint = router.example.com:51820
# 想访问哪些内网段,就写哪些。下面是家里全部内网 + 隧道段:
AllowedIPs = 192.168.1.0/24, fd00:cafe:1::/64, 10.200.0.0/24, fd00:cafe:9::/64
# 想让全部流量都走家里(全隧道),改为:
# AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25
AllowedIPs 的含义:既决定”哪些目标流量进隧道”,也决定”服务端把哪些源地址路由给这个 peer”。所以它必须精确包含该客户端的隧道地址之外,还要包含它要访问的内网段。
⚠️ 客户端配置三大坑(实测踩过,务必避免):
  1. 绝不要在客户端 [Interface] 里写 ListenPort。这是服务端用的。客户端设了 ListenPort 会强制绑定本地端口监听,导致 Android/iOS 的 VpnService 路由混乱、握手包发不出去(表现:tcpdump 抓不到 WG 包,wg show 无握手,但 ping/nmap 能通)。客户端用随机端口主动连服务端即可。
  2. AllowedIPs 格式必须用半角逗号+空格 , 分隔,绝不能用全角逗号 。从中文输入法粘贴、或带不可见字符,Android App 会报 bad address。最稳的做法是用二维码导入(见 §3.5),由 qrencode 保证格式。手动输入时每段可单独成行。验证配置文件无隐藏字符:cat -A client.conf(正常行尾 $,无 ^M/M- 乱码)。
  3. 路由环:Endpoint 所在网段不能出现在 AllowedIPs 里(内网测试时)。若 Endpoint = 192.168.1.1:51820AllowedIPs192.168.1.0/24,手机会把去往 192.168.1.1 的握手包也导向尚未建立的隧道 → 死锁 → 不发包。内网测试时 AllowedIPs 去掉 Endpoint 网段,只留隧道段:10.200.0.0/24, fd00:cafe:9::/64, fd00:cafe:1::/64。公网测试(Endpoint=域名)无此问题,因为域名解析出的公网地址不在内网段。0.0.0.0/0 全流量模式下,WG 会自动排除 Endpoint 路由,公网场景可用;但内网 Endpoint + 0.0.0.0/0 仍可能死锁,故内网测试优先用具体段。
MTU 选择:默认 1280(IPv6 最小值,保守但低效,包多速度慢)。链路 MTU 1500 时用 1420,PPPoE(1492)用 1412。调大可提速,但过大会丢包。测隧道 MTU:ping -M do -s 1392 10.200.0.1,不通就减,最大不丢包值 +28 = WG MTU。

3.2 (推荐)生成预共享密钥 PSK,多一层对称加密

在任一机器上:

wg genpsk > psk.txt

把内容同时填到服务端该 peer 的 preshared_key客户端 [Peer]PresharedKey。即使量子计算时代也能多抗一层。

3.3 生成客户端密钥对

Linux / macOS:

wg genkey | tee client.key | wg pubkey > client.pub

Windows(PowerShell,装了官方 WireGuard 后):

& "C:\Program Files\WireGuard\wg.exe" genkey | Out-File -Encoding ascii client.key
& "C:\Program Files\WireGuard\wg.exe" pubkey < client.key | Out-File -Encoding ascii client.pub

或在官方 GUI 里点 “Generate keypair”

iOS / Android: 在官方 WireGuard App 里新建隧道时选”Create from scratch”,App 会自动生成密钥;你只需把它的 Public key 抄出来填到路由器。

3.4 把客户端公钥注册到 OpenWrt 服务端

假设手机客户端公钥是 PHONE_PUB,隧道地址 10.200.0.2 / fd00:cafe:9::2:

uci add network wireguard_wg0
uci set network.@wireguard_wg0[-1].description='phone'
uci set network.@wireguard_wg0[-1].public_key='PHONE_PUB'
uci set network.@wireguard_wg0[-1].preshared_key="$(cat /etc/wireguard/psk.txt)"   # 可选
uci add_list network.@wireguard_wg0[-1].allowed_ips='10.200.0.2/32'
uci add_list network.@wireguard_wg0[-1].allowed_ips='fd00:cafe:9::2/128'
uci commit network
ifdown wg0 && ifup wg0
服务端 peer 的 allowed_ips 只填该客户端的隧道地址(它要访问的内网段不写在这里)。要”反向”让家里访问手机上的服务,靠的就是这个隧道地址——家里设备连 10.200.0.2:端口 即可(见 §4.3)。

3.5 各 OS 导入与启用

Windows

  1. WireGuard for Windows
  2. 打开 → “Import tunnel(s) from file” → 选 client.conf
  3. 双击隧道 → 连接。
  4. 测试:ping 10.200.0.1ping nas.lanping 192.168.1.10

macOS

  1. App Store 装 “WireGuard” 或 brew install wireguard-tools
  2. App:”Import from file” → client.conf;或命令行:
    sudo wg-quick up client   # 配置放 /usr/local/etc/wireguard/client.conf
    
  3. 测试同上。

Linux(以 wg-quick + systemd 为例)

sudo apt install wireguard                 # Debian/Ubuntu
# 或 sudo dnf install wireguard-tools      # Fedora
sudo install -d -m 700 /etc/wireguard
sudo cp client.conf /etc/wireguard/wg-home.conf
sudo wg-quick up wg-home                   # 临时启用
sudo systemctl enable --now wg-quick@wg-home   # 开机自启

NetworkManager 方式(GUI 发行版):

nmcli connection import type wireguard file wg-home.conf
nmcli connection up wg-home

iOS / Android

  1. 装官方 WireGuard App。
  2. 两种导入:
    • 二维码:在路由器上对配置生成码,手机扫码——
      # 把上面 client.conf 内容生成二维码(路由器装了 qrencode)
      qrencode -t ansiutf8 < client.conf
      # 手机 App → "+" → "Scan from QR code"
      
    • 文件:把 client.conf 传到手机 → App → “+” → “Import from file”。
  3. 连接后用 Safari/Chrome 访问 http://nas.lan 验证。
给手机配 IP 时,建议用固定隧道地址(如手机固定 10.200.0.2),方便家里反向连它。每多一台设备,递增一个地址并在服务端加 peer。

4. 双向访问实现(四种场景全部打通)

4.1 场景一:外部访问内网 Web 服务

拨入 VPN 后,客户端的 AllowedIPs192.168.1.0/24,DNS 指向路由器 → 直接:

http://nas.lan:5000        # NAS 管理台
http://ha.lan:8123         # HomeAssistant
http://jellyfin.lan:8096

无需任何公网暴露。若想用 HTTPS,在 NAS/服务本机用自签证书或内网 CA 即可(因为只走隧道,证书可信度由你自己的客户端决定,见 §6.4)。

4.2 场景二:外部管理内网设备(SSH / RDP)

拨入 VPN 后直连内网 IP:

# SSH 到 Linux 机器
ssh user@192.168.1.10
ssh user@nas.lan

# RDP 到 Windows 机器(先在目标机开启远程桌面)
# Windows: 设置 → 远程桌面 → 启用;并确保防火墙放行 3389(内网即可)
# 从客户端:
xfreerdp /v:192.168.1.20 /u:username      # Linux/macOS
# 或 Windows 自带"远程桌面连接" → 192.168.1.20
目标机只需在内网放行对应端口(SSH 22 / RDP 3389 给 192.168.1.0/24 或给路由器),无需对公网开放。

4.3 场景三:内网设备主动连外(反向,家里访问手机上的服务)

WireGuard 是点对点隧道,一旦手机拨入,隧道双向可达。家里设备要访问手机上跑的服务(如手机上的 Termux SSH、AirDroid、调试服务):

  1. 手机客户端隧道地址固定为 10.200.0.2(§3.4 已注册)。
  2. 防火墙 §2.4 已开 lan→wg forwarding。
  3. 手机上的服务监听 0.0.0.0 或隧道地址 10.200.0.2
  4. 家里任意设备:
    ssh termux@10.200.0.2 -p 8022      # 连手机 Termux SSH
    curl http://10.200.0.2:8080         # 连手机上的服务
    
关键:服务端 peer 的 allowed_ips 必须包含 10.200.0.2/32(§3.4 已做),否则家里发往 10.200.0.2 的包不知道路由给哪个 peer。

4.4 场景四:多站点互联(家 ↔ 父母家)→ 见 §5


5. 多站点互联(站点对站点 WireGuard)

两台 OpenWrt:站点 A(家,router.example.com)、站点 B(父母家,router-b.example.com)。让 A 站的设备能直接访问 B 站的设备,反之亦然。

5.1 前提

  • 两站 LAN 段不重叠(本例 A=192.168.1.0/24,B=192.168.2.0/24)。
  • 两站 DDNS 都配好(AAA 记录指向各自路由器 GUA)。
  • 两站都按 §2 装好 WireGuard 并有 wg0 接口。建议两站 wg0 用同一隧道网段不同地址:A=10.200.0.1,B=10.200.0.2
若 A 的 wg0 已是 10.200.0.1 并服务着手机客户端,只需把 B 也接进这个 wg0(作为另一个 peer)即可。下面假设这种”一台路由器 wg0 既是接入服务端又是站点 peer”的常见结构。

5.2 站点 A(家)配置 B 为 peer

uci add network wireguard_wg0
uci set network.@wireguard_wg0[-1].description='site-B'
uci set network.@wireguard_wg0[-1].public_key='<B的公钥>'
uci set network.@wireguard_wg0[-1].preshared_key="$(cat /etc/wireguard/pskAB.txt)"
# 关键:把 B 站的 LAN 段和 B 的隧道地址都放进 allowed_ips
uci add_list network.@wireguard_wg0[-1].allowed_ips='10.200.0.2/32'
uci add_list network.@wireguard_wg0[-1].allowed_ips='192.168.2.0/24'
uci add_list network.@wireguard_wg0[-1].allowed_ips='fd00:cafe:2::/64'
# 站点 B 没有公网 v4 时,给 endpoint 用它的 DDNS 域名 + keepalive 维持
uci set network.@wireguard_wg0[-1].endpoint_host='router-b.example.com'
uci set network.@wireguard_wg0[-1].endpoint_port='51820'
uci set network.@wireguard_wg0[-1].persistent_keepalive='25'
uci commit network
ifdown wg0 && ifup wg0

5.3 站点 B 配置 A 为 peer(对称)

在站点 B 的 OpenWrt 上(假设它的 wg0 地址是 10.200.0.2):

uci add network wireguard_wg0
uci set network.@wireguard_wg0[-1].description='site-A'
uci set network.@wireguard_wg0[-1].public_key='<A的公钥>'
uci set network.@wireguard_wg0[-1].preshared_key="$(cat /etc/wireguard/pskAB.txt)"
uci add_list network.@wireguard_wg0[-1].allowed_ips='10.200.0.1/32'
uci add_list network.@wireguard_wg0[-1].allowed_ips='192.168.1.0/24'
uci add_list network.@wireguard_wg0[-1].allowed_ips='fd00:cafe:1::/64'
uci set network.@wireguard_wg0[-1].endpoint_host='router.example.com'
uci set network.@wireguard_wg0[-1].endpoint_port='51820'
uci set network.@wireguard_wg0[-1].persistent_keepalive='25'
uci commit network
ifdown wg0 && ifup wg0

5.4 两站防火墙:允许 wg↔lan

两站都执行(与 §2.4 的 wg↔lan forwarding 相同,若已建 wg 区域则已有):

# 若还没建 wg 区域,参考 §2.4。已有则只需确保 forwarding 存在:
uci add firewall forwarding; uci set firewall.@forwarding[-1].src='wg'; uci set firewall.@forwarding[-1].dest='lan'
uci add firewall forwarding; uci set firewall.@forwarding[-1].src='lan'; uci set firewall.@forwarding[-1].dest='wg'
uci commit firewall; /etc/init.d/firewall restart

5.5 (关键)关闭两站之间的 ICMP/路由限制 + 允许转发

确保内核转发开启(OpenWrt 默认开):

cat /proc/sys/net/ipv4/ip_forward          # 应为 1
cat /proc/sys/net/ipv6/conf/all/forwarding # 应为 1

5.6 验证

# 在 A 站路由器上
wg show                       # 看到 site-B peer 有最新握手 + rx/tx 计数
ping 192.168.2.1              # ping 通 B 站路由器
# 在 A 站某台 LAN 设备上
ping 192.168.2.x              # ping 通 B 站内网设备
ssh user@192.168.2.x          # 直接 SSH 到父母家设备
三站及以上同理:每两站互指 peer,allowed_ips 写对方所有内网段。注意别成环,必要时用更精确的路由 / 调整 allowed_ips。

6. 安全加固(全 VPN 架构下的纵深防御)

即使”只暴露一个 WG 端口”,也要按”这个端口迟早会被发现”来设防。

6.1 WireGuard 自身

  • PSK 必加(§3.2),抗未来量子泄露。
  • 私钥权限 chmod 600,不上传公网仓库/云笔记。建议离线保管(如加密 U 盘)。
  • 端口可改非 51820(降低扫描噪声):改 listen_port 与防火墙放行端口即可。
  • peer 列表最小化:离职/卖掉的设备立刻删 peer(uci delete network.@wireguard_wg0[N])。
  • 客户端 AllowedIPs 用最小集:只放需要访问的内网段,别图省事全 0.0.0.0/0(除非你真要全流量走家)。

6.2 边界防火墙

  • wan 区域 INPUT/FORWARD 默认 DROP,仅放行 51820/udp(§2.4)。
  • 关闭 UPnP / NAT-PMP:/etc/config/upnpd 删除或禁用服务,防止内网程序自行开洞。
    /etc/init.d/miniupnpd disable && /etc/init.d/miniupnpd stop
    
  • ICMPv6 必需类型放行(ND/PMtuD:类型 1/2/3/4/128/129/133-137),否则 IPv6 异常;echo-reply 可选关闭防扫描。
  • LuCI / SSH 绝不监听 WAN:/etc/config/uhttpd 监听地址只绑内网;dropbear 同理。

6.3 路由器加固

# 改强 root 密码
passwd

# 用 SSH key 登录,禁密码(/etc/config/dropbear)
uci set dropbear.@dropbear[0].PasswordAuth='off'
uci set dropbear.@dropbear[0].RootPasswordAuth='off'
uci commit dropbear; /etc/init.d/dropbear restart
# 先放好 authorized_keys 再禁密码!否则会锁死
mkdir -p /etc/dropbear
cp your_pubkey /etc/dropbear/authorized_keys
  • 关闭不用的服务:telnet、tftp、旧 uhttpd 端口。
  • 及时更新:apk update && apk upgrade(apk,25.x)或 opkg update && opkg upgrade(opkg,旧版),关注 OpenWrt 安全版本。
  • 备份含密钥:sysupgrade -b 导出的 tar 里有私钥,离线保管。

6.4 服务侧(即使只在内网)

  • NAS/HA/Jellyfin 仍要强密码 + 2FA,因为 VPN 内的设备也可能被 compromise(如手机丢了)。
  • 敏感服务走 HTTPS(自签证书或自建 CA),防 VPN 内嗅探。
  • IoT 隔离:摄像头/扫地机放独立 VLAN(见 §7),禁止其访问 lan/wg,仅允许 iot→wan

6.5 DNSPod DDNS 凭证最小权限

  • 腾讯云 → 访问管理 CAM → 新建子用户 → 只授予 QcloudDNSPodFullAccess 或更精细的”仅指定域的记录编辑”自定义策略。
  • 不要用主账号 API 密钥
  • SecretId/SecretKey 存路由器脚本里 chmod 600,且不要打印到日志/备份
  • 定期轮换;监控更新失败(前缀变了但更新失败 → 域名指向旧地址 → VPN 断)。

6.6 客户端安全

  • 手机/电脑系统全盘加密、锁屏密码。
  • WireGuard 配置文件含私钥,传输用加密渠道,用完即删。
  • 公司设备别长期挂着家 VPN,避免家内网被公司 MDM 风险波及。

7. 性能瓶颈与硬件选型(重要:先看再排查速度)

实测案例:小米路由器 4C(MT7628N 单核 580MHz、64MB 内存、16MB flash)跑 WireGuard,速度仅 15-25 Mbps,CPU 100% 满载。 这是硬件极限,任何配置调整都无法突破。本节帮你判断自己是否撞到硬件墙。

7.1 怎么判断是不是硬件瓶颈

跑 WG 传大文件时,在路由器上看 CPU:

top -d 2 -n 3

若看到(典型硬件瓶颈特征):

CPU: 0% usr  60% sys  0% idle  40% sirq        ← idle 0%,满载
[kworker/0:2+wg-]  36%                          ← WG 加密内核线程占大头
[kworker/0:1+wg-]  29%
[ksoftirqd/0]      11%
Load average: 4.36                              ← 单核负载 4 倍,严重过载
netifd  D 状态                                   ← D=不可中断,CPU 太忙连 netifd 都卡

wg- 内核 worker 占满 CPU + idle 0% = CPU 加密到顶,纯硬件瓶颈。配置层面无解。

7.2 各级硬件的 WireGuard 速度天花板

硬件 典型机型 WG 速度 备注
MT7628N 单核 小米4C、NW702 等 15-25 Mbps 入门级,跑 WG 勉强,只够轻量管理
MT7621AT 双核 红米 AC2100、Newifi3、K2P 150-250 Mbps 性价比之选,日常够用
MT7621 + 硬件加密 部分型号 略提升 WG 用 ChaCha20,多数硬件加密引擎只支持 AES,帮不上
MT7981/7986 红米 AX6000、GL.iNet Flint2 300-500 Mbps 现代 WiFi6,主流推荐
IPQ8074 小米 AX9000 500-700 Mbps 高端
x86 N100/J4125 软路由 ~1 Gbps 跑满千兆
注意:WireGuard 用 ChaCha20-Poly1305,多数路由器的硬件加密引擎只加速 AES(给 IPSec/OpenVPN 用),对 WG 帮不上。所以 WG 速度主要看 CPU 核数和主频,不能只看”有硬件加密”。

7.3 软优化(撞墙前的调优,有限提升)

  1. 改 split tunnel(最有效):客户端 AllowedIPs 用具体内网段,而非 0.0.0.0/0。这样上网流量不走 VPN、不经路由器加密,CPU 只处理内网访问流量。0.0.0.0/0 全流量模式让 CPU 同时干加密+NAT,雪上加霜。
  2. 调大 MTU:MTU = 1420(链路 1500)或 1412(PPPoE 1492)。1280 包多中断多、CPU 忙。测 MTU:ping -M do -s 1392 10.200.0.1
  3. 关掉不用的服务腾 CPU/内存:如同时跑 ZeroTier(和 WG 功能重叠)、UPnP 等。top 看谁占资源,不用就 disable
  4. IPv6 不走 VPN 出口:隧道地址用 ULA(公网不可路由),WG 客户端 v6 出公网用客户端本地 v6,不做 NAT66。这避免了 NAT66 的 CPU 开销和反模式坑。

7.4 硬突破:把 WG 服务端挪到强设备

路由器 CPU 弱但家里有强设备(NAS、小主机、树莓派4、常开电脑)时,WG 服务端跑在强设备上,路由器只做端口转发,加密负担转移:

  • 强设备(如 NAS)装 WireGuard,监听 10520
  • 路由器把公网 10520/udp 转发到强设备内网 IP
  • 手机拨路由器公网地址:10520 → 路由器转发 → 强设备加密
  • 速度由强设备决定,通常能跑满百兆甚至千兆
# OpenWrt 端口转发(v4;v6 场景让强设备直接拿 GUA 监听更优)
config redirect
    option name 'WG-to-NAS'
    option src 'wan'
    option src_dport '10520'
    option dest 'lan'
    option dest_ip '192.168.1.50'      # 强设备内网 IP
    option dest_port '10520'
    option proto 'udp'

7.5 迁移案例:小米4C → 红米 AC2100

若手头有更强路由器(如红米 AC2100,MT7621 双核),直接让它当主路由跑 WG,4C 退二线。速度从 20M 提到 150-250M(7-10 倍),CPU 不再满载,内存 128M 宽裕。

迁移要点:

  1. AC2100 刷 OpenWrt/ImmortalWrt(原厂不支持 WG)。
  2. AC2100 接光猫/上级网络,重做 §2 全部配置。
  3. 密钥可复用:在 4C 导出 uci show network.wg0server.keypsk.txt,在 AC2100 用相同值配置 → 手机客户端配置零改动(只要 DDNS 域名仍指向新路由器的 GUA)。
  4. DDNS 在 AC2100 上跑,域名更新指向 AC2100 的 GUA。
  5. IPv6 场景下 AC2100 直接当主路由最顺(避免 4C 做 v6 转发的复杂拓扑)。
结论:在弱路由器(4C/MT7628N 级)上反复调 WG 速度是浪费时间——配置调不出硬件没有的速度。 先确认 CPU 是否满载(§7.1),是则按 §7.4 挪走 WG 或 §7.5 换路由器。split tunnel(§7.3)只让”上网不受 VPN 限速”,内网访问速度仍受硬件天花板限制。

7.6 弱路由器的内存与闪存纪律

以小米4C(64MB 内存 / 16MB flash)为例,资源极紧张,务必:

  • 服务最小集,不堆插件(反代、监控等别往上加)。
  • apk cache clean / rm -f /var/opkg-lists/* 清缓存;df -h / 保持几 MB 余量。
  • 16MB flash 无 USB 口无法 extroot,只能省着用。
  • 内存紧会 OOM/服务崩溃,top 定期看 Mem

8. 进阶:IPv6 子网划分与 VLAN 隔离(可选)

运营商下发 /60 或 /56 时,可给不同 VLAN 分配不同 /64 公网前缀,同时每 VLAN 配 ULA(断网也能内联)。

WAN 下发 2408:aaaa:bbbb:cc00::/60
├─ lan    2408:aaaa:bbbb:cc10::/64   fd00:cafe:1::/64   信任设备
├─ iot    2408:aaaa:bbbb:cc20::/64   fd00:cafe:3::/64   IoT 隔离
└─ guest  2408:aaaa:bbbb:cc30::/64   fd00:cafe:4::/64   访客

OpenWrt 示例(IoT 接口):

# 网络
uci set network.iot=interface
uci set network.iot.proto='static'
uci set network.iot.ipaddr='192.168.3.1/24'
uci add_list network.iot.ip6addr='fd00:cafe:3::1/64'
uci set network.iot.device='br-iot'
# DHCPv6/RA 分发公网前缀
uci set dhcp.iot=dhcp
uci set dhcp.iot.interface='iot'
uci set dhcp.iot.ra='server'
uci set dhcp.iot.dhcpv6='server'
uci set dhcp.iot.ra_management='1'
# 防火墙:iot 独立区域,仅 iot→wan,禁止 iot↔lan / iot↔wg
uci add firewall zone; uci set firewall.@zone[-1].name='iot'
uci set firewall.@zone[-1].input='REJECT'; uci set firewall.@zone[-1].output='ACCEPT'
uci set firewall.@zone[-1].forward='REJECT'; uci add_list firewall.@zone[-1].network='iot'
uci add firewall forwarding; uci set firewall.@forwarding[-1].src='iot'; uci set firewall.@forwarding[-1].dest='wan'
uci commit; /etc/init.d/network restart; /etc/init.d/firewall restart; /etc/init.d/dnsmasq restart
IoT 区域 forward=REJECT 天然阻止其访问 lan/wg,即便某摄像头被攻破也无法横向到 NAS。若 HomeAssistant 在 lan 需控制 iot,再加一条 lan→iot 的精确端口放行。

9. 纯 v4 环境兜底:VPS 中继(可选)

若客户端常在无 IPv6 的网络,可在双栈 VPS 上做中继:客户端 → VPS(wg0)→ 家(wg1)。VPS 的 wg1 peer 指向 router.example.com:51820(家里),家里 peer 的 endpoint 指向 VPS。VPS 内开启 ip_forward 并在两个 wg 接口间放行转发。这样客户端只需能连到 VPS 的 v4/v6,家里无需公网 v4。

要点(VPS 上):

sysctl -w net.ipv4.ip_forward=1
sysctl -w net.ipv6.conf.all.forwarding=1
# wg0(面向客户端)与 wg1(面向家)各自配置 peer,
# AllowedIPs 互指对端内网段,防火墙放行 wg0↔wg1 转发。

10. 排错速查

现象 排查
wg0 没有 MAC 地址 正常现象,不是故障。WireGuard 是 L3 隧道接口,没有以太网帧头,MAC 是 L2 概念只有 eth0/br-lan/wlan0 才有。判断 wg0 状态看 IP 地址和 wg show,不看 MAC(见 §2.7)。
不确定是否连接成功 接口层:ip addr show wg0 有地址 + wg show 能列出 peer。握手层(硬指标):wg show 里 peer 有 latest handshake: 几秒前transfer 计数在涨 = 连接成功;无 latest handshake、rx=0 = 握手失败(查公钥/endpoint/防火墙,见 §2.7.1)。
手机报 bad address 客户端 AllowedIPs 格式错:用了全角逗号 或带不可见字符。用半角 , 分隔,或用二维码导入(§3.5)。检查 cat -A client.conf
手机能 ping6 路由器、但 wg show 无握手、tcpdump 抓不到 WG 包 客户端配置里残留 ListenPort(必须删),或路由环(Endpoint 网段在 AllowedIPs 内,见 §3.1 坑2/3)。删 ListenPort + 内网测试去掉 Endpoint 网段。
握手成功但 ping 不通内网 客户端 AllowedIPs 是否含目标段;服务端 peer allowed_ips 是否含客户端隧道地址;wg↔lan forwarding 是否存在。
能 ping IP 但域名不通 客户端 DNS 是否指向 10.200.0.1;路由器 dnsmasq 是否监听 wg0(§2.5);.lan 记录是否定义。
全屋设备突然无法上网/DNS 解析全超时 dnsmasq 的 interface 字段被设成 wg0,导致只监听 wg0、排除了 lan。 修复:uci delete dhcp.@dnsmasq[0].interface; uci commit dhcp; /etc/init.d/dnsmasq restart(见 §2.5 警告)。dnsmasq 默认监听所有接口,不要设 interface,用 notinterface 排除 wan 即可。
反向(家→手机)不通 服务端 peer allowed_ips 是否含 10.200.0.2/32;lan→wg forwarding 是否存在;手机服务是否监听 0.0.0.0/隧道地址。
多站点某方向不通 对端 allowed_ips 是否含本站 LAN 段;两站 LAN 段是否重叠;persistent_keepalive 是否设了。
DDNS 更新后仍连不上 dig AAAA 看是否真更新;WG endpoint 会缓存解析,重启隧道 ifdown wg0 && ifup wg0 让它重解析。
公网拨入 packets 0(换端口也不涨)、内网却正常 光猫拨号模式下光猫挡了入站 IPv6 UDP。进光猫管理界面关 IPv6 防火墙/放行,或光猫改桥接让 OpenWrt 直接拨号拿公网 v6。见 §9 VPS 中继为兜底。
IPv6 拿不到(wan6 up:false) logread -e odhcp6c 看握手;/lib/netifd/dhcpv6.script 是否报 Command failed: Not found(缺依赖工具,重装 odhcp6c);uci commit network 是否漏了;装协议包后用 network restart 而非 reload(见 §2.6)。
IPv6 异常/时断时续 ICMPv6 必需类型是否被防火墙误杀;前缀变更后 wg0 的 GUA endpoint 是否过时。
WG 速度慢、跑不满宽带 top 看 CPU:若 wg- 内核 worker 占满、idle 0% = 硬件加密瓶颈(见 §7)。弱路由器(MT7628N 等)无解,改 split tunnel + 调 MTU 缓解,或挪走 WG/换路由器。

常用命令:

wg show                    # 握手/流量
wg show wg0 latest-handshakes
nft list ruleset | grep 51820   # 端口计数器(packets 涨=包到了)
tcpdump -i any -n 'udp port 51820'   # 抓 WG 握手包(区分包到没到)
top -d 2                   # 看 CPU 是否被 wg 加密占满(§7)
ip -6 route                # v6 路由
logread -e netifd          # 接口起不来必看
logread -e odhcp6c         # DHCPv6/前缀委派
dig AAAA router.example.com

11. 上线自检 checklist

  • [ ] dig AAAA router.example.com 与路由器 WAN GUA 一致
  • [ ] wan INPUT/FORWARD 默认 DROP,仅放行 51820/udp
  • [ ] wg0 接口已起,wg show 有 listen 端口
  • [ ] wg↔lan 双向 forwarding 存在
  • [ ] dnsmasq 监听 wg0,.lan 域名已定义
  • [ ] 每个客户端 peer 都加了 PSK
  • [ ] 各 OS 客户端能拨入且 ping nas.lan
  • [ ] 反向:家里能 ping 通手机隧道地址
  • [ ] 多站点:两站互 ping 对方 LAN 设备通
  • [ ] UPnP 已关;LuCI/SSH 不监听 WAN
  • [ ] root 强密码 + SSH key 登录
  • [ ] DNSPod 用子账号最小权限,凭证 chmod 600
  • [ ] IoT 在独立 VLAN 且 forward=REJECT
  • [ ] 配置已 sysupgrade -b 备份并离线保管

附录 A:客户端配置文件完整示例(手机)

[Interface]
PrivateKey = <手机私钥>
Address = 10.200.0.2/24, fd00:cafe:9::2/64
DNS = 10.200.0.1, fd00:cafe:9::1
MTU = 1420

[Peer]
PublicKey = <服务端 /etc/wireguard/server.pub>
PresharedKey = <psk.txt 内容>
Endpoint = router.example.com:51820
AllowedIPs = 192.168.1.0/24, fd00:cafe:1::/64, 10.200.0.0/24, fd00:cafe:9::/64
PersistentKeepalive = 25
注意:不要写 ListenPort(客户端禁忌,见 §3.1 坑1)。AllowedIPs 用半角逗号+空格分隔(见 §3.1 坑2)。内网测试(Endpoint=内网IP)时去掉 Endpoint 所在网段避免路由环(见 §3.1 坑3)。

附录 B:Windows RDP 目标机开启步骤

  1. 设置 → 系统 → 远程桌面 → 启用。
  2. Windows 防火墙:允许”远程桌面”入站,范围限制为 192.168.1.0/24(内网/VPN)。
  3. 设强密码账户,或禁用密码改用更安全方式。
  4. 客户端拨 VPN 后 mstsc /v:192.168.1.20

附录 C:DNSPod IPv6 DDNS 更新脚本(路由器,参考/核对用)

你的 DDNS 已配好则无需重做,此脚本用于核对逻辑或迁移。用 CAM 子账号的 SecretId/SecretKey。
#!/bin/sh
# /etc/wireguard/ddns_dnspod.sh
set -e
SECRET_ID='your_id'
SECRET_KEY='your_key'
DOMAIN='router.example.com'      # 完整主机名
RECORD_TYPE='AAAA'

# 取路由器 WAN 侧第一个非临时 GUA
IP=$(ip -6 addr show pppoe-wan 2>/dev/null | grep -v temporary \
     | awk '{print $2}' | cut -d/ -f1 | head -n1)
[ -z "$IP" ] && { echo "no GUA"; exit 1; }

# 调用 DNSPod v3 API(签名较繁琐,推荐用现成的 ddns-scripts)
# OpenWrt 推荐做法(apk,25.x;旧版用 opkg):
apk add ddns-scripts_dnspod
# 在 /etc/config/ddns 配置 dnspod 服务,service_name 选 dnspod

OpenWrt 推荐用官方 ddns-scripts_dnspod 包,在 /etc/config/ddns 里:

config service 'myddns'
    option service_name 'dnspod'
    option domain 'router.example.com'
    option username '<SecretId>'
    option password '<SecretKey>'
    option ip_source 'network'
    option ip_network 'wan6'
    option check_interval '5'
    option check_unit 'minutes'
    option force_interval '60'
    option force_unit 'minutes'
    option use_ipv6 '1'
/etc/init.d/ddns enable && /etc/init.d/ddns start
logread -e ddns    # 查看更新结果
DDNS 更新失败时要能告警(前缀变了但没更新 → VPN endpoint 失效)。可在脚本里加 webhook 通知。