←Back to Home
•homelab

家庭双栈网络与 HomeLab 重构思路:从物理虚拟化到“一截管一截”的解耦架构

梳理家庭网络重构背后的核心设计思路与避坑逻辑:从 PVE 硬件直通的 VFIO 内存锁死、纯 IPv6 路由直通、动态前缀门禁,到 Docker 的“一截管一截”分层治理与 WireGuard 漫游。

在折腾家庭网络和 HomeLab 时,许多人容易陷入“规则与插件越多越好”的误区,防火墙、端口映射与双栈策略交织在一起,一旦出现网络波动便无从排查。

这篇记录一次家庭网络重构。我把底层内存、IPv6 入站、动态前缀、Docker 出站和 WireGuard 漫游拆开来看,重点写实际遇到的坑。


一、核心架构与设计总览

在搭建软路由与虚拟化服务时,最棘手的问题往往是组件之间的过度耦合:调整一处防火墙,可能导致远程访问失效;修改一个 DNS 规则,又引发手机端加载异常。

理顺这套网络的核心原则其实非常清晰:“各司其职,一截管一截”。

家庭双栈网络与 HomeLab 整体解耦架构拓扑

整套体系由内向外清晰划分为五个设计层面:

  1. 底座层:硬件直通环境下的物理内存定额思路;
  2. 入站层:为什么全面弃用 IPv4 端口映射,转向纯 IPv6 路由直通;
  3. 门禁层:在家用动态 IPv6 前缀下,如何放行防火墙并实现轻量 DDNS;
  4. 业务与出站层:Docker 内部坚持纯 IPv4,如何无缝承接外部 IPv6 与出境透明加速;
  5. 漫游层:WireGuard 专线接入与非对称回程黑洞的排查思路。

二、物理底座:硬件直通下的内存分配逻辑

在使用 PVE 搭建小主机时,面对 16GB 物理内存,许多人的第一直觉往往是:“我主要就跑一个 Docker 虚拟机,不如直接给它分配 12GB 甚至 14GB,把物理性能吃满。”

但如果该虚拟机开启了 PCI 硬件直通(例如将 Intel 核显 SR-IOV 虚拟切片直通给 Docker 用于视频硬解),这种看似合理的分配往往会导致宿主机频繁耗尽内存、Swap 暴涨甚至系统假死。

为什么硬件直通会“锁死”物理内存?

普通虚拟机之所以可以超额分配内存,依赖的是内存气球机制(VirtIO Ballooning)。即使分配了 12GB,虚拟机内部若只占用 2GB,宿主机便可以动态回收并借调剩余的 10GB。

但一旦引入 PCIe 硬件直通,底层逻辑就发生了根本改变:

  • 直通设备(如核显)具备 DMA(直接内存访问) 能力,它需要直接读写内存物理地址,不再经过宿主机的 CPU 调度中转;
  • Linux 内核的 VFIO 驱动为了防止设备读写内存时出现地址漂移、引发硬件级总线崩溃,强制要求在虚拟机启动时,将其配置的内存 100% 钉死(Pin)在物理内存中;
  • 这意味着:内存气球机制完全失效。配置了 12GB,宿主机物理内存就会被硬性锁定 12GB,绝不释放。
[普通虚拟机 (无 PCI 直通)]
分配 12GB ───► 动态按需 / 气球调配 ───► 物理实占 ~2GB (闲置可借)
[PCI / GPU 直通虚拟机 (VFIO)]
分配 12GB ───► VFIO 强制 100% 物理锁死 ───► 物理实占 12.5GB (固定钉死,绝不可借)

设计思路:按需定额,留足余量

算一下宿主机的基本内存消耗:

  • PVE 宿主机系统底层常驻底噪约 1.5GB ~ 2GB;
  • 两个软路由虚拟机各占 1GB,合计 2GB;
  • 若再给 Docker 虚拟机锁死 12GB:12 + 2 + 2 = 16GB,物理内存瞬间告罄。宿主机被迫将关键系统进程换出到硬盘 Swap,整机 I/O 延迟剧增。

