Avoid the unbounded runtime during random allocation, if random allocation fails after a first pass at random through the provided ranges, pick the next free address by walking through the allocated set. The new ipx utilities provide a bitset based allocation pool, good for small to moderate ranges of IPv4 addresses as used in natc. Updates #15367 Signed-off-by: James Tucker <james@tailscale.com>main
parent
fb47824d74
commit
95034e15a7
@ -0,0 +1,130 @@ |
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package main |
||||
|
||||
import ( |
||||
"math/big" |
||||
"math/bits" |
||||
"math/rand/v2" |
||||
"net/netip" |
||||
|
||||
"go4.org/netipx" |
||||
) |
||||
|
||||
func addrLessOrEqual(a, b netip.Addr) bool { |
||||
if a.Less(b) { |
||||
return true |
||||
} |
||||
if a == b { |
||||
return true |
||||
} |
||||
return false |
||||
} |
||||
|
||||
// indexOfAddr returns the index of addr in ipset, or -1 if not found.
|
||||
func indexOfAddr(addr netip.Addr, ipset *netipx.IPSet) int { |
||||
var base int // offset of the current range
|
||||
for _, r := range ipset.Ranges() { |
||||
if addr.Less(r.From()) { |
||||
return -1 |
||||
} |
||||
numFrom := v4ToNum(r.From()) |
||||
if addrLessOrEqual(addr, r.To()) { |
||||
numInRange := int(v4ToNum(addr) - numFrom) |
||||
return base + numInRange |
||||
} |
||||
numTo := v4ToNum(r.To()) |
||||
base += int(numTo-numFrom) + 1 |
||||
} |
||||
return -1 |
||||
} |
||||
|
||||
// addrAtIndex returns the address at the given index in ipset, or an empty
|
||||
// address if index is out of range.
|
||||
func addrAtIndex(index int, ipset *netipx.IPSet) netip.Addr { |
||||
if index < 0 { |
||||
return netip.Addr{} |
||||
} |
||||
var base int // offset of the current range
|
||||
for _, r := range ipset.Ranges() { |
||||
numFrom := v4ToNum(r.From()) |
||||
numTo := v4ToNum(r.To()) |
||||
if index <= base+int(numTo-numFrom) { |
||||
return numToV4(uint32(int(numFrom) + index - base)) |
||||
} |
||||
base += int(numTo-numFrom) + 1 |
||||
} |
||||
return netip.Addr{} |
||||
} |
||||
|
||||
// TODO(golang/go#9455): once we have uint128 we can easily implement for all addrs.
|
||||
|
||||
// v4ToNum returns a uint32 representation of the IPv4 address. If addr is not
|
||||
// an IPv4 address, this function will panic.
|
||||
func v4ToNum(addr netip.Addr) uint32 { |
||||
addr = addr.Unmap() |
||||
if !addr.Is4() { |
||||
panic("only IPv4 addresses are supported by v4ToNum") |
||||
} |
||||
b := addr.As4() |
||||
var o uint32 |
||||
o = o<<8 | uint32(b[0]) |
||||
o = o<<8 | uint32(b[1]) |
||||
o = o<<8 | uint32(b[2]) |
||||
o = o<<8 | uint32(b[3]) |
||||
return o |
||||
} |
||||
|
||||
func numToV4(i uint32) netip.Addr { |
||||
var addr [4]byte |
||||
addr[0] = byte((i >> 24) & 0xff) |
||||
addr[1] = byte((i >> 16) & 0xff) |
||||
addr[2] = byte((i >> 8) & 0xff) |
||||
addr[3] = byte(i & 0xff) |
||||
return netip.AddrFrom4(addr) |
||||
} |
||||
|
||||
// allocAddr returns an address in ipset that is not already marked allocated in allocated.
|
||||
func allocAddr(ipset *netipx.IPSet, allocated *big.Int) netip.Addr { |
||||
// first try to allocate a random IP from each range, if we land on one.
|
||||
var base uint32 // index offset of the current range
|
||||
for _, r := range ipset.Ranges() { |
||||
numFrom := v4ToNum(r.From()) |
||||
numTo := v4ToNum(r.To()) |
||||
randInRange := rand.N(numTo - numFrom) |
||||
randIndex := base + randInRange |
||||
if allocated.Bit(int(randIndex)) == 0 { |
||||
allocated.SetBit(allocated, int(randIndex), 1) |
||||
return numToV4(numFrom + randInRange) |
||||
} |
||||
base += numTo - numFrom + 1 |
||||
} |
||||
|
||||
// fall back to seeking a free bit in the allocated set
|
||||
index := -1 |
||||
for i, word := range allocated.Bits() { |
||||
zbi := leastZeroBit(uint(word)) |
||||
if zbi == -1 { |
||||
continue |
||||
} |
||||
index = i*bits.UintSize + zbi |
||||
allocated.SetBit(allocated, index, 1) |
||||
break |
||||
} |
||||
if index == -1 { |
||||
return netip.Addr{} |
||||
} |
||||
return addrAtIndex(index, ipset) |
||||
} |
||||
|
||||
// leastZeroBit returns the index of the least significant zero bit in the given uint, or -1
|
||||
// if all bits are set.
|
||||
func leastZeroBit(n uint) int { |
||||
notN := ^n |
||||
rightmostBit := notN & -notN |
||||
if rightmostBit == 0 { |
||||
return -1 |
||||
} |
||||
return bits.TrailingZeros(rightmostBit) |
||||
} |
||||
@ -0,0 +1,150 @@ |
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package main |
||||
|
||||
import ( |
||||
"math" |
||||
"math/big" |
||||
"net/netip" |
||||
"testing" |
||||
|
||||
"go4.org/netipx" |
||||
"tailscale.com/util/must" |
||||
) |
||||
|
||||
func TestV4ToNum(t *testing.T) { |
||||
cases := []struct { |
||||
addr netip.Addr |
||||
num uint32 |
||||
}{ |
||||
{netip.MustParseAddr("0.0.0.0"), 0}, |
||||
{netip.MustParseAddr("255.255.255.255"), 0xffffffff}, |
||||
{netip.MustParseAddr("8.8.8.8"), 0x08080808}, |
||||
{netip.MustParseAddr("192.168.0.1"), 0xc0a80001}, |
||||
{netip.MustParseAddr("10.0.0.1"), 0x0a000001}, |
||||
{netip.MustParseAddr("172.16.0.1"), 0xac100001}, |
||||
{netip.MustParseAddr("100.64.0.1"), 0x64400001}, |
||||
} |
||||
|
||||
for _, tc := range cases { |
||||
num := v4ToNum(tc.addr) |
||||
if num != tc.num { |
||||
t.Errorf("addrNum(%v) = %d, want %d", tc.addr, num, tc.num) |
||||
} |
||||
if numToV4(num) != tc.addr { |
||||
t.Errorf("numToV4(%d) = %v, want %v", num, numToV4(num), tc.addr) |
||||
} |
||||
} |
||||
|
||||
func() { |
||||
defer func() { |
||||
if r := recover(); r == nil { |
||||
t.Fatal("expected panic") |
||||
} |
||||
}() |
||||
|
||||
v4ToNum(netip.MustParseAddr("::1")) |
||||
}() |
||||
} |
||||
|
||||
func TestAddrIndex(t *testing.T) { |
||||
builder := netipx.IPSetBuilder{} |
||||
builder.AddRange(netipx.MustParseIPRange("10.0.0.1-10.0.0.5")) |
||||
builder.AddRange(netipx.MustParseIPRange("192.168.0.1-192.168.0.10")) |
||||
ipset := must.Get(builder.IPSet()) |
||||
|
||||
indexCases := []struct { |
||||
addr netip.Addr |
||||
index int |
||||
}{ |
||||
{netip.MustParseAddr("10.0.0.1"), 0}, |
||||
{netip.MustParseAddr("10.0.0.2"), 1}, |
||||
{netip.MustParseAddr("10.0.0.3"), 2}, |
||||
{netip.MustParseAddr("10.0.0.4"), 3}, |
||||
{netip.MustParseAddr("10.0.0.5"), 4}, |
||||
{netip.MustParseAddr("192.168.0.1"), 5}, |
||||
{netip.MustParseAddr("192.168.0.5"), 9}, |
||||
{netip.MustParseAddr("192.168.0.10"), 14}, |
||||
{netip.MustParseAddr("172.16.0.1"), -1}, // Not in set
|
||||
} |
||||
|
||||
for _, tc := range indexCases { |
||||
index := indexOfAddr(tc.addr, ipset) |
||||
if index != tc.index { |
||||
t.Errorf("indexOfAddr(%v) = %d, want %d", tc.addr, index, tc.index) |
||||
} |
||||
if tc.index == -1 { |
||||
continue |
||||
} |
||||
addr := addrAtIndex(tc.index, ipset) |
||||
if addr != tc.addr { |
||||
t.Errorf("addrAtIndex(%d) = %v, want %v", tc.index, addr, tc.addr) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestAllocAddr(t *testing.T) { |
||||
builder := netipx.IPSetBuilder{} |
||||
builder.AddRange(netipx.MustParseIPRange("10.0.0.1-10.0.0.5")) |
||||
builder.AddRange(netipx.MustParseIPRange("192.168.0.1-192.168.0.10")) |
||||
ipset := must.Get(builder.IPSet()) |
||||
|
||||
allocated := new(big.Int) |
||||
for range 15 { |
||||
addr := allocAddr(ipset, allocated) |
||||
if !addr.IsValid() { |
||||
t.Errorf("allocAddr() = invalid, want valid") |
||||
} |
||||
if !ipset.Contains(addr) { |
||||
t.Errorf("allocAddr() = %v, not in set", addr) |
||||
} |
||||
} |
||||
addr := allocAddr(ipset, allocated) |
||||
if addr.IsValid() { |
||||
t.Errorf("allocAddr() = %v, want invalid", addr) |
||||
} |
||||
wantAddr := netip.MustParseAddr("10.0.0.2") |
||||
allocated.SetBit(allocated, indexOfAddr(wantAddr, ipset), 0) |
||||
addr = allocAddr(ipset, allocated) |
||||
if addr != wantAddr { |
||||
t.Errorf("allocAddr() = %v, want %v", addr, wantAddr) |
||||
} |
||||
} |
||||
|
||||
func TestLeastZeroBit(t *testing.T) { |
||||
cases := []struct { |
||||
num uint |
||||
want int |
||||
}{ |
||||
{math.MaxUint, -1}, |
||||
{0, 0}, |
||||
{0b01, 1}, |
||||
{0b11, 2}, |
||||
{0b111, 3}, |
||||
{math.MaxUint, -1}, |
||||
{math.MaxUint - 1, 0}, |
||||
} |
||||
if math.MaxUint == math.MaxUint64 { |
||||
cases = append(cases, []struct { |
||||
num uint |
||||
want int |
||||
}{ |
||||
{math.MaxUint >> 1, 63}, |
||||
}...) |
||||
} else { |
||||
cases = append(cases, []struct { |
||||
num uint |
||||
want int |
||||
}{ |
||||
{math.MaxUint >> 1, 31}, |
||||
}...) |
||||
} |
||||
|
||||
for _, tc := range cases { |
||||
got := leastZeroBit(tc.num) |
||||
if got != tc.want { |
||||
t.Errorf("leastZeroBit(%b) = %d, want %d", tc.num, got, tc.want) |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue