summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-29 11:14:13 +0300
committerPaul Buetow <paul@buetow.org>2026-06-29 11:14:13 +0300
commita8057647167e943e3f3b1f68a2db94a158d80d68 (patch)
tree1780a95d1c2b24779d99908a830558604775603a
parent70acbd00e0f235d8f257a1f23a558a66ece55e9c (diff)
Fix roaming clients getting duplicate 0.0.0.0/0 on both gateway peers
wg-quick can only install one default route; giving AllowedIPs=0.0.0.0/0 to both blowfish and fishfinger caused the second peer to silently end up with allowed-ips:(none) in the running WireGuard config. This made blowfish a dead peer on earth/pixel7pro/uranus despite active keepalives. Fix: introduce primary_gateway:true on fishfinger in YAML. In compute_allowed_ips, only the peer flagged primary_gateway gets 0.0.0.0/0 for roaming+gateway:true clients; all other peers (secondary gateways and infra) receive their specific /32+/128 IPs only. Also document in extra_ips_via_gateway why reachable_via must name only one gateway — WireGuard enforces each AllowedIPs prefix belongs to exactly one peer, so duplicating earth's IPs across both gateways would cause the same silent conflict problem. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
-rw-r--r--wireguardmeshgenerator.rb22
-rw-r--r--wireguardmeshgenerator.yaml7
2 files changed, 25 insertions, 4 deletions
diff --git a/wireguardmeshgenerator.rb b/wireguardmeshgenerator.rb
index ee9492f..f563a78 100644
--- a/wireguardmeshgenerator.rb
+++ b/wireguardmeshgenerator.rb
@@ -218,6 +218,9 @@ WireguardConfig = Struct.new(:myself, :hosts) do
# return traffic must flow via that gateway back to them. This ensures the gateway
# peer's AllowedIPs covers the roaming client's wg0 IPs, so infra hosts route
# traffic destined for those clients through the gateway.
+ # Note: WireGuard requires each AllowedIPs prefix to belong to exactly one peer,
+ # so reachable_via must name only one gateway — duplicating IPs across two peers
+ # causes WireGuard to silently assign the prefix to only one of them.
def extra_ips_via_gateway(gateway_name)
hosts.filter_map do |_name, data|
next unless data['reachable_via'] == gateway_name
@@ -230,7 +233,11 @@ WireguardConfig = Struct.new(:myself, :hosts) do
# Computes AllowedIPs for a peer entry on the current (myself) host.
# Returns [allowed_ips_string, updated_primary_gateway_assigned].
- # - Roaming clients with gateway:true: all traffic via VPN (0.0.0.0/0, ::/0).
+ #
+ # - Roaming clients with gateway:true: the peer flagged primary_gateway:true gets
+ # 0.0.0.0/0, ::/0 (full default route); all other peers get their specific IPs.
+ # wg-quick can only install one default route — giving 0.0.0.0/0 to multiple peers
+ # causes all but the first to silently receive allowed_ips:(none) in the running config.
# - Roaming clients with gateway:false: mesh subnet for primary gateway, specific IPs for others.
# - Regular mesh peers: their specific IPs; gateway peers also get extra IPs for
# roaming clients declared via reachable_via, so infra hosts can route to them via the gateway.
@@ -238,17 +245,24 @@ WireguardConfig = Struct.new(:myself, :hosts) do
peer_is_gateway = data.key?('internet')
ipv4 = data['wg0']['ip']
ipv6 = data['wg0']['ipv6']
+ specific_ips = ipv6 ? "#{ipv4}/32, #{ipv6}/128" : "#{ipv4}/32"
if is_roaming && use_gateway
- return '0.0.0.0/0, ::/0', primary_gateway_assigned
+ # Only the primary gateway (primary_gateway: true) gets the full default route.
+ # Secondary gateways and all other peers get only their specific wg0 IPs so
+ # WireGuard doesn't hit conflicting default route assignments.
+ return '0.0.0.0/0, ::/0', primary_gateway_assigned if peer_is_gateway && data['primary_gateway']
+
+ return specific_ips, primary_gateway_assigned
elsif is_roaming && !use_gateway
return roaming_no_gateway_ips(peer_is_gateway, ipv4, ipv6, primary_gateway_assigned)
end
# Regular (non-roaming) host: route specific IPs only.
# For gateway peers, also append IPs of roaming clients reachable via this gateway,
- # so infra hosts can reach them (e.g. earth via fishfinger) without a direct peer block.
- ips = ipv6 ? "#{ipv4}/32, #{ipv6}/128" : "#{ipv4}/32"
+ # so infra hosts can reach them (e.g. earth via fishfinger or blowfish) without a
+ # direct peer block.
+ ips = specific_ips
if peer_is_gateway
extra = extra_ips_via_gateway(peer)
ips = ([ips] + extra).join(', ') unless extra.empty?
diff --git a/wireguardmeshgenerator.yaml b/wireguardmeshgenerator.yaml
index afceb5b..164a264 100644
--- a/wireguardmeshgenerator.yaml
+++ b/wireguardmeshgenerator.yaml
@@ -149,6 +149,11 @@ hosts:
ipv6: fd42:beef:cafe:2::110
fishfinger:
os: OpenBSD
+ # primary_gateway: true marks fishfinger as the single gateway peer that receives
+ # AllowedIPs = 0.0.0.0/0, ::/0 on roaming clients (earth, pixel7pro, uranus).
+ # wg-quick can only install one default route, so only the primary gateway gets
+ # the catch-all; secondary gateways (blowfish) get their specific /32+/128 only.
+ primary_gateway: true
ssh:
user: rex
port: 2
@@ -168,6 +173,8 @@ hosts:
# gateway peer's AllowedIPs on infra hosts that peer with that gateway.
# Earth is behind NAT and cannot be a direct peer on infra hosts; instead,
# traffic routes via fishfinger -> earth.
+ # WireGuard requires each AllowedIPs prefix to belong to exactly one peer, so
+ # only one gateway can hold earth's IPs; fishfinger is the primary gateway.
reachable_via: fishfinger
wg0:
domain: wg0.wan.buetow.org