优化思路:检查 Docker 内部的真实负载——即使运行了 MySQL、PostgreSQL、Caddy、各类管理面板等 17 个容器,日常内存占用通常在 1.8GB ~ 2.5GB。后来我把 Docker 虚拟机下调到 8GB,内部仍有 5GB 以上余量,PVE 宿主机也不再频繁使用 Swap。

虚拟机内安装并运行 qemu-guest-agent 后,通过 PVE 界面点击关机时,系统会触发标准的内核平滑关机信号。Systemd 会遵循逆向依赖,预留安全宽限期通知 Docker 容器完成数据刷盘保存,随后才卸载存储卷,确保数据库文件安全。


三、外部入站:为什么坚决弃用 IPv4 端口映射?

在传统的家庭网络组网中,最常见的做法是在主路由上为内网服务器配置 IPv4 端口映射(DNAT)。外网能打开网页,就以为组网已经大功告成。

但经过深入梳理后,我们选择彻底关停主路由上的所有 IPv4 端口映射,将入站请求完全收敛至纯 IPv6。

为什么不再使用 IPv4 端口映射?

  1. 公网 IPv4 的不确定性与扫描风险:家用宽带公网 IPv4 的 80/443 端口长期被运营商封锁,开在动态 IPv4 上的高位端口(如 5443)容易遭受全网自动化脚本的高频探测扫描;此外,运营商随时可能收回公网 IPv4 并转换为大内网(CGNAT),映射链路面临不可控的中断风险。
  2. 双栈 Happy Eyeballs 的超时陷阱:现代移动端系统(iOS / Android 5G)在域名同时解析到 A(IPv4)和 AAAA(IPv6)记录时,会严格遵从 RFC 8305 算法优先尝试 IPv6 握手。如果主路由的 IPv6 通信受阻,客户端会等待 10~20 秒直至握手超时,才会回退至 IPv4,导致严重的访问卡顿。
  3. NAT 性能开销:IPv4 端口映射是典型的 DNAT(改写目的地址),路由器必须拆解每个数据包、重写 IP 报头、重新校验,并在内核中维护庞大的连接跟踪表(Conntrack);而 IPv6 在这里采用纯三层路由转发,路由器只根据规则放行,数据包不需要做 IPv4 目的地址转换。
比较维度 IPv4 端口映射 (NAT) IPv6 纯路由放行 (Forwarding)
地址分配机制 全家设备共享 1 个公网 IPv4 每台设备拥有全球唯一的公网 IPv6
路由器动作 拆包改头:改写目的 IP 为内网私网地址 核对放行:报头原封不动,符合规则直接开门
性能损耗 占用 CPU 计算资源,依赖 Conntrack 跟踪表 线速路由转发,无 NAT 转换开销
安全机制 容易遭受全网 IPv4 空间自动化扫描 IPv6 地址空间很大,随机扫描成本更高,但仍然需要防火墙规则保护

四、动态前缀门禁:防火墙放行与轻量 DDNS 思路

采用纯 IPv6 路由直通后,必须解决家用宽带的核心特性:宽带每隔数天重新拨号,IPv6 前缀(前 64 位)持续变动,防火墙如何精准放行?域名如何稳定追踪该设备?

为什么防火墙无法按 MAC 地址进行放行?

一个常见的疑问是:“既然 IP 会变,为什么不能在主路由防火墙中直接绑定内网主机的 MAC 地址?”

这在三层路由逻辑上是不成立的:

  • 外部公网数据包到达路由器 WAN 口时,二层链路封装的是运营商光猫或上级网关的 MAC,数据帧中根本不存在局域网服务器的物理 MAC;
  • Linux 内核在进行三层路由转发决策(Forward 链)时,数据包尚未进入内网二层封装阶段。在 OpenWrt 防火墙(fw4 / nftables)中指定 dest_mac 会直接被内核忽略。

解决方案:EUI-64 后缀掩码

