家用宽带 IPv6 实践笔记:固定地址、临时地址、DDNS 与公网访问

我在给 PVE 虚拟机配置 IPv6、ImmortalWrt、OPNsense 和 Cloudflare DDNS 时,连续遇到了地址变化、AAAA 记录选址、端口不通和 IPv4-only 客户端无法访问等问题。这篇笔记按我的实际排查顺序记录:先判断地址为什么变化,再确认哪个地址适合对外服务,最后处理 DDNS、路由、防火墙和协议兼容。

公网前缀、域名和部分拓扑信息已脱敏。示例统一使用 2001:db8::/32 文档地址段,不可直接用于公网通信。

1. 我真正需要解决的不是“IPv6 会不会变”

最初看到同一张网卡上存在多个公网 IPv6,我把问题简单理解成了“一个固定、一个临时”。继续排查后发现,这种说法不够准确:完整 IPv6 由运营商前缀和主机接口标识符共同组成,任意一部分变化都会生成新地址;同一接口还可以同时保留稳定地址、临时地址、旧前缀地址和链路本地地址。

我遇到的现象主要有四类:

  • 路由器重拨后,虚拟机的完整 IPv6 改变,但部分地址后缀看起来没变。
  • 同一张网卡出现两个或更多全局 IPv6,不知道 DDNS 应该发布哪个。
  • Cloudflare AAAA 已经指向当前地址,SSH、面板或 Minecraft 仍连接失败。
  • ImmortalWrt 无法从邻居表获取 OPNsense 后方虚拟机的 IPv6 和 MAC。

排查后的核心结论可以压缩成下面几条:

结论 实际含义
家宽“固定 IPv6”通常不是完整地址永久不变 更多时候是 IID 稳定,或者依靠 DDNS 保持域名不变
临时地址主要用于出站连接 系统定期生成新地址,并逐步弃用、删除旧地址
服务器 AAAA 应指向稳定地址 地址必须为全局、首选、未过期,并排除 temporary
DDNS 只解决重新定位 它不会把运营商的动态前缀变成静态前缀
DNS 正确不等于服务可达 后面仍有客户端路由、上级防火墙、本机防火墙和服务监听

2. 我先把一个 IPv6 地址拆开看

IPv6 地址长度为 128 位。全局单播地址通常包含运营商提供的 Global Routing Prefix、站点划分的 Subnet ID,以及主机生成的 Interface ID(IID)。在普通 SLAAC /64 链路中,可以先按“64 位前缀 + 64 位 IID”理解:

|<--------------- 128 bit --------------->|
|         /64 前缀        |     64 位 IID   |
|2001:db8:100:800         |abcd:1234:5678:9abc|

RFC 4862 描述了 SLAAC 的基本过程:路由器通过 RA 通告前缀和生命周期,主机自行生成 IID,再将两者组合成地址。一个接口同时拥有多个 IPv6 属于正常设计,并不表示地址冲突。RFC 4862 RFC 4291

家用宽带前缀通常由 DHCPv6-PD 动态委派。PPPoE 重拨、光猫或主路由重启、租约更新、运营商调整分配策略、下级路由重新申请子前缀,都可能让前缀变化。即使 IID 完全不变,完整地址也会改变:

变更前:2001:db8:100:800:be24:11ff:fe1c:782a
变更后:2001:db8:101:800:be24:11ff:fe1c:782a
                         └──── IID 未变化 ────┘

因此,我不再把“固定 IPv6”当成单一概念,而是区分以下四种情况:

口语中的“固定” 实际含义 前缀变化后的结果
手工静态地址 管理员写死完整 128 位地址 旧前缀失效后通常不可达
EUI-64 稳定后缀 IID 由 MAC 推导 完整地址改变,IID 通常不变
RFC 7217 stable-privacy 同一接口、同一子网和前缀内稳定的匿名 IID 换前缀时 IID 也可能变化
DDNS 稳定域名 持续更新 AAAA 记录 域名不变,记录转向新地址

3. 我如何区分稳定地址和临时地址

3.1 先用 EUI-64 验证一个确定案例

我检查过一张 MAC 为 bc:24:11:1c:78:2a 的虚拟网卡。Modified EUI-64 会在 MAC 中间插入 ff:fe,并将首字节的 U/L 位取反:

