checking in some WIP
* added some netx funcs * added netx/dnsx * currently updating docs and adding *x funcs to sprigx
This commit is contained in:
571
netx/dnsx/funcs.go
Normal file
571
netx/dnsx/funcs.go
Normal file
@@ -0,0 +1,571 @@
|
||||
package dnsx
|
||||
|
||||
import (
|
||||
`bytes`
|
||||
`encoding/base32`
|
||||
`fmt`
|
||||
`math`
|
||||
`net`
|
||||
`net/netip`
|
||||
`strings`
|
||||
|
||||
`go4.org/netipx`
|
||||
`r00t2.io/goutils/stringsx`
|
||||
)
|
||||
|
||||
/*
|
||||
AddrFromPtr returns a [net/netip.Addr] from a PTR record.
|
||||
|
||||
It is the inverse of [AddrToPtr].
|
||||
|
||||
See also [IpFromPtr].
|
||||
*/
|
||||
func AddrFromPtr(s string) (ip netip.Addr, err error) {
|
||||
|
||||
var idx int
|
||||
var ipStr string
|
||||
var tmpStr string
|
||||
var spl []string = strings.Split(strings.TrimSuffix(s, "."), ".")
|
||||
|
||||
switch len(spl) {
|
||||
case 6:
|
||||
if strings.Join(spl[4:], ".") != "in-addr.arpa" {
|
||||
err = ErrBadPtrRoot
|
||||
return
|
||||
}
|
||||
ipStr = fmt.Sprintf("%s.%s.%s.%s", spl[3], spl[2], spl[1], spl[0])
|
||||
case 34:
|
||||
if strings.Join(spl[32:], ".") != "ip6.arpa" {
|
||||
err = ErrBadPtrRoot
|
||||
return
|
||||
}
|
||||
tmpStr = stringsx.Reverse(strings.ReplaceAll(strings.Join(spl[:32], ""), ".", ""))
|
||||
for idx = 0; idx < len(tmpStr); idx++ {
|
||||
if idx%4 == 0 && idx != 0 {
|
||||
ipStr += ":"
|
||||
}
|
||||
ipStr += string(rune(tmpStr[idx]))
|
||||
}
|
||||
default:
|
||||
err = ErrBadPtrLen
|
||||
return
|
||||
}
|
||||
|
||||
if ip, err = netip.ParseAddr(ipStr); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
AddrToPtr returns a PTR record from ip.
|
||||
|
||||
It is the inverse of [AddrFromPtr].
|
||||
|
||||
It includes the root label at the end (the trailing period).
|
||||
*/
|
||||
func AddrToPtr(ip netip.Addr) (s string) {
|
||||
|
||||
var idx int
|
||||
var b []byte
|
||||
var ipStr string
|
||||
|
||||
if ip.Is6() {
|
||||
ipStr = stringsx.Reverse(strings.ReplaceAll(ip.StringExpanded(), ":", ""))
|
||||
ipStr = strings.Join(
|
||||
strings.Split(ipStr, ""),
|
||||
".",
|
||||
)
|
||||
s = fmt.Sprintf("%s.ip6.arpa.", ipStr)
|
||||
} else {
|
||||
b = make([]byte, 4)
|
||||
copy(b, ip.AsSlice())
|
||||
for idx = len(b) - 1; idx >= 0; idx-- {
|
||||
ipStr += fmt.Sprintf("%d.", b[idx])
|
||||
}
|
||||
s = fmt.Sprintf("%s.in-addr.arpa.", ipStr)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
DnsStrToWire returns a wire-format of a DNS name.
|
||||
|
||||
No validation or conversion (other than to wire format) is performed,
|
||||
and it is expected that any IDN(A)/Punycode translation has
|
||||
*already been performed* such that recordNm is in the ASCII form.
|
||||
(See [IsFqdn] for more information on IDN(A)/Punycode.)
|
||||
|
||||
For encoding reasons, if any given label/segment has a length of 0 or greater than 255 ([math.MaxUint8]),
|
||||
[ErrBadLabelLen] will be returned.
|
||||
|
||||
See [DnsWireToStr] for the inverse.
|
||||
*/
|
||||
func DnsStrToWire(recordNm string) (recordNmBytes []byte, err error) {
|
||||
|
||||
var cLen int
|
||||
var c []byte
|
||||
var cStr string
|
||||
var spl []string
|
||||
var buf *bytes.Buffer = new(bytes.Buffer)
|
||||
|
||||
spl = strings.Split(strings.TrimSuffix(recordNm, "."), ".")
|
||||
for _, cStr = range spl {
|
||||
c = []byte(cStr)
|
||||
cLen = len(c)
|
||||
if !(cLen > 0 && cLen <= math.MaxUint8) {
|
||||
err = ErrBadLabelLen
|
||||
return
|
||||
}
|
||||
buf.Write(append([]byte{uint8(cLen)}, c...))
|
||||
}
|
||||
|
||||
recordNmBytes = append(buf.Bytes(), 0x00)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
DnsWireToStr is the inverse of [DnsStrToWire]. A trailing . is not included.
|
||||
|
||||
For decoding reasons, it will exit with [ErrBadLabelLen] if recordNmBytes is nil/empty or
|
||||
if no terminating nullbyte is found after 256 label characters have been encountered.
|
||||
*/
|
||||
func DnsWireToStr(recordNmBytes []byte) (recordNm string, err error) {
|
||||
|
||||
var c []byte
|
||||
var cLen uint8
|
||||
var arrLen int
|
||||
var numChars int
|
||||
var labels []string
|
||||
var buf *bytes.Buffer
|
||||
|
||||
if recordNmBytes == nil || len(recordNmBytes) == 0 {
|
||||
err = ErrBadLabelLen
|
||||
return
|
||||
}
|
||||
buf = bytes.NewBuffer(recordNmBytes)
|
||||
labels = make([]string, 0)
|
||||
|
||||
arrLen = len(recordNmBytes)
|
||||
for {
|
||||
if cLen, err = buf.ReadByte(); err != nil {
|
||||
return
|
||||
}
|
||||
if cLen == 0x00 {
|
||||
break
|
||||
}
|
||||
numChars += int(cLen)
|
||||
if numChars > 255 {
|
||||
err = ErrBadLabelLen
|
||||
return
|
||||
}
|
||||
if numChars > arrLen {
|
||||
err = ErrBadLabelLen
|
||||
return
|
||||
}
|
||||
c = buf.Next(int(cLen))
|
||||
labels = append(labels, string(c))
|
||||
}
|
||||
|
||||
recordNm = strings.Join(labels, ".")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IpFromPtr is like [AddrFromPtr] but with a [net.IP] instead.
|
||||
|
||||
It is the inverse of [IpToPtr].
|
||||
*/
|
||||
func IpFromPtr(s string) (ip net.IP, err error) {
|
||||
|
||||
var a netip.Addr
|
||||
|
||||
if a, err = AddrFromPtr(s); err != nil {
|
||||
return
|
||||
}
|
||||
ip = net.IP(a.AsSlice())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IpToPtr is like [AddrToPtr] but with a [net.IP] instead.
|
||||
|
||||
It is the inverse of [IpFromPtr].
|
||||
*/
|
||||
func IpToPtr(ip net.IP) (s string) {
|
||||
|
||||
var a netip.Addr
|
||||
|
||||
a, _ = netipx.FromStdIP(ip)
|
||||
s = AddrToPtr(a)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IsFqdn returns a boolean indicating if s is an FQDN that strictly adheres to RFC format requirements.
|
||||
It performs no lookups/resolution attempts or network operations otherwise.
|
||||
|
||||
It will return true for the "apex record" (e.g. the "naked domain"), as this is a valid assignable FQDN.
|
||||
|
||||
It will return false for wildcard records (see [IsFqdnWildcard]).
|
||||
|
||||
s may or may not end in a period (the root zone; "absolute" FQDNs) (0x00 in wire format).
|
||||
|
||||
# TLDs
|
||||
|
||||
Because valid TLDs are fairly dynamic and can change frequently,
|
||||
validation is *not* performed against the TLD itself.
|
||||
This only ensures that s has a TLD label conforming to the character rules in the referenced RFCs.
|
||||
See [golang.org/x/net/publicsuffix] if precise TLD validation is required (though true TLD validation generally
|
||||
requires fetching the current TLD lists from IANA at runtime like [github.com/bombsimon/tld-validator]).
|
||||
|
||||
# Special RFC-Defined Accommodations
|
||||
|
||||
RFC 2181 [§ 11] specifies that site-local DNS software may accommodate non-RFC-conforming rules.
|
||||
This function may and likely will return false for these site-local deviations.
|
||||
The Lookup* functions/mthods in [net] should be used to validate in these casts
|
||||
if that accommodation is necessary.
|
||||
|
||||
Note that underscores are not valid for "true" FQDNs as they are only valid for e.g. SRV record names,
|
||||
TXT records, etc. - not A/AAAA/CNAME, etc. - see RFC 8553 for details.
|
||||
|
||||
See the following functions for allowing additional syntax/rule validation
|
||||
that have record-type-specific accommodations made:
|
||||
|
||||
* [IsFqdnDefinedTxt]
|
||||
* [IsFqdnNsec3]
|
||||
* [IsFqdnSrv]
|
||||
* [IsFqdnWildcard]
|
||||
|
||||
# RFC Coverage
|
||||
|
||||
This function should conform properly to:
|
||||
|
||||
* RFC 952
|
||||
* RFC 1034 and RFC 1035
|
||||
* RFC 1123
|
||||
* RFC 2181 (selectively, see above)
|
||||
|
||||
preferring the most up-to-date rules where relevant (e.g. labels may start with digits, as per RFC 1123).
|
||||
It enforces/checks label and overall length limits as defined by RFC.
|
||||
|
||||
# IDN(A) and Punycode
|
||||
|
||||
Note that it expects the ASCII-only/presentation form of a record name and
|
||||
will not perform any IDN/IDNA nor Punycode translation.
|
||||
If a caller anticipates FQDNs in their localized format,
|
||||
the caller must perform translation first
|
||||
(via e.g. [gitlab.com/golang-commonmark/puny], [golang.org/x/net/idna], etc.).
|
||||
|
||||
To reiterate, IDN/IDNA:
|
||||
|
||||
* RFC 3490
|
||||
* RFC 5890
|
||||
* RFC 5891
|
||||
* RFC 5892
|
||||
* RFC 5893
|
||||
* RFC 5894
|
||||
|
||||
and Punycode (RFC 3492) *MUST* use their ASCII forms, NOT the localized/Unicode formats.
|
||||
|
||||
[§ 11]: https://datatracker.ietf.org/doc/html/rfc2181#section-11
|
||||
*/
|
||||
func IsFqdn(s string) (fqdn bool) {
|
||||
|
||||
var lbl string
|
||||
var lbls []string = strings.Split(strings.TrimSuffix(s, "."), ".")
|
||||
|
||||
if !commonFqdn(s) {
|
||||
return
|
||||
}
|
||||
for _, lbl = range lbls {
|
||||
if !IsLabel(lbl) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
fqdn = true
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IsFqdnDefinedTxt is like [IsFqdn] but explicitly *only* allows fully-qualified
|
||||
RFC-defined TXT "subtypes":
|
||||
|
||||
* ACME DNS-01 (RFC 8555)
|
||||
* BIMI (RFC draft [bimi])
|
||||
* DKIM (RFC 6376, RFC 8301, RFC 8463)
|
||||
* DKIM ATPS (RFC 6541)
|
||||
* DMARC (RFC 7489, RFC 9091, RFC 9616)
|
||||
* MTA-STS (RFC 8461)
|
||||
* TLSRPT (RFC 8460)
|
||||
|
||||
Note that the following TXT "subtypes" do not have special formatting in labels/name,
|
||||
and thus are not covered by this function:
|
||||
|
||||
* SPF (RFC 4408, RFC 7208)
|
||||
|
||||
[bimi]: https://datatracker.ietf.org/doc/html/draft-brand-indicators-for-message-identification
|
||||
*/
|
||||
func IsFqdnDefinedTxt(fqdn string) (isOk bool) {
|
||||
|
||||
var lbls []string = strings.Split(fqdn, ".")
|
||||
|
||||
if lbls == nil || len(lbls) < 2 {
|
||||
return
|
||||
}
|
||||
|
||||
switch lbls[0] {
|
||||
case "_dmarc", "_mta-sts", "_acme-challenge":
|
||||
if len(lbls) < 2 {
|
||||
return
|
||||
}
|
||||
isOk = IsFqdn(strings.Join(lbls[1:], "."))
|
||||
case "_smtp":
|
||||
if len(lbls) <= 2 {
|
||||
return
|
||||
}
|
||||
if lbls[1] != "_tls" {
|
||||
return
|
||||
}
|
||||
isOk = IsFqdn(strings.Join(lbls[2:], "."))
|
||||
default:
|
||||
if !IsLabel(lbls[0]) {
|
||||
return
|
||||
}
|
||||
switch lbls[1] {
|
||||
case "_domainkey", "_atps", "_bimi":
|
||||
isOk = IsFqdn(strings.Join(lbls[2:], "."))
|
||||
}
|
||||
}
|
||||
|
||||
// TODO
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IsFqdnNsec3 confirms (partially) that s is a valid NSEC3 record name.
|
||||
|
||||
Note that due to the record name being a base32 encoding of a *hash*, the validity
|
||||
can't be 100% confirmed with certainty - only basic checks can be done.
|
||||
|
||||
NSEC3 can be found via:
|
||||
|
||||
* RFC 5155
|
||||
* RFC 6840
|
||||
* RFC 6944
|
||||
* RFC 7129
|
||||
* RFC 8198
|
||||
* RFC 9077
|
||||
* RFC 9157
|
||||
* RFC 9276
|
||||
* RFC 9905
|
||||
|
||||
At the time of writing, only one hashing algorithm (SHA-1) has been specified.
|
||||
However, because this function does not check against the IANA registration at runtime,
|
||||
it's possible that this changes but the library may not immediately reflect this.
|
||||
*/
|
||||
func IsFqdnNsec3(s string) (maybeNsec3 bool) {
|
||||
|
||||
var h []byte
|
||||
var err error
|
||||
var isAscii bool
|
||||
var lbls []string = strings.Split(strings.TrimSuffix(s, "."), ".")
|
||||
|
||||
if !commonFqdn(s) {
|
||||
return
|
||||
}
|
||||
if len(lbls) <= 2 {
|
||||
return
|
||||
}
|
||||
if len(lbls[0]) != 32 { // SHA1 is 160 bits/20 bytes digest, which is always 32 chars in base32(hex)
|
||||
return
|
||||
}
|
||||
if h, err = base32.StdEncoding.DecodeString(strings.ToUpper(lbls[0])); err != nil {
|
||||
return
|
||||
}
|
||||
if isAscii, err = stringsx.IsAsciiSpecial(
|
||||
strings.ToLower(lbls[0]),
|
||||
false, false, false, false,
|
||||
[]byte{
|
||||
// Normally, Base32 goes A-Z, 2-7
|
||||
// but NSEC3 uses Base32Hex (RFC 4648 § 7),
|
||||
// which is 0-9A-V
|
||||
'0', '1', '2', '3', '4', '5',
|
||||
'6', '7', '8', '9', 'a', 'b',
|
||||
'c', 'd', 'e', 'f', 'g', 'h',
|
||||
'j', 'k', 'm', 'n', 'p', 'q',
|
||||
'r', 's', 't', 'u', 'v',
|
||||
},
|
||||
nil,
|
||||
); err != nil {
|
||||
return
|
||||
}
|
||||
if isAscii {
|
||||
return
|
||||
}
|
||||
if len(h) != 20 {
|
||||
return
|
||||
}
|
||||
maybeNsec3 = IsFqdn(strings.Join(lbls[1:], "."))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IsFqdnSrv is like [IsFqdn], but explicitly *only* allows fully-qualified SRV records
|
||||
(i.e. underscores must start the first two labels, and there must be at least two additional
|
||||
labels after these labels).
|
||||
|
||||
Note that the protocol is not checked for validity, as that would require runtime
|
||||
validation against a resource liable to change and would need to be fetched dynamically - see
|
||||
the [IANA Protocol Numbers registry].
|
||||
|
||||
[IANA Protocol Numbers registry]: https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml
|
||||
*/
|
||||
func IsFqdnSrv(s string) (srv bool) {
|
||||
|
||||
var lbls []string = strings.Split(strings.TrimSuffix(s, "."), ".")
|
||||
|
||||
if !commonFqdn(s) {
|
||||
return
|
||||
}
|
||||
if len(lbls) <= 4 {
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(lbls[0], "_") {
|
||||
return
|
||||
}
|
||||
if !strings.HasPrefix(lbls[1], "_") {
|
||||
return
|
||||
}
|
||||
srv = IsFqdn(strings.Join(lbls[2:], "."))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// IsFqdnWildcard is like [IsFqdn] but explicitly *only* allows fully-qualified wildcard records.
|
||||
func IsFqdnWildcard(s string) (wildcard bool) {
|
||||
|
||||
var lbls []string = strings.Split(strings.TrimSuffix(s, "."), ".")
|
||||
|
||||
if len(lbls) < 2 {
|
||||
return
|
||||
}
|
||||
|
||||
if lbls[0] != "*" {
|
||||
return
|
||||
}
|
||||
wildcard = IsFqdn(strings.Join(lbls[1:], "."))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// IsLabel returns true if s is a valid DNS label for standard records.
|
||||
func IsLabel(s string) (isLbl bool) {
|
||||
|
||||
var err error
|
||||
|
||||
if strings.HasPrefix(s, "-") {
|
||||
return
|
||||
}
|
||||
if strings.HasSuffix(s, "-") {
|
||||
return
|
||||
}
|
||||
if isLbl, err = stringsx.IsAsciiSpecial(
|
||||
s,
|
||||
false, false, false, false,
|
||||
[]byte{
|
||||
'a', 'b', 'c', 'd', 'e', 'f',
|
||||
'g', 'h', 'i', 'j', 'k', 'l',
|
||||
'm', 'n', 'o', 'p', 'q', 'r',
|
||||
's', 't', 'u', 'v', 'w', 'x',
|
||||
'y', 'z', '0', '1', '2', '3',
|
||||
'4', '5', '6', '7', '8', '9',
|
||||
'-',
|
||||
},
|
||||
nil,
|
||||
); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
IsPtr returns true if s is a PTR (also called an "rDNS" or "reverse DNS" record) name.
|
||||
|
||||
If true, the IP is returned as well (otherwise it will be nil).
|
||||
*/
|
||||
func IsPtr(s string) (isPtr bool, addr net.IP) {
|
||||
|
||||
var err error
|
||||
var lbls []string = strings.Split(strings.TrimSuffix(s, "."), ".")
|
||||
|
||||
if len(lbls) < 6 {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = AddrFromPtr(s); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
isPtr = true
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// commonFqdn is used to validate some rules common to all record names.
|
||||
func commonFqdn(s string) (isOk bool) {
|
||||
|
||||
var err error
|
||||
var lbl string
|
||||
var isAscii bool
|
||||
var labels []string
|
||||
var domstr string = strings.ToLower(strings.TrimSuffix(s, "."))
|
||||
|
||||
if isAscii, err = stringsx.IsAsciiSpecial(
|
||||
domstr, false, false, false, false,
|
||||
[]byte{
|
||||
'a', 'b', 'c', 'd', 'e', 'f',
|
||||
'g', 'h', 'i', 'j', 'k', 'l',
|
||||
'm', 'n', 'o', 'p', 'q', 'r',
|
||||
's', 't', 'u', 'v', 'w', 'x',
|
||||
'y', 'z', '0', '1', '2', '3',
|
||||
'4', '5', '6', '7', '8', '9',
|
||||
'-', '_', '.',
|
||||
},
|
||||
nil,
|
||||
); err != nil {
|
||||
return
|
||||
}
|
||||
if !isAscii {
|
||||
return
|
||||
}
|
||||
|
||||
if (len(domstr) + 1) > 255 { // +1 for root label
|
||||
return
|
||||
}
|
||||
labels = strings.Split(domstr, ".")
|
||||
|
||||
for _, lbl = range labels {
|
||||
if len(lbl) < 1 || len(lbl) > 63 {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// TODO?
|
||||
|
||||
isOk = true
|
||||
|
||||
return
|
||||
}
|
||||
Reference in New Issue
Block a user