IPv6 协议在无状态自动配置(SLAAC)中引入了 EUI-64 规范:它通过对网卡物理 MAC 地址进行固定映射(中间插入 ff:fe 并反转 U/L 位),生成固定的后 64 位接口 ID:

  • 网卡物理 MAC:bc:24:11:xx:xx:71
  • 对应 IPv6 后缀:::be24:11ff:fexx:xx71

无论运营商分配的前缀(前 64 位)如何变动,这串后 64 位接口 ID 保持恒定。

我们在主路由防火墙中只需配置一条后缀掩码匹配规则:

目的 IP 匹配:::be24:11ff:fexx:xx71 / ::ffff:ffff:ffff:ffff
放行端口:5443 (TCP)

底层编译生成的 nftables 规则利用位与运算,只校验报文目的 IP 的后 64 位是否匹配该设备。无论宽带如何重拨,防火墙规则无需任何更新,始终精准放行。

关于 EUI-64 隐私与固定后缀的思考:业内常有“EUI-64 会泄露隐私”的说法。这主要针对手机、笔记本等个人移动终端:如果后 64 位固定,设备在不同 Wi-Fi 间切换时容易被第三方跨网络追踪硬件身份(因此移动端默认使用 RFC 4941 随机临时后缀)。但对于局域网固定服务器而言,原则恰好相反:后 64 位必须固定。若后缀频繁随机变动,防火墙掩码与 DNS 解析将完全无法定位。若不希望透露网卡物理特征,也可通过 IPv6 Token 自定义固定后缀(如 ::100),核心原则依然是保持后缀稳定。

轻量 DDNS 探针:主路由直查 NDP 邻居表

防火墙门禁放通后,还需要将该设备变动的全球单播 IPv6 实时同步给 Cloudflare。

传统做法是在 Docker 虚拟机中单独常驻 DDNS 客户端,但这增加了虚拟机的维护成本。更轻量的设计是由主路由主动获取并推送:

  • 主路由内核维护着局域网的 NDP(邻居发现协议)邻居表,记录了内网各设备 MAC 与其实际获取到的公网 IPv6 的映射关系;
  • 在主路由编写探针脚本 /etc/get_docker_ipv6.sh:先 ping 目标主机的链路本地地址刷新 NDP 缓存,随后通过 ip -6 neigh 根据 MAC 提取出其全球公网 IPv6 直接返回;
  • OpenWrt 内置的 ddns-scripts 直接调用该脚本作为 IP 来源,自动更新 Cloudflare AAAA 记录。

DDNS 配置排坑重点:

  1. OpenWrt 中的 cloudflare.com-v4 代表 Cloudflare API 第 4 版协议,而非仅支持 IPv4,其完全支持推送 AAAA 记录;
  2. Cloudflare 解析列表中不得残留 :: 等空占位记录,否则脚本内置正则校验会将其判定为无效 IP,导致 DDNS 陷入重试循环;
  3. 泛域名解析(如 *.myhome.xyz)在配置检测域名(lookup_host)时,必须填写具体子域名(如 dashy.myhome.xyz)。DNS RFC 规范中通配符仅匹配子域名而不匹配根域名,若填写根域名会导致检测落空。

五、业务与出站解耦:“网络就是一截管一截”

在打通外网 IPv6 访问后,很多人的第一个疑问是:
“我的 Docker 容器根本没有开启 IPv6 网络(桥接网卡为纯私网 IPv4 172.18.0.x),为什么外部通过 IPv6 能够一路顺畅访问内部容器?”

这正是网络分层代理与解耦的典型体现。

【入站解耦流程】
外部手机 5G ──► 宿主机端口 [::]:5443 ──► Caddy 反代容器 172.18.0.8:443 ──► 内部业务容器 172.18.0.x
(纯公网 IPv6) (docker-proxy 转换) (纯内网私网 IPv4) (纯内网私网 IPv4)
【出站解耦流程】
内部业务容器 ──► 宿主机 tun0 虚拟网卡 ──► VLESS 加密专线跨洋 ──► 海外落地机 (DMIT) ──► OpenAI / ChatGPT
(纯内网私网 IPv4) (sing-box 透明分流) (出境加密专线) (原生纯净海外 IPv6) (海外接口握手)

