diff --git a/netx/dnsx/ptr/consts.go b/netx/dnsx/ptr/consts.go new file mode 100644 index 0000000..f9d12b2 --- /dev/null +++ b/netx/dnsx/ptr/consts.go @@ -0,0 +1,7 @@ +package ptr + +const ( + TLD string = "arpa" + Zone4 string = "in-addr." + TLD + Zone6 string = "ip6." + TLD +) diff --git a/netx/dnsx/ptr/doc.go b/netx/dnsx/ptr/doc.go new file mode 100644 index 0000000..a378399 --- /dev/null +++ b/netx/dnsx/ptr/doc.go @@ -0,0 +1,5 @@ +/* +Package ptr includes some helper functions for working with PTR records/zones +and mapping/converting them to/from [net/netip.Addr] and [net/netip.Prefix] representations respectively. +*/ +package ptr diff --git a/netx/dnsx/ptr/errs.go b/netx/dnsx/ptr/errs.go new file mode 100644 index 0000000..0d57c64 --- /dev/null +++ b/netx/dnsx/ptr/errs.go @@ -0,0 +1,11 @@ +package ptr + +import ( + "errors" +) + +var ( + ErrInvalidBounds error = errors.New("invalid PTR boundary") + ErrInvalidPfx error = errors.New("invalid prefix") + ErrInvalidPtr error = errors.New("invalid PTR") +) diff --git a/netx/dnsx/ptr/funcs.go b/netx/dnsx/ptr/funcs.go new file mode 100644 index 0000000..a402b7f --- /dev/null +++ b/netx/dnsx/ptr/funcs.go @@ -0,0 +1,266 @@ +package ptr + +import ( + "encoding/hex" + "fmt" + "net" + "net/netip" + "slices" + "strconv" + "strings" + + "github.com/miekg/dns" + "go4.org/netipx" + "r00t2.io/subnetter/netsplit" +) + +// AddrPtr returns the PTR lookup name of IP address ip (as a [net/netip.Addr]), normalized (lowercased and no trailing dot). +func AddrPtr(ip netip.Addr) (ptr string, err error) { return IpStrPtr(ip.String()) } + +// IpPtr returns the PTR lookup name of IP address ip (as a [net.IP]), normalized (lowercased and no trailing dot). +func IpPtr(ip net.IP) (ptr string, err error) { return IpStrPtr(ip.String()) } + +// IpStrPtr returns the PTR lookup name of IP address ip (as a string), normalized (lowercased and no trailing dot). +func IpStrPtr(ip string) (ptr string, err error) { + + if ptr, err = dns.ReverseAddr(ip); err != nil { + return + } + + ptr = normalizePtr(ptr) + + return +} + +/* +PfxToPtr returns the PTR zone name(s) for a specified prefix. +Any host bits set in pfx will be masked off without error +(e.g. 192.0.2.20/24 -> 192.0.2.0/24). + +err will be [ErrInvalidBounds] if pfx prefix length is longer than a /24 (e.g. a /32) for IPv4 +or longer than the last/shorter than the first 4-bit/nibble/nybble boundary - /124 (e.g. a /128) +and /4 respectively - for IPv6. +*/ +func PfxToPtr(pfx netip.Prefix) (ptrZones []string, err error) { + + var idx int + var tgtSz uint8 + var n *net.IPNet + var maxBitLen int + var pfxs []*netip.Prefix + var c *netsplit.CIDRSplitter + + if !pfx.IsValid() { + err = ErrInvalidPfx + return + } + + pfx = pfx.Masked() + + if pfx.Addr().Is4() { + maxBitLen = 24 + if pfx.Bits() <= 24 { + tgtSz = 24 + } + if pfx.Bits() <= 16 { + tgtSz = 16 + } + if pfx.Bits() <= 8 { + tgtSz = 8 + } + } else { + maxBitLen = 124 + if pfx.Bits() < 4 { + err = ErrInvalidBounds + return + } + tgtSz = uint8(pfx.Bits()) + for tgtSz%4 != 0 { + tgtSz++ + } + } + if pfx.Bits() > maxBitLen { + err = ErrInvalidBounds + return + } + + c = &netsplit.CIDRSplitter{ + PrefixLength: tgtSz, + BaseSplitter: new(netsplit.BaseSplitter), + } + n = netipx.PrefixIPNet(pfx) + c.BaseSplitter.SetParent(*n) + + if pfxs, _, err = c.Split(); err != nil { + return + } + + ptrZones = make([]string, len(pfxs)) + for idx = range pfxs { + ptrZones[idx] = pfxZonePtr(*pfxs[idx]) + } + + return +} + +// PtrToAddr returns a [net/netip.Addr] from a PTR record name. +func PtrToAddr(ptr string) (addr netip.Addr, err error) { + + var pfx netip.Prefix + + if pfx, err = PtrToPfx(ptr); err != nil { + return + } + addr = pfx.Addr() + + return +} + +/* +PtrToPfx returns a [net/netip.Prefix] from a PTR zone (or record). + +The same boundary rules as [PfxToPtr] are in place; delegated subzones (RFC 2317) are not supported. +If an invalid PTR is received, err will be [ErrInvalidPtr]. +*/ +func PtrToPfx(ptr string) (pfx netip.Prefix, err error) { + + var idx int + var ok bool + var b []byte + var sfx string + var cidr uint8 + var spl []string + var addr netip.Addr + + ptr = normalizePtr(ptr) + spl = strings.Split(ptr, ".") + if len(spl) < 2 { + err = ErrInvalidPtr + return + } + sfx = strings.Join(spl[len(spl)-2:], ".") + spl = strings.Split(normalizePtr(strings.TrimSuffix(ptr, sfx)), ".") + + slices.Reverse(spl) + + switch sfx { + case Zone4: + switch len(spl) { + case 0: + cidr = 0 + spl = []string{"0", "0", "0", "0"} + case 1, 2, 3, 4: + cidr = uint8(len(spl) * 8) + for len(spl) < 4 { + spl = append(spl, "0") + } + default: + err = ErrInvalidPtr + return + } + if addr, err = netip.ParseAddr( + strings.Join(spl, "."), + ); err != nil { + return + } + case Zone6: + if len(spl) > 32 { + err = ErrInvalidPtr + return + } + if len(spl) == 0 { + cidr = 0 + spl = make([]string, 32) + for idx = 0; idx < len(spl); idx++ { + spl[idx] = "0" + } + } else { + cidr = uint8(len(spl) * 4) + for len(spl) < 32 { + spl = append(spl, "0") + } + } + if b, err = hex.DecodeString( + strings.ToLower(strings.Join(spl, "")), + ); err != nil { + return + } + if addr, ok = netip.AddrFromSlice(b); !ok { + err = ErrInvalidPfx + return + } + default: + err = ErrInvalidPtr + return + } + + if pfx, err = addr.Prefix(int(cidr)); err != nil { + return + } + + return +} + +func normalizePtr(ptr string) (norm string) { return strings.ToLower(strings.TrimRight(ptr, ".")) } + +func pfxZonePtr(pfx netip.Prefix) (ptr string) { + + if pfx.Addr().Is4() { + ptr = pfxZonePtr4(pfx) + } else { + ptr = pfxZonePtr6(pfx) + } + + return +} + +func pfxZonePtr4(pfx netip.Prefix) (ptr string) { + + var idx int + var b []byte + var s []string + + idx = pfx.Bits() / 8 + + b = pfx.Addr().AsSlice()[0:idx] + s = make([]string, len(b)) + + for i := 0; i < idx; i++ { + s[idx-1-i] = strconv.Itoa(int(uint8(b[i]))) + } + + ptr = strings.Join(s, ".") + "." + Zone4 + + return +} + +func pfxZonePtr6(pfx netip.Prefix) (ptr string) { + + var b byte + var nib byte + var numNibs int + var b16 [16]byte + var sb *strings.Builder = new(strings.Builder) + + numNibs = pfx.Bits() / 4 + b16 = pfx.Addr().As16() + + for i := numNibs - 1; i >= 0; i-- { + b = b16[i/2] + if i%2 == 0 { + nib = b >> 4 + } else { + nib = b & 0x0f + } + fmt.Fprintf( + sb, + "%x.", + nib, + ) + } + sb.WriteString(Zone6) + + ptr = sb.String() + + return +} diff --git a/tplx/sprigx/README.html b/tplx/sprigx/README.html index 027a9e6..65a938a 100644 --- a/tplx/sprigx/README.html +++ b/tplx/sprigx/README.html @@ -636,7 +636,7 @@ pre.rouge .gs {
Brent Saner
bts@square-r00t.net
-Last rendered 2026-09-04 04:15:33 -0400 +Last rendered 2026-09-11 22:12:00 -0400
Table of Contents
diff --git a/tplx/sprigx/README.md b/tplx/sprigx/README.md index 5892f7d..2aeef98 100644 --- a/tplx/sprigx/README.md +++ b/tplx/sprigx/README.md @@ -6,7 +6,7 @@ Brent Saner\ \ -Last rendered 2026-09-04 04:15:38 -0400 +Last rendered 2026-09-11 22:12:05 -0400