Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

生产部署

本页浓缩了完整的 Hub + 双 Spoke 生产演练。详尽版本——含流量整形、网卡调优与基准测试—— 见仓库中的 docs/deployment.zh-CN.md。 可直接编辑的产物位于 deploy/subnetrad.servicenet.subnetra.subnetrad.plisthub.jsonspoke-a.jsonspoke-b.json)。

0. 组件

一次部署有一个 Hub(稳定的公网 UDP 端点)和一个或多个 Spoke(仅出站,常位于 NAT 之后)。它们运行相同的 subnetrad 守护进程与 subnetra 控制工具;区别在配置 (角色)。

1. 安装二进制

使用发布 tar 包或容器镜像。在裸主机上:

sudo install -m 0755 subnetrad subnetra /usr/local/bin/

2. 准备配置与密钥

config.json 放到服务期望的位置(单元使用 /etc/subnetra/config.json),属主 root、 权限 0600,因为它含有 PSK:

sudo install -d -m 0750 /etc/subnetra
sudo install -m 0600 hub.json /etc/subnetra/config.json
subnetrad --check --config /etc/subnetra/config.json
# subnetra v… (mtu=…, mode=raw_direct, local_id=…, peers=…) [config ok]

openssl rand -hex 32 为每条链路生成 PSK,并 每链路使用唯一 值。见 安全模型

3. 主机网络

守护进程打印——但绝不应用——主机规划。审阅并执行它(见 主机网络规划):

subnetrad --print-network-plan --config /etc/subnetra/config.json

4. 作为服务运行

Linux —— systemd

sudo install -m 0644 deploy/subnetrad.service /etc/systemd/system/subnetrad.service
sudo systemctl daemon-reload
sudo systemctl enable --now subnetrad
journalctl -u subnetrad -f

该单元只请求 CAP_NET_ADMIN,授予 /dev/net/tun,以 ExecStartPre 运行 subnetrad --check,失败时重启,其余均沙箱化(ProtectSystem=strictNoNewPrivileges、受限地址族)。编辑注释掉的 ExecStartPost 行以匹配你的 --print-network-plan 输出。

macOS —— launchd

macOS Spoke 作为系统守护进程运行(创建 utun 需要 root):

sudo install -m 0644 deploy/net.subnetra.subnetrad.plist \
    /Library/LaunchDaemons/net.subnetra.subnetrad.plist
sudo launchctl bootstrap system /Library/LaunchDaemons/net.subnetra.subnetrad.plist
sudo launchctl enable system/net.subnetra.subnetrad
sudo tail -f /var/log/subnetrad.log
# subnetra v… (… mode=raw_direct …) tun=utun4 sock=/var/run/subnetra.sock [ready]

utunN 名字由内核分配——从 [ready] 横幅读取它,并在守护进程起来 之后 应用规划。见 macOS Spoke 指南。

5. 安装中继策略(Hub)

捷径: 如果你的配置设置了 "role": "hub" / "spoke",守护进程会 在启动时推导出 整套策略,本节可跳过。见 角色

对于 role=manual,在运行时通过控制套接字安装中继/投递规则(热更新、无需重启)。在 Linux 上 CLI 默认值已与守护进程一致,无需设置 SUBNETRA_SOCK(仅自定义路径时才设置):

# Hub:把叠加网流量中继给正确的 Spoke
sudo -E subnetra policy add --src 0.0.0.0/0 --dst 10.0.0.2/32 --action forward --target 2
sudo -E subnetra policy add --src 0.0.0.0/0 --dst 10.0.0.3/32 --action forward --target 3
sudo -E subnetra policy show
sudo -E subnetra save        # 持久化一份可重放的快照

# Spoke:把发往本地叠加地址的隧道流量投递到本地 TUN(target 0)
sudo -E subnetra policy add --src 0.0.0.0/0 --dst 10.0.0.2/32 --action forward --target 0

6. 运维

subnetra status 显示对端、流量与按原因分类的 drop;--json 是供监控使用的稳定 schema。 见 可观测性与排障

7. 防火墙 / NAT

  • Hub 必须接受来自互联网、发往全部已配置 listen_ports 的入站 UDP。默认值是显式端口集 18020, 18023, 18026(不是范围),避开 WireGuard 知名端口指纹;单个端口被封锁或限速也 不会让节点离线。
  • 每个 Spoke 只需要对 Hub 的 出站 UDP 可达性——无需入站端口转发(由 Spoke 发起)。 因此 Spoke 只需 一个 监听端口(按角色而定的默认值只绑定 [18020]);多个 listen_ports 是 Hub 一侧的特性,因为 Hub 才是被 Spoke 主动连接的可达端点。
  • 若某 Spoke 的 NAT 映射变化,Hub 会从下一个已认证数据报重新学习它的新 endpoint。保持 Hub 端点稳定;Spoke 始终发起。

NAT 保活(内置)

空闲 Spoke 的 NAT 映射会超时(UDP 常约 ~30 秒),之后入站中继会被黑洞。role=spoke 默认 运行 内置保活keepalive_secs = 20):每隔一段时间一个极小的已认证数据报保持 NAT 孔 打开,并保持 Hub 学到的 endpoint 新鲜。它零分配,不增加线程或外部进程。用 keepalive tx / keepalive rx 计数确认。设 keepalive_secs = 0 关闭(例如不在 NAT 后的 Spoke)。

Hub 使用动态 IP(DDNS)

endpoint 是数字 IP:port,且端点学习是单向的——Spoke 无法发现搬了家的 Hub。请优先为 Hub 使用 稳定公网 IP。若必须让它位于动态地址之后,在每个 Spoke 上用一个小型 DDNS 监视器解决 :重写 endpoint 并重启(无状态的)守护进程——无需改动守护进程。

Hub 位于 NAT 之后(静态端口映射)

Hub 不需要是一台公网 IP 主机——它需要的是一个稳定、可入站到达的 UDP 端点。如果边缘路由器 有一组从固定公网 UDP 端口到 Hub 内网 IP:listen_ports静态端口映射(DNAT),那么 位于 NAT 之后的主机同样符合条件:

  • Spoke 拨向_外部_地址。 每个 Spoke 的对端 endpoint 是该映射中的一个公网 IP:port(通常是主端口 :18020),而非 Hub 的私网地址。
  • listen_ports 是_内部_目标。 为每个已配置 UDP 端口做转发(默认是公网 18020/18023/18026 → 内网 18020/18023/18026)。单数 listen_port 仍是单端口部署的 向后兼容别名;当 listen_ports 存在时会被忽略。
  • 映射必须是静态的。 是固定端口映射,而非按流改写源端口的动态 PAT。若公网 IP 本身也会变, 请与上面的 DDNS 方案结合使用。
  • 同一局域网内的 Spoke 需用内部端点(hairpin)。 处于同一 NAT 内的 Spoke 通常无法经由 公网 IP 到达 Hub,除非路由器支持 NAT 发夹/回环——请给这些 Spoke 配置 Hub 的内网 IP:port(主端口)。
  • CGNAT 无法承载 Hub。 若“公网“地址本身是运营商级 NAT、没有入站端口控制,你就无法向它做 端口映射;这样的主机只能当 Spoke。

这仍然是一个普通的单 Hub——只是其可达性经由 DNAT——因此仍处于已验证的单层模型之内。端点 学习依旧是单向的:保持外部映射稳定;始终由 Spoke 发起。

8. 高可用

v1 按设计 单 Hub。数据面是单路径、无状态、无握手的,守护进程 从不探测对端健康、也不自动 切换路径(一条明确的非目标)。因此多 Hub 与故障 切换都构建在守护进程 之外,用普通的配置 + 操作系统工具实现,并由 subnetra status --json 中 的 仅观测 健康信号驱动(onlinelast_seen_age_seconds、一个平直的 auth_or_invalid)。 官方认可两种方案。

方案 A —— active/standby Hub VIP(推荐)

两台 Hub 主机置于一个 VRRP/keepalived VIP(或 anycast 前缀)之后,二者共享 完全相同 的配 置——相同 local_id、相同的每 Spoke PSK、相同策略——因此每个 Spoke 只看到 一个 对端 (VIP),无需任何特殊配置:一个普通的 role=spoke,把 VIP 作为它唯一的 Hub。

flowchart LR
    subgraph Spokes
        S2["Spoke · 10.0.0.2"]
        S3["Spoke · 10.0.0.3"]
    end
    VIP(["Hub VIP · 203.0.113.10:18020"])
    subgraph HubPair["Hub 对 · 共享 local_id + PSK"]
        HA["hub-a · ACTIVE"]
        HB["hub-b · standby"]
    end
    S2 <-->|"加密 UDP"| VIP
    S3 <-->|"加密 UDP"| VIP
    VIP --- HA
    VIP -. 接管 .- HB

每台 Hub 上的最小 keepalived,用 notify 钩子在接管时 重启 守护进程:

vrrp_instance subnetra {
    state BACKUP            # 两台都 BACKUP + nopreempt,避免无谓抖动
    interface eth0
    virtual_router_id 51
    priority 150            # hub-a 用 150,hub-b 用 100
    nopreempt
    advert_int 1
    virtual_ipaddress { 203.0.113.10/32 }
    notify_master "/usr/local/sbin/subnetra-takeover.sh"
}
# /usr/local/sbin/subnetra-takeover.sh —— 本机抢到 VIP 时执行。
#!/bin/sh
# 重启,使守护进程采样到一个高于旧 active 的全新 boot epoch(见下)。
systemctl restart subnetrad

两条注意事项是关键:

  • epoch 顺序(头号坑)。 每个数据报都携带发送方的 boot epoch(启动时的墙钟纳秒),且接收 方是 仅向前 的:epoch 比 Spoke 已接受的 更低 的会话,会在 进入加密前 被丢弃,直到墙钟 越过它。一个长时间空闲、且启动早于 active 的 standby 会呈现 更低 的 epoch,从而被静默黑洞。 缓解:让 两台 Hub 都用 NTP,并在 接管时重启守护进程(即 notify_master 钩子),让它打上 一个全新的、更高的 epoch。这正是此处 active/standby 优于 active/active 的原因。
  • 端点重学习窗口。 端点学习是单向的,因此新的 active 起步时 没有 任何已学习的 Spoke 端点: Hub→Spoke 的 中继 会黑洞,直到每个 Spoke 的下一次 keepalive 重新教会它(Spoke→Hub 立即可用 ——由 Spoke 主动发起)。恢复时间以 keepalive_secs(Spoke 默认 20)为上界;在 Spoke 上调小它 可加快切换。

方案 B —— 静态双 Hub(独立身份)

两个 完全独立 的 Hub——local_id 不同、PSK 不同、无共享密钥、无 epoch 耦合——都中继同一个 overlay。每个 Spoke 把 两者 都列为对端并保留一个主用,靠改 Spoke 的策略来切换。这同时带来 就近性:把区域前缀指向区域内 Hub,只有跨区域目的地才走长途链路(每个区域 Hub 都必须承载你希 望就近可达的那些 Spoke)。

由于 role=spoke 校验 恰好一个 Hub 对端,双 Hub 的 Spoke 改用 role=manual,并通过控制套接字 安装自己的最长前缀策略:

# 主路径:整个 overlay 走 hub-1(id 1);本机做本地投递。
sudo -E subnetra policy add --src 0.0.0.0/0 --dst 10.0.0.0/24 --action forward --target 1
sudo -E subnetra policy add --src 0.0.0.0/0 --dst 10.0.0.5/32 --action forward --target 0
sudo -E subnetra policy show
sudo -E subnetra save

# 把 overlay 切到 hub-2(id 2):两条 /25 比 /24 更具体(最长前缀胜出),
# 从而在不触碰 /24 规则的情况下把全部 overlay 流量改道。
sudo -E subnetra policy add --src 0.0.0.0/0 --dst 10.0.0.0/25   --action forward --target 2
sudo -E subnetra policy add --src 0.0.0.0/0 --dst 10.0.0.128/25 --action forward --target 2

做负载拆分时,让两个 Hub 的前缀 互不重叠——这与 role=huballowed_src 的约束是同一条纪律 ——以避免脑裂(两个中继争抢同一目的地)。

没有在线的 policy replace 控制套接字是 只追加 的(policy add / policy show / save——没有 replacedelclear),而最长前缀只允许 更具体 的规则胜出。因此要 移动 一个前缀,你要么 (a) 用更新后的配置/快照 重启 Spoke 守护进程——它是无状态的,重启只损耗一个 keepalive 重学习窗口——要么 (b) 像上面那样推一条更具体的 覆盖 规则(表会变大;干净回退仍需重 启)。把拆分设计成 基本静态;不要把方案 B 当作亚秒级故障切换。

如何选择

方案 A —— VIP方案 B —— 静态双 Hub
目标可用性(一个逻辑 Hub)就近性 + 可用性(两个区域)
Spoke 配置不变(role=spoke,单对端)role=manual,两个 Hub,手动策略
Hub 身份共享 local_id + PSK不同 local_id + PSK
切换触发网络层(VRRP),秒级运维重启 / 前缀覆盖
主要坑epoch 顺序 + 重学习窗口无在线 replace;拆分要静态
Anycast可行,但比 VRRP 更冒险(中途 POP 迁移会扰动端点学习)不适用

密钥材料提示(方案 A)。 共享 local_id + PSK 会把相同的密钥放到两台机器上——请用同等标准 保护两者;任一被攻陷即等于所有 Spoke 链路被攻陷。两种方案中,守护进程自身都 不做 任何切换 决策。

9. 流量整形与调优

在跨 ISP 的长链路上,抖动/丢包的主因是 承载,而非检测。所有整形都在 OS 层用 tc 完成 ——不改守护进程或协议。把出站限到链路 稳定 吞吐的约 60–80%,平滑突发,并(可选)调优 套接字缓冲与 IRQ/CPU 亲和。内核在明文 snr0 设备上看到真实的内层五元组。完整配方与活叠加 基准见 docs/deployment.zh-CN.md §9–§10