原始 MAC:bc:24:11:1c:78:2a
插入 ff:fe:bc:24:11:ff:fe:1c:78:2a
首字节取反:0xbc XOR 0x02 = 0xbe
最终 IID:be24:11ff:fe1c:782a

因此,be24:11ff:fe1c:782a 可以用计算证明来自这张网卡的 MAC。RFC 4291 附录 A 说明了插入 ff:fe 和翻转 U/L 位的过程。RFC 4291 Appendix A

EUI-64 的好处是后缀容易预测,前缀变化时 IID 仍可保持不变;缺点是会把稳定的链路层标识嵌入 IPv6。RFC 8064 已推荐使用 RFC 7217 生成稳定 SLAAC 地址,并反对继续默认嵌入稳定的链路层地址。RFC 8064

另一张网卡的 MAC 是 bc:24:11:c1:1d:f0,按 EUI-64 计算应得到:

be24:11ff:fec1:1df0

但我实际看到的 IID 是 47cc:5694:5d11:4789。两者没有 EUI-64 对应关系,所以我不能仅凭它“看起来随机”就断言它是固定地址或临时地址;它可能来自 RFC 7217、RFC 8981、NetworkManager、systemd-networkd,也可能是手工配置的 Token。

3.2 RFC 7217 和 RFC 8981 解决的是不同问题

RFC 7217 使用前缀、接口、网络标识、计数器和本机秘密值等信息生成不可直接反推出 MAC 的 IID。它的稳定范围是“同一接口、同一子网、同一前缀”,不是跨所有网络永远不变;运营商前缀变化后,IID 也可能重新生成。RFC 7217

RFC 8981 临时地址则使用不可预测 IID,并设置较短的首选与有效生命周期。旧地址会先从 preferred(首选)变成 deprecated(弃用),不再用于新连接;valid_lft(有效生命周期)到期后才会被删除。RFC 8981 给出的默认上限为 TEMP_PREFERRED_LIFETIME=1 dayTEMP_VALID_LIFETIME=2 days,实际值还受系统配置和 RA 前缀生命周期限制。RFC 8981

RFC 6724 的默认源地址选择规则会在适用时优先使用临时地址,所以服务器访问外部网站时,对方看到的出口 IPv6 可能与服务器 AAAA 记录不同。RFC 6724

3.3 我最终只相信标志、生命周期和生成配置

下面这种输出说明稳定地址和临时地址同时存在:

inet6 2001:db8:100:800:be24:11ff:fe1c:782a/64 scope global dynamic mngtmpaddr
inet6 2001:db8:100:800:47cc:5694:5d11:4789/64 scope global temporary dynamic
inet6 fe80::be24:11ff:fe1c:782a/64 scope link
标志 我在排查时的理解
temporary 该地址是临时隐私地址
mngtmpaddr 该地址可作为生成临时地址的模板,不表示它本身是临时地址
dynamic 地址由动态机制管理,不等于马上会改变
deprecated 不再用于新连接,但尚未完全失效
tentative DAD 重复地址检测尚未完成
valid_lft 地址还可以存在多久
preferred_lft 地址还能作为新连接首选源地址多久

我在 Linux 上集中使用下面几组命令,不再按地址外观猜测:

# 查看网卡上的全局地址、标志和生命周期
ip -6 addr show dev ens18 scope global

# 查看访问外部 IPv6 时,内核准备选择的源地址
ip -6 route get 2606:4700:4700::1111

# 查看临时地址策略
sysctl net.ipv6.conf.ens18.use_tempaddr

# 查看 IID 生成模式
cat /proc/sys/net/ipv6/conf/ens18/addr_gen_mode

# 连续监控地址新增、弃用和删除
ip monitor address dev ens18

use_tempaddr(临时地址策略)中,<= 0 表示禁用,1 表示启用但偏向稳定地址,> 1 表示启用并优先选择临时地址。Linux 还提供 temp_prefered_lft(临时地址首选时长)和 temp_valid_lft(临时地址有效时长)。addr_gen_mode(地址生成模式)的常见值如下:Linux IPv6 sysctl

