两个站点、一个子网、一个 WireGuard 中心节点:让它跑起来
两个 LAN 都在 10.10.7.0/24 上,中心节点无法按目的地选择站点。给每个服务一个别名,并在它的网关上做转换。
两个站点使用同一个 LAN 网段,真正让设计行不通的并不是地址重叠本身。中心节点只能依据目的地址来选择站点,而两个完全相同的目的地让它无从选择。为了让一条隧道成立而给一个正常运转的办公室重新编址,通常比给每个发布的服务一个隧道能够承载的独立地址更不划算。下面的设计每个站点只发布一个 HTTPS 服务,不发布其他东西。
为什么地址重叠不是真正的问题
WireGuard 按目的地址转发:一个 peer 的 AllowedIPs 决定数据包会交给哪个 peer。两个 peer 不能同时宣告 10.10.7.0/24,否则其中一个会赢得每一次查表。这是对等体表的性质,而不是隧道的性质,所以这里的解决办法是地址规划,而不是路由技巧。
计划是完全停止宣告 LAN。改为按服务发布别名,每个站点一个地址,让每个站点的网关把这个别名转换成它真正的应用。
用户只路由两个唯一地址,永远看不到那个含糊的前缀,两个办公室里的东西都不需要改。这也诚实地说明了边界:你是在每个站点各发布一个 HTTPS 服务,而不是把两个网络合并。正因为这句话很窄,后面的规则才能保持很小。更宽的表述会需要更宽的规则,这些设计变复杂也正是由此而来。
给每个站点一个用户可以路由的别名
从一段与用户机器、中心节点和两个 LAN 都不冲突的地址范围里挑选。这里中心节点是 10.66.0.1,用户是 10.66.0.10,网关是 10.66.0.21 和 10.66.0.22,两个别名是 10.77.1.20 和 10.77.2.20。中心节点通过持有该别名的 peer 来发布它,因此发往某个别名的出站数据包只能从对应的站点出去:
[Interface]
Address = 10.66.0.1/32
ListenPort = 51820
PrivateKey = HUB_KEY
[Peer]
PublicKey = USER_PUBLIC_KEY
AllowedIPs = 10.66.0.10/32
[Peer]
PublicKey = SITE_A_PUBLIC_KEY
AllowedIPs = 10.66.0.21/32, 10.77.1.20/32
[Peer]
PublicKey = SITE_B_PUBLIC_KEY
AllowedIPs = 10.66.0.22/32, 10.77.2.20/32
用户侧只需要这两个别名,这样客户端配置里就不必出现两个 LAN 前缀。那份配置里没有一处提到真实的 LAN:
[Interface]
Address = 10.66.0.10/32
PrivateKey = USER_KEY
[Peer]
PublicKey = HUB_PUBLIC_KEY
Endpoint = hub.example.net:51820
AllowedIPs = 10.77.1.20/32, 10.77.2.20/32
PersistentKeepalive = 25
每个网关只宣告中心节点和用户来源,别的什么都不宣告,所以一个站点永远不会知道另一个站点的 LAN。Keepalive 在站点上行链路上维持 NAT 映射;它不授权流量,是否放行仍由防火墙决定。把一个网关的配置复制到另一个是可以的,前提是私钥、接口地址和别名都改掉。
中心节点必须允许它的两个 peer 之间做什么
中心节点在自己的两个 peer 之间转发数据包,这是 forward 链上的决定,而不是输入链上的决定。用平台的常规机制开启 IPv4 转发,然后只放行设计所需的那几对。
下面的规则只允许用户访问一个别名和一个端口,并通过连接跟踪放行回程流量。它丢弃 forward 路径上其他一切,这正是要点:
nft add table inet hub_transit
nft add chain inet hub_transit forward '{ type filter hook forward priority 0 ; policy drop ; }'
nft add rule inet hub_transit forward ct state established,related accept
nft add rule inet hub_transit forward iifname wg0 oifname wg0 tcp dport 443 ip saddr 10.66.0.10 ip daddr 10.77.1.20 accept
有两点值得说明,因为它们会造成静默失败。一个基础链里的 accept 不会覆盖另一个链里的 drop,所以要把这条规则并入现有策略,而不是并列加载。源地址标识的是设备而不是人;请把应用认证与网络准入分开。
别名在哪里变成真实地址
站点 A 收到发往 10.77.1.20 的请求,把目的地址改写为真实应用,并把源地址改写为自己的 LAN 地址,这样回应无需指向隧道的路由就能返回。把两种转换放在同一个地方完成,网络的其余部分就不必知道这个别名。下面的规则就是全部机制:
nft add table ip site_a_nat
nft add chain ip site_a_nat pre '{ type nat hook prerouting priority -100 ; policy accept ; }'
nft add chain ip site_a_nat post '{ type nat hook postrouting priority 100 ; policy accept ; }'
nft add rule ip site_a_nat pre iifname wg0 tcp dport 443 ip saddr 10.66.0.10 ip daddr 10.77.1.20 dnat to 10.10.7.20:443
nft add rule ip site_a_nat post iifname wg0 oifname lan0 tcp dport 443 ip saddr 10.66.0.10 ip daddr 10.10.7.20 snat to 10.10.7.2
接下来站点 A 的 forward 策略必须放行从隧道接口到 LAN 接口的已转换流量,以及回程方向。forward 钩子看到的是目的地址转换之后、源地址改写之前的目的地,这就是规则里要写真实应用地址的原因。每当一条规则看起来什么都不匹配时,就按这个顺序读这两个钩子。针对别名写的 forward 规则永远匹配不上,因为在那个钩子上别名已经不存在了。
站点 B 用自己的别名和自己的密钥重复一遍。它不能复用站点 A 的私钥,因为两个 peer 共用一个身份是配置错误,这种错误很难看出来,却很容易靠复制文件制造出来。除必须不同的值以外,两个网关文档要保持一致,这样差异在评审时才会凸显。复制文件正是这类故障的常见来源。
改写源地址是有代价的。应用把网关看作自己的客户端,所以日志里不再显示是哪个用户连上来的。请保留网关的审计记录,不要为了补偿转换抹掉的网络身份而削弱应用登录。
如何证明两个发布的名字都能应答
给每个别名一个 DNS 名字,并为这个名字配置应用和证书,因为转换不会改写重定向或内嵌的绝对 URL。每个站点用一个名字,这样证书不匹配就绝不会被误认为路由故障。
从真实的客户端测试,确认是哪个站点应答的,并且也测试一个未被授权的端口。任何 NAT 改动之后都要用新连接,因为已建立的状态会保留旧映射:
wg show wg0 allowed-ips
sudo nft list ruleset
curl -sS -o /dev/null -w "%{http_code}\n" https://site-a.example.net/
第一条命令里如果别名是空的或出乎意料,说明在任何防火墙规则起作用之前,peer 选择就已经错了。如果别名正确而名字仍然失败,问题已经转移到 DNS 和证书上。等两个名字都能应答、第三个端口仍然关闭时,这个设计就做到了它宣称的事:两个窄范围发布的服务,而不是两个合并的 LAN。