From ba5434917622a7a9402b6c675870685fb43df4d4 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Fri, 5 Jun 2026 14:59:10 +0000 Subject: [PATCH] util: handle single-address IPv4 prefix in reverse DNS generation A /32 indexed past the 4-byte address and panicked at startup. --- hscontrol/util/dns.go | 20 ++++++++++++++++++++ hscontrol/util/dns_rootdomain_test.go | 20 ++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 hscontrol/util/dns_rootdomain_test.go diff --git a/hscontrol/util/dns.go b/hscontrol/util/dns.go index e41f23ed..8c2655b8 100644 --- a/hscontrol/util/dns.go +++ b/hscontrol/util/dns.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "net/netip" + "slices" "strconv" "strings" "unicode" @@ -103,6 +104,25 @@ func GenerateIPv4DNSRootDomain(ipPrefix netip.Prefix) []dnsname.FQDN { // wildcardBits is the number of bits not under the mask in the lastOctet wildcardBits := ByteSize - maskBits%ByteSize + // A mask covering the full address width (an IPv4 /32) leaves no wildcard + // octet, so lastOctet would index past the address. Emit the single + // reverse-DNS name for that exact address instead of panicking. + if lastOctet >= len(netRange.IP) { + rdnsSlice := make([]string, 0, len(netRange.IP)+1) + for _, v := range slices.Backward(netRange.IP) { + rdnsSlice = append(rdnsSlice, strconv.FormatUint(uint64(v), 10)) + } + + rdnsSlice = append(rdnsSlice, "in-addr.arpa.") + + fqdn, err := dnsname.ToFQDN(strings.Join(rdnsSlice, ".")) + if err != nil { + return nil + } + + return []dnsname.FQDN{fqdn} + } + // minVal is the value in the lastOctet byte of the IP // maxVal is basically 2^wildcardBits - i.e., the value when all the wildcardBits are set to 1 minVal := uint(netRange.IP[lastOctet]) diff --git a/hscontrol/util/dns_rootdomain_test.go b/hscontrol/util/dns_rootdomain_test.go new file mode 100644 index 00000000..a53d6480 --- /dev/null +++ b/hscontrol/util/dns_rootdomain_test.go @@ -0,0 +1,20 @@ +package util + +import ( + "net/netip" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestGenerateIPv4DNSRootDomainSingleAddress ensures a single-address IPv4 +// prefix (/32) does not index past the 4-byte address and panic. A full-width +// mask leaves no wildcard octet, so the function must emit the one reverse-DNS +// name for that address. +func TestGenerateIPv4DNSRootDomainSingleAddress(t *testing.T) { + require.NotPanics(t, func() { + fqdns := GenerateIPv4DNSRootDomain(netip.MustParsePrefix("100.64.0.1/32")) + assert.Len(t, fqdns, 1) + }) +}