fix(wireguard): step over wide address claims when allocating

allocateWireguardAddress walked the scope one address at a time and
tested each against every wide claim, so a claim covering an IPv6 /64
(a node can report such allowedIPs) turned the next address-less client
add into a 2^64-step loop that also held every later client change on
that inbound. The scan now jumps past a wide claim in one step, so it is
bounded by the number of claims.
This commit is contained in:
MHSanaei
2026-10-31 14:35:01 +01:00
parent 351a01ff0c
commit d69b924baf
2 changed files with 27 additions and 10 deletions
+24 -10
View File
@@ -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) {
@@ -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) {