入站解耦:docker-proxy 的承接机制

查看宿主机的端口监听进程,底层转换机制一目了然:

docker-proxy -proto tcp -host-ip :: -host-port 5443 -container-ip 172.18.0.8 -container-port 443
  • 外部网络 ➡️ 宿主机:宿主机在外部网卡监听 [::]:5443,完整支持公网 IPv6 接入;
  • 宿主机 ➡️ 容器内部:连接建立后,docker-proxy 负责将数据包转交至容器私网 IPv4(172.18.0.8)的 Caddy 服务;
  • 容器之间:Caddy 与各后端业务容器统一处于私网 IPv4 网桥中,按应用域名直接转发。

容器网络完全不需要感知外部是 IPv4 还是 IPv6,它只需专心处理送到本地私网端口的 HTTP 请求。

出站解耦:以 cli-proxy-api 连 ChatGPT 为例

出站通信遵循同样的解耦原则。我们在梳理业务配置时,曾遇到一个典型场景:

  • 一个需要频繁向 OpenAI 接口发起请求的容器 cli-proxy-api,其历史配置中保留着:proxy-url: "http://192.168.2.2:7890"(早期指向旁路由的代理端口);
  • 而实际上,Docker 宿主机自身已经配置了 sing-box TUN 模式,宿主机本身即是透明出境网关。

关键认知纠偏:

  1. 容器无需配置应用层代理:直接将 proxy-url 清空,容器发出的请求自然流经宿主机内核网络栈;
  2. 容器不需要原生 IPv6 连 ChatGPT:OpenAI 严格封控中国大陆 IP,若容器直接使用家庭公网 IPv6 发起连接,会被服务商直接拒绝;
  3. 真实的数据链路传递:容器发起私网请求,被宿主机 tun0 透明捕获,分流进入加密专线跨洋传输至海外落地 VPS(如 DMIT)。最终与 OpenAI 接口建立 TLS 握手的,是海外落地机上纯净原生的海外公网 IPv6;
  4. 结论:容器自身的网络栈开不开 IPv6 并不重要,出境访问协议与质量完全由边界网关与落地节点保障。

那么,Docker 容器到底何时才需要开启 IPv6?

梳理清楚各层职责后,容器是否需要开启 IPv6 变得非常直观:

业务场景 建议方案 架构考量
Web 反代服务(Caddy / Nginx) 纯 IPv4 宿主机端口由 docker-proxy 承接外部 IPv6,内部纯 IPv4 最为简洁稳定
出海 API 业务(如 ChatGPT 调用) 纯 IPv4 流量经本地透明网关拦截,最终由海外落地机的 IPv6 接口出境
内部数据库 / 缓存中间件 纯 IPv4 纯局域网调用,无需将其暴露到公网 IPv6 地址空间中增加风险
BT / P2P 下载容器(qBittorrent 等) 建议开启 需主动与全球海量拥有公网 IPv6 的 Peer 建立双向直连以提升传输速率
分布式对等网络(IPFS / 区块链节点) 建议开启 依赖全球公网 IPv6 供其他对等节点主动发现与通信

对于大多数常规 Web 与业务容器而言,“外部 IPv6 接入,内部纯 IPv4 运行,网关透明出境” 是维护成本最低、攻击面最小的设计模式。


六、漫游专线:WireGuard 4-in-6 与非对称路由排查

除了通过 5443 端口暴露公共 Web 服务外,对于 PVE 管理面板、SSH 等敏感管理入口,我们使用 WireGuard 纯单栈隧道 进行安全接入。

4-in-6 隧道设计与客户端策略

  • 外层(Underlay):客户端在移动网络下通过纯公网 IPv6 连接旁路由的 WireGuard 监听端口(UDP 35265);
  • 内层(Overlay):建立隧道后,虚拟网卡同时分配私网 IPv4(10.10.2.x)与私网 IPv6(fd10:10:2::x),进门单栈,内部双栈互联;
  • 客户端避坑:部分移动端(如 iOS)在域名同时存在 A 和 AAAA 记录时可能优先尝试 IPv4 握手。由于主路由已关停 IPv4 端口映射,会导致连接受阻。通过为 WireGuard 分配专用的纯 AAAA 记录二级域名(如 wg.myhome.xyz),可彻底避免回退等待。

“能 ping 通内网 IP,却无法加载网页”:排查非对称路由

在完成初步配置后,曾遇到一个典型故障:连上 WireGuard 后可以稳定 ping 通内网各主机 IP,但通过浏览器访问 Docker 上的 Web 服务时始终连接超时。

抓包分析表明,这是典型的局域网非对称路由(回程黑洞):

[进站] 远程手机 (fd10:10:2::x) ──► 旁路由 WireGuard ──► Docker 宿主机 (192.168.2.99)
│
(准备回包应答)
│
[回程] 丢弃黑洞 ◄─── (无法识别 fd10 私网路由) ◄─── 主路由网关 (192.168.2.1) ◄┘
  1. 客户端请求经由旁路由(192.168.2.2)转发至内网 Docker 主机(192.168.2.99);
  2. Docker 主机接收到请求并准备回包,发现源地址为 fd10:10:2::x。因主机未配置该私网网段的明细静态路由,数据包按默认网关被直接送交主路由(192.168.2.1);
  3. 主路由无法识别该 VPN 内部虚拟网段,直接丢弃报文,形成单向通信中断;
  4. ICMP ping 包较小且无状态校验,有时表面连通;但 TCP 握手与大报文交互因无法原路应答而彻底阻塞。

解决思路:在旁路由的防火墙规则中启用 IPv6 动态伪装(NAT66 / Masquerade):

Terminal window
uci set firewall.lan.masq6='1'
uci commit firewall
fw4 reload

当流量离开旁路由进入局域网前,源 IP 被重写为旁路由自身的局域网 IPv6 地址。Docker 主机识别为旁路由发来的通信,回包自然原路返回旁路由,旁路由再封装送回客户端,路由实现对称闭环。

在调试防火墙规则时,若误将“地址族限制”设为仅允许 IPv6,可能导致旁路由所有 IPv4 端口阻断、SSH 无法登入。此时无需外接显示器,在 PVE 宿主机终端中使用 qm sendkey 直接向虚拟控制台注入按键命令:

Terminal window
for k in f w 4 spc s t o p ret; do qm sendkey 102 $k; sleep 0.05; done

虚拟机会按物理键盘输入顺序执行 fw4 stop,防火墙当即停止,局域网连接即刻恢复。


七、总结与设计清单

这次重构后,我把检查清单收敛成下面几条:

  1. 底座层:硬件直通必定导致 VFIO 内存全额锁死,摒弃超售心理,按真实容器负载合理定额,保障宿主机稳定;
  2. 入站层:关闭易被扫描且受制于 CGNAT 的 IPv4 端口映射,转向纯 IPv6 路由直通,消除移动端双栈握手等待;
  3. 门禁层:把握服务器接口后缀必须稳定的原则,通过 EUI-64 掩码抵御动态前缀重拨,利用主路由 NDP 邻居表实现轻量探针 DDNS;
  4. 业务层:容器内部坚持纯 IPv4 极简分发,入站由 docker-proxy 完成 IPv6 到 IPv4 转换,出境由宿主机网关透明分流至海外纯净出口,上下游充分解耦;
  5. 专线层:单栈 IPv6 承载双栈漫游通道,遇到“ping 通却无法打开网页”的症状,优先定位局域网非对称路由并用 NAT66 修复回程。

这样做的好处是,故障出现时可以先定位在哪一层,不必在所有规则里来回猜。