diff options
| author | Paul Buetow <paul@buetow.org> | 2026-06-29 11:14:13 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-06-29 11:14:13 +0300 |
| commit | a8057647167e943e3f3b1f68a2db94a158d80d68 (patch) | |
| tree | 1780a95d1c2b24779d99908a830558604775603a | |
| parent | 70acbd00e0f235d8f257a1f23a558a66ece55e9c (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.rb | 22 | ||||
| -rw-r--r-- | wireguardmeshgenerator.yaml | 7 |
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 |