生成方式
0 使用 EUI-64
1 不自动生成链路本地地址;SLAAC 地址仍使用 EUI-64
2 使用已配置的 stable_secret 生成 RFC 7217 地址
3 未设置秘密值时自动生成随机秘密值,再生成稳定隐私地址

4. 我在服务器上采用的地址配置

排查时我曾看到 dhcp6-privacy: false 这种写法,但 Netplan 官方配置中没有 dhcp6-privacy。与这个问题直接相关的字段是:

参数 中文解释
ipv6-privacy IPv6 隐私扩展:控制是否生成并优先使用临时地址
ipv6-address-generation IPv6 地址生成:可选 eui64stable-privacy
ipv6-address-token IPv6 地址 Token:手工指定 SLAAC 使用的固定 IID
accept-ra 接受路由通告:接收默认路由、前缀和生命周期
dhcp6 启用 DHCPv6:是否真正获取地址仍取决于 RA 和网络侧策略

字段定义以 Netplan YAML 官方文档 为准。

我的 DDNS 客户端直接运行在目标虚拟机中,不需要依赖“前缀 + 固定后缀”拼接,因此优先使用 stable-privacy

network:
  version: 2
  renderer: networkd
  ethernets:
    ens18:
      dhcp4: true
      dhcp6: true
      accept-ra: true
      ipv6-privacy: false
      ipv6-address-generation: stable-privacy

renderer: networkd(网络配置后端:由 systemd-networkd 应用配置);dhcp6: true(启用 DHCPv6:不代表一定由 DHCPv6 分配地址);accept-ra: true(接受 RA:获取默认路由、前缀和生命周期);ipv6-privacy: false(关闭临时地址:减少服务器和 DDNS 选址歧义);ipv6-address-generation: stable-privacy(稳定隐私地址:不直接暴露 MAC)。

服务器并非必须关闭临时地址。也可以保留临时地址用于出站,同时让 DDNS 明确排除 temporary。我关闭它只是为了减少服务器上的地址歧义,不是获得稳定服务地址的唯一方法。

如果确实需要 IID 在前缀变化后仍保持固定,我会改用管理员指定的 Token,而不是暴露 MAC:

network:
  version: 2
  renderer: networkd
  ethernets:
    ens18:
      dhcp4: true
      dhcp6: true
      accept-ra: true
      ipv6-privacy: false
      ipv6-address-token: "::8a6f:2d91:4c30:7b10"

ipv6-address-tokenipv6-address-generation 互斥,不能同时配置;Token 还要确保同一 /64 内不重复。应用配置前,我会保留 PVE Console 等带外入口,再执行:

sudo netplan generate
sudo netplan try
sudo netplan apply
ip -6 addr show dev ens18

netplan generate(生成配置:检查语法和后端渲染);netplan try(试运行:连接中断时可回滚);netplan apply(正式应用:立即生效)。远程修改网络时直接跳过 netplan try,一旦 SSH 中断会增加恢复成本。

5. 我最终如何处理 DDNS

我给公开服务选择 AAAA 地址时,只接受同时满足以下条件的地址:scope global、仍处于 preferred、没有 temporary、属于实际业务网卡,并且上级路由和防火墙允许外部访问。fe80::/10 链路本地地址、ULA、Docker/Tunnel 地址、旧前缀地址和 deprecated 地址都不应被发布。

我最终更倾向于把 DDNS 客户端部署在目标虚拟机内,因为它能直接读取完整地址、标志和生命周期,不需要猜测下游 /64,也不会受 VLAN、OPNsense 路由边界和邻居表可见范围影响。即使 RFC 7217 在前缀改变时同时生成新 IID,本机 DDNS 仍能上报新的完整地址。Cloudflare 侧则使用仅具备指定 Zone DNS 编辑权限的 API Token,不在多台业务机上复用 Global API Key。

集中式获取并非完全不可用,但必须先确认网络边界:

ip -6 neigh show
ip -6 route get 2001:db8:100:800::1234

ip -6 neigh show 只能显示本机直连链路上的 NDP 邻居。虚拟机位于 OPNsense 后方、其他 VLAN 或其他三层网段时,ImmortalWrt 通常只能看到下一跳路由器,而不是目标虚拟机 MAC。邻居状态 STALE 只表示近期没有重新确认可达性,不表示 IPv6 已失效,所以不能把所有 STALE 项直接排除。

“提取前缀 + 拼接固定后缀”只有在控制器和目标机使用同一个 /64、目标 IID 明确跨前缀稳定、提取的是目标链路前缀且前缀长度确定为 /64 时才成立。运营商如果下发 /56/60,主路由会给不同 LAN/VLAN 划分不同 /64;拿上级接口前四组拼接下游 IID,可能得到一个语法正确但根本没有路由到目标机的地址。

ip -6 route get 的输出如果只有 dev br-lan,并且邻居表中能看到目标,通常表示它位于直连链路;如果输出包含 via <下一跳>,目标位于路由器后方,不能依赖本机邻居表获取其 MAC。

6. AAAA 正确但服务不通时,我按层排查

我把公网访问拆成五层:DNS 只负责把域名解析成地址;客户端必须有可用 IPv6 路由;主路由或 OPNsense 必须放行入站;虚拟机本机防火墙必须允许目标端口;应用最后还要真正监听 IPv6。前一层正常,不能证明后一层正常。

域名 AAAA
    ↓
客户端 IPv6 路由
    ↓
主路由/OPNsense 入站策略
    ↓
虚拟机本机防火墙
    ↓
应用 IPv6 监听

我通常一次执行下面几组命令:

# 1. DNS 是否指向目标机当前的稳定地址
dig AAAA host.example.com +short

# 2. 地址是否仍为首选、是否误用了临时或旧前缀地址
ip -6 addr show dev ens18 scope global

# 3. 服务是否监听 IPv6 和目标端口
ss -lntup
ss -lntup | grep ':22|:25565|:18091'

# 4. 先从本机回环测试服务
ssh -6 root@::1
curl -g -6 -v 'http://[::1]:18091/'

# 5. 从网卡抓取 SSH 入站报文
sudo tcpdump -ni ens18 'ip6 and tcp port 22'

监听结果中的 :::22 通常表示 SSH 正在 IPv6 通配地址上监听;0.0.0.0:22 只代表 IPv4,还要检查是否另有 :::22 或具体 IPv6 地址;完全没有目标端口则说明服务未启动、端口写错或绑定到了其他地址。本机回环都无法访问时,我会先修复应用监听,不继续折腾公网路由。

抓包可以快速划分责任范围:

抓包结果 我接下来检查的位置
完全没有 SYN 客户端 IPv6、运营商链路、上级路由或防火墙
收到 SYN,立即返回 RST 目标端口无监听,或防火墙主动拒绝
收到 SYN,没有响应 本机防火墙丢弃、策略路由或服务异常
完成三次握手后断开 应用协议、认证、TLS 或服务端配置

Ping 通也不能代替端口测试。ICMPv6 Echo、TCP 22、TCP 443 和游戏端口是不同流量;ITDog 能 Ping 通,只能说明部分 ICMPv6 和路由路径可用。反过来,我也不会为了“安全”关闭全部 ICMPv6,因为邻居发现、RA、错误通知和 Path MTU Discovery 都依赖 ICMPv6。RFC 4890 建议按消息类型和方向过滤,而不是照搬 IPv4 的粗暴禁用方式。RFC 4890

IPv6 地址后面携带端口时必须使用方括号,避免与地址内部的冒号混淆:

http://[2001:db8:100:800::10]:18091/
[2001:db8:100:800::10]:25565

7. IPv4-only 客户端和 Minecraft 的处理

只有 AAAA 记录不能让 IPv4-only 客户端访问 IPv6 服务器,必须增加一个同时具备 IPv4 和 IPv6 的中间层。我根据服务类型选择方案,而不是把所有需求都塞进 Cloudflare Tunnel:

方案 适用服务 主要限制
Cloudflare 橙云代理 HTTP/HTTPS 网站 只支持 Cloudflare 列出的 Web 端口
Cloudflare Tunnel Web、受控 SSH/RDP/TCP 非 HTTP 通常需要客户端侧 cloudflared 或受控客户端
双栈 VPS + FRP/socat/gost SSH、Minecraft、自定义 TCP/UDP 需要 VPS 和端口转发配置
Tailscale/ZeroTier/WARP 私网 个人管理、NAS、SSH 双方需要客户端,不适合任意公众访问
Cloudflare Spectrum 公开 TCP/UDP、部分 Minecraft 场景 协议能力和可用范围受套餐限制

