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 }