diff --git a/internal/web/service/client_wireguard.go b/internal/web/service/client_wireguard.go index d3a64722f..0285f30d3 100644 --- a/internal/web/service/client_wireguard.go +++ b/internal/web/service/client_wireguard.go @@ -117,16 +117,15 @@ func allocateWireguardAddress(used []string, base string, allowWidening bool) (s wide = append(wide, p) } } - isTaken := func(a netip.Addr) bool { - if _, ok := taken[a]; ok { - return true - } + // wideClaimOf returns a wide claim holding a, so the scan can step over the whole claim: + // a node may report one spanning the scope, and walking it is 2^64 steps in IPv6. + wideClaimOf := func(a netip.Addr) (netip.Prefix, bool) { for _, p := range wide { if p.Contains(a) { - return true + return p, true } } - return false + return netip.Prefix{}, false } scopes := []netip.Prefix{prefix} if allowWidening && prefix.Addr().Is4() && prefix.Bits() > wireguardPoolFloorBits { @@ -136,16 +135,31 @@ func allocateWireguardAddress(used []string, base string, allowWidening bool) (s } for _, scope := range scopes { addr := scope.Masked().Addr().Next().Next() - for scope.Contains(addr) { - if !isTaken(addr) { - return addr.String() + "/" + hostBits, nil + for addr.IsValid() && scope.Contains(addr) { + if _, ok := taken[addr]; ok { + addr = addr.Next() + continue } - addr = addr.Next() + if claim, ok := wideClaimOf(addr); ok { + addr = lastPrefixAddr(claim).Next() + continue + } + return addr.String() + "/" + hostBits, nil } } return "", common.NewError("wireguard: no free address available in", scopes[len(scopes)-1].String()) } +// lastPrefixAddr is the highest address of p. +func lastPrefixAddr(p netip.Prefix) netip.Addr { + bytes := p.Masked().Addr().AsSlice() + for bit := p.Bits(); bit < len(bytes)*8; bit++ { + bytes[bit/8] |= 0x80 >> (bit % 8) + } + addr, _ := netip.AddrFromSlice(bytes) + return addr +} + // normalizeWireguardAllowedIPs validates user-supplied allowedIPs entries and // canonicalizes them: bare addresses become single-host prefixes, duplicates drop. func normalizeWireguardAllowedIPs(values []string) ([]string, error) { diff --git a/internal/web/service/client_wireguard_test.go b/internal/web/service/client_wireguard_test.go index 7fc4860e4..6c723c44d 100644 --- a/internal/web/service/client_wireguard_test.go +++ b/internal/web/service/client_wireguard_test.go @@ -24,6 +24,9 @@ func TestAllocateWireguardAddress(t *testing.T) { {name: "default base when empty", used: nil, base: "", want: "10.0.0.2/32"}, {name: "full ipv4 scope widens instead of failing", used: []string{"10.9.0.2/32", "10.9.0.3/32"}, base: "10.9.0.0/30", want: "10.9.0.4/32"}, {name: "exhausted ipv6 scope errors", used: []string{"fd00::2/128", "fd00::3/128"}, base: "fd00::/126", err: true}, + // A node can report a wide claim; the scan must step over it, not through 2^64 addresses. + {name: "wide claim over an ipv6 scope", used: []string{"fd00::/64"}, base: "fd00::/64", err: true}, + {name: "first address past a wide claim", used: []string{"10.0.0.0/25"}, base: "10.0.0.0/24", want: "10.0.0.128/32"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) {