Cloudflare 普通代理默认只处理文档列出的 HTTP/HTTPS 端口,其他公开 TCP/UDP 通常需要 Spectrum;Tunnel 的非 HTTP 服务通常要求客户端安装 cloudflared,不能直接当作无需客户端的免费 Minecraft 公网中转。Cloudflare 支持端口 Tunnel 协议说明 Spectrum 套餐协议

Minecraft Java Edition 可以直接使用 IPv6。出现 Connection refused: no further information 时,我先检查服务端口和监听,不会直接判断为“不支持 IPv6”。server.properties 保持:

server-ip=
server-port=25565

server-ip(监听地址:留空通常表示监听可用接口,避免误绑某个 IPv4);server-port(游戏端口:默认 TCP 25565)。服务端通过 ss -lntp | grep ':25565' 确认监听,客户端按 [2001:db8:100:800::10]:25565 输入地址。朋友的网络没有 IPv6 时,灰云 AAAA 无法解决兼容问题,只能增加双栈 VPS 转发、使用合适套餐的 Spectrum,或让固定成员通过 Tailscale/ZeroTier 进入私网。

8. ImmortalWrt、OPNsense 和级联 IPv6 的边界

排查旁路由时,我先确认它到底是“单臂旁路由”还是“真正的下级路由器”。这两个叫法在日常交流中经常混用,但 IPv6 配置完全不同:

拓扑 特征 配置重点
单臂旁路由 ImmortalWrt 与终端都在主路由 LAN,仅提供代理或 DNS 通常继续由主路由发送 RA,避免出现两个默认 IPv6 网关
下级路由器 终端位于 ImmortalWrt 的另一 LAN/VLAN 后方 上级需要通过 PD 委派子前缀,或受限使用 RA/NDP Relay

我优先采用原生路由:上级委派 /56/60 等前缀,下级再为每个 LAN/VLAN 分配独立 /64。上级无法委派时,才评估 RA Relay 或 NDP Proxy;NAT66 只作为兼容性兜底。RFC 4291 的基本模型是一个子网前缀对应一条链路,不能把同一个 /64 随意放到多个被路由器隔开的二层网络,再期待普通邻居发现自动跨越路由边界。RFC 4291

9. 最终配置与排错清单

我现在对动态前缀家宽中的 PVE 服务器采用以下组合:

  1. 通过 RA/SLAAC 正常获取前缀,不在虚拟机里写死运营商当前下发的完整地址。
  2. 服务地址使用 stable-privacy;确有跨前缀固定 IID 需求时,再配置 ipv6-address-token
  3. DDNS 优先运行在目标虚拟机内,并排除 temporarydeprecatedfe80::/10、ULA 和容器虚拟网卡地址。
  4. Cloudflare AAAA 使用最小权限 API Token 更新,不复用 Global API Key。
  5. 防火墙只开放实际服务端口,同时保留 IPv6 正常运行所需的 ICMPv6 类型。
  6. 地址不通时依次检查 DNS、地址状态、路由、防火墙、监听和抓包,不用 Ping 代替端口测试。
  7. IPv4-only 访问单独设计双栈代理或隧道,不与 DDNS 混为一谈。

最后保留一张误区对照表,方便以后排查时快速确认方向:

常见判断 修正后的理解
没有 ff:fe 就是临时地址 RFC 7217 稳定地址同样没有固定外观,要看 temporary 和生命周期
关闭隐私扩展就能写死 IPv6 只能停止生成临时地址,运营商前缀仍可能变化
RFC 7217 后缀永远不变 只保证同一接口、同一前缀/子网内稳定
DDNS 解析正确就应该能连 DNS 后面还有路由、防火墙和服务监听
邻居表能看到整个内网 MAC NDP 只覆盖直连链路,跨路由只能看到下一跳
STALE 表示 IPv6 失效 它是邻居可达性状态,不是地址生命周期
Tunnel 可以公开代理任意 TCP/UDP 非 HTTP 服务存在客户端要求、协议和套餐限制
IPv6 没有 NAT,所以不需要防火墙 公网可路由不等于允许入站,仍需有状态防火墙

10. 参考资料