From 99ad555d6452586caf4981fe877c6481f095e417 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Fri, 5 Jun 2026 15:12:05 +0000 Subject: [PATCH] db: handle degenerate prefixes in random IP allocation Single-address prefixes panicked; zero-high-byte addresses failed to encode. --- hscontrol/db/ip.go | 11 ++++++++++- hscontrol/db/randomnext_test.go | 35 +++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 hscontrol/db/randomnext_test.go diff --git a/hscontrol/db/ip.go b/hscontrol/db/ip.go index b2033595..bdfce709 100644 --- a/hscontrol/db/ip.go +++ b/hscontrol/db/ip.go @@ -251,6 +251,12 @@ func randomNext(pfx netip.Prefix) (netip.Addr, error) { // after. tempMax := big.NewInt(0).Sub(&to, &from) + // A single-address prefix (/32 or /128) has from == to, so tempMax is 0 and + // rand.Int would panic on a non-positive bound. Return the sole address. + if tempMax.Sign() <= 0 { + return fromIP, nil + } + out, err := rand.Int(rand.Reader, tempMax) if err != nil { return netip.Addr{}, fmt.Errorf("generating random IP: %w", err) @@ -258,7 +264,10 @@ func randomNext(pfx netip.Prefix) (netip.Addr, error) { valInRange := big.NewInt(0).Add(&from, out) - ip, ok := netip.AddrFromSlice(valInRange.Bytes()) + // big.Int.Bytes() strips leading zero bytes, so a value with a zero high + // byte yields a too-short slice that AddrFromSlice rejects. Pad to the + // prefix's address width. + ip, ok := netip.AddrFromSlice(valInRange.FillBytes(make([]byte, len(fromIP.AsSlice())))) if !ok { return netip.Addr{}, errGeneratedIPBytesInvalid } diff --git a/hscontrol/db/randomnext_test.go b/hscontrol/db/randomnext_test.go new file mode 100644 index 00000000..d20ecf41 --- /dev/null +++ b/hscontrol/db/randomnext_test.go @@ -0,0 +1,35 @@ +package db + +import ( + "net/netip" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestRandomNextSingleAddressPrefix ensures a single-address prefix (/32 or +// /128) does not panic. from == to makes the random range zero, and rand.Int +// panics on a non-positive bound; the sole address must be returned instead. +func TestRandomNextSingleAddressPrefix(t *testing.T) { + for _, p := range []string{"100.64.0.1/32", "fd7a:115c:a1e0::1/128"} { + pfx := netip.MustParsePrefix(p) + require.NotPanics(t, func() { + ip, err := randomNext(pfx) + require.NoError(t, err) + assert.Equal(t, pfx.Addr(), ip) + }, "prefix %s", p) + } +} + +// TestRandomNextLeadingZeroBytes ensures prefixes whose addresses have a zero +// high byte allocate successfully. big.Int.Bytes() strips leading zeros, so the +// drawn value would be too short for netip.AddrFromSlice without padding. +func TestRandomNextLeadingZeroBytes(t *testing.T) { + pfx := netip.MustParsePrefix("0.0.0.0/16") + for range 100 { + ip, err := randomNext(pfx) + require.NoError(t, err) + assert.True(t, pfx.Contains(ip), "ip %s not in %s", ip, pfx) + } +}