角色
与其手工注入 subnetra policy add 规则,不如设置一个 role,让守护进程在启动时推导
转发表。共有三种角色;role 默认为 "manual"。
| 角色 | 推导策略? | 典型节点 |
|---|---|---|
manual | 否(初始策略为空——自行注入规则) | 自定义场景、向后兼容 |
spoke | 是——本地目标 + 其余一切经 Hub | 分支办公室、RouterOS 容器、Mac |
hub | 是——为每个 Spoke 的 allowed_src 生成一条转发规则 | 中心中继 |
你随时可以在运行时把额外的 subnetra policy 规则叠加到推导出的表之上。
manual(默认)
manual 是原始的显式模式,也是默认值。守护进程在启动时不推导任何策略——转发表从
空开始,你通过控制套接字自行安装每一条规则。早于角色特性的既有配置照常工作、不受影响。
manual 相对推导角色改变了什么:
- 不推导策略。 你用
subnetra policy add自行构建转发表。 - 没有角色专属的
--check。subnetrad --check仍会跑通用合理性检查(MTU 范围、 16 位 id、与主机子网重叠),但不会施加hub/spoke的结构性规则(每对端的allowed_src、恰好一个 Hub、一个本地目标、无0.0.0.0/0本地路由)。配错的转发意图 要你自己发现。 - 保活默认为
0。 若一个manual节点位于 NAT 之后,请自行设置keepalive_secs(spoke会替你处理)。
manual 没有 改变什么——安全性完全一致。 角色只选择启动期策略,绝不触及数据面。
每链路加密、会话 epoch 排序、抗重放,以及——关键的——每对端 allowed_src 内层源校验
全都照旧运行。策略仅按目的地匹配(最长前缀);每个对端的 allowed_src 独立地约束该对端
可声称的内层源地址。因此手工构建的 manual 表无法被诱骗去接受伪造的内层源——你放弃的
是推导出来的便捷表与角色专属护栏,而非密码学保证。
何时使用 manual
hub/spoke形态在单个节点上无法表达的拓扑——例如一个节点同时是对上的 Spoke、对下的 中继(hub/spoke各自只校验一种姿态;manual让一个节点兼具两者)。这超出了推导 角色所验证的单层模型,因此转发表——以及上游 Hub 的allowed_src聚合——由你负责。- 逐字复现一张手调的策略表,或与早于角色的配置向后兼容。
手工构建转发表
规则按目的地最长前缀匹配;src 取宽松值(0.0.0.0/0)。--target 0 投递到本地 TUN,
其它任何 target 则中继给该对端 id:
# 在 Linux 上 CLI 默认值已与守护进程一致,无需设置 SUBNETRA_SOCK。
# 把本节点自身的叠加地址本地投递。
sudo subnetra policy add --src 0.0.0.0/0 --dst 10.0.0.9/32 --action forward --target 0
# 把一个下游前缀中继给 peer 5;其余一切上送 Hub(peer 1)。
sudo subnetra policy add --src 0.0.0.0/0 --dst 10.0.0.32/27 --action forward --target 5
sudo subnetra policy add --src 0.0.0.0/0 --dst 10.0.0.0/24 --action forward --target 1
sudo subnetra policy show # 核对顺序
sudo subnetra save # 跨重启持久化
每个对端仍必须带上正确的 allowed_src,以匹配它被允许声称的内层源——该绑定无论这些规则
如何都会被强制执行。
spoke
一个暴露自身叠加 IP、其余一切经中继的家庭/办公 Spoke,只需要:
{
"role": "spoke",
"virtual_subnet": "10.0.0.0/24",
"local_id": 2,
"local_tun_ip": "10.0.0.2/24",
"local_routes": ["10.0.0.2/32"],
"peers": [
{ "id": 1, "endpoint": "203.0.113.1:18020", "allowed_src": "10.0.0.0/24", "psk": "…64 hex…" }
]
}
这会自动推导出:
10.0.0.2/32 → LOCAL(投递到本节点自身的 TUN)10.0.0.0/24 → hub(id 1)(其余一切经中继)
要发布 Spoke 背后的局域网(Site-to-Site),把它加入 local_routes
(例如 ["10.0.0.2/32", "192.168.2.0/24"]),使推导表把该前缀本地投递。
内置 NAT 保活
spoke 默认开启 NAT 保活(keepalive_secs = 20)。它每隔一段时间向其 Hub 发送一个
极小的已认证数据报,使空闲 Spoke 的 NAT 孔保持打开、Hub 保持新鲜回程路由——无需外部
pinger、无需 cron。显式设置 keepalive_secs 调节,或设为 0 关闭。
spoke 的校验规则
subnetrad --check 强制:
- 恰好 一个 Hub 对端,
- 至少一个本地目标(
local_routes或local_tun_ip), - 没有
0.0.0.0/0本地路由(那会把主机默认路由绑到隧道并将其黑洞)。
hub
对应的 Hub 只需列出它的 Spoke;每个对端的 allowed_src 成为指向该对端的一条转发规则:
{
"role": "hub",
"virtual_subnet": "10.0.0.0/24",
"local_id": 1,
"peers": [
{ "id": 2, "endpoint": "203.0.113.2:18020", "allowed_src": "10.0.0.2/32", "psk": "…64 hex…" },
{ "id": 3, "endpoint": "203.0.113.3:18020", "allowed_src": "10.0.0.3/32", "psk": "…64 hex…" }
]
}
这会推导出 10.0.0.2/32 → peer 2 与 10.0.0.3/32 → peer 3。Hub 按最长前缀匹配在 Spoke
之间中继,且绝不把包反射回源端。
访问 Hub 自身
推导出的 Hub 表 只转发给 Spoke——它从不投递到 Hub 自身的 TUN。因此默认情况下 Hub 没有叠加地址、在叠加网络上不可达:它是一个纯中继。这通常正是所需——中继不在网格上暴露 任何可寻址的东西。
若要让 Hub 自身经隧道可达(用于 SSH 登录它,或在叠加网络上托管服务),需要做 两 件事:
-
用
local_tun_ip给它一个地址,使网络规划为其 TUN 配置地址,并且 -
为该地址追加一条本地投递规则——把该节点按
manual运行并使用显式策略表,或在推导出的 Hub 表之上叠加一条规则:# 把 Hub 自身的叠加地址本地投递到它的 TUN sudo -E subnetra policy add --src 0.0.0.0/0 --dst 10.0.0.1/32 --action forward --target 0
不设 local_tun_ip(也不加这条规则)即让 Hub 保持纯中继,叠加网络上没有任何东西能寻址它。
hub 的校验规则
subnetrad --check 拒绝:
allowed_src缺失(或宽松的0.0.0.0/0)的对端,因为 Hub 无法判断一个包属于哪个 Spoke;- 两个
allowed_src前缀 重叠 的对端,那会使转发产生歧义。
Hub 的保活默认为 0(它不向 Spoke 发起保活)。
可直接编辑的示例
仓库的 deploy/ 目录提供可编辑的
hub.json、spoke-a.json、spoke-b.json 以及服务单元。完整的 Hub + 双 Spoke 演练在
生产部署。