diff --git a/dnscrypt-proxy/example-forwarding-rules.txt b/dnscrypt-proxy/example-forwarding-rules.txt index e6952ec8dd..d8511b63c6 100644 --- a/dnscrypt-proxy/example-forwarding-rules.txt +++ b/dnscrypt-proxy/example-forwarding-rules.txt @@ -10,6 +10,8 @@ ## The following keywords can also be used instead of a server address: ## $BOOTSTRAP to use the default bootstrap resolvers ## $DHCP to use the default DNS resolvers provided by the DHCP server +## $RESOLVCONF: to use the resolvers specified in (with +## resolv.conf syntax); name of mustn't contain any commas (,) ## In order to enable this feature, the "forwarding_rules" property needs to ## be set to this file name inside the main configuration file. @@ -27,10 +29,14 @@ ## Forward *.local to the resolvers provided by the DHCP server # local $DHCP +## Forward *.localnet to the resolvers specified in '/etc/resolv.conf' +# localnet $RESOLVCONF:/etc/resolv.conf + ## Forward *.internal to 192.168.1.1, and if it doesn't work, to the -## DNS from the local DHCP server, and if it still doesn't work, to the -## bootstrap resolvers -# internal 192.168.1.1,$DHCP,$BOOTSTRAP +## DNS from the local DHCP server, and if it that doesn't work, to the +## bootstrap resolvers, and if it still doesn't work, to the resolvers +## specified in '/etc/resolv.conf' +# internal 192.168.1.1,$DHCP,$BOOTSTRAP,$RESOLVCONF:/etc/resolv.conf ## Forward queries for example.com and *.example.com to 9.9.9.9 and 8.8.8.8 # example.com 9.9.9.9,8.8.8.8 diff --git a/dnscrypt-proxy/plugin_forward.go b/dnscrypt-proxy/plugin_forward.go index ff8e4800eb..277ee48ac3 100644 --- a/dnscrypt-proxy/plugin_forward.go +++ b/dnscrypt-proxy/plugin_forward.go @@ -6,10 +6,12 @@ import ( "fmt" "math/rand" "net" + "path" "strings" "sync" "codeberg.org/miekg/dns" + "codeberg.org/miekg/dns/dnsconf" "github.com/jedisct1/dlog" "github.com/lifenjoiner/dhcpdns" ) @@ -20,11 +22,13 @@ const ( Explicit SearchSequenceItemType = iota Bootstrap DHCP + Resolvconf ) type SearchSequenceItem struct { - typ SearchSequenceItemType - servers []string + typ SearchSequenceItemType + servers []string + resolvconf string } type PluginForwardEntry struct { @@ -140,6 +144,17 @@ func (plugin *PluginForward) parseForwardFile(lines string) (bool, []PluginForwa } requiresDHCP = true default: + const resolvconfPrexix = "$RESOLVCONF:" + if strings.HasPrefix(server, resolvconfPrexix) { + file := server[len(resolvconfPrexix):] + if len(file) == 0 { + dlog.Criticalf("File needs to be specified for $RESOLVCONF in line %d", 1+lineNo) + continue + } + sequence = append(sequence, SearchSequenceItem{typ: Resolvconf, resolvconf: path.Clean(file)}) + dlog.Infof("Forwarding [%s] to the servers specified in '%s'", domain, file) + continue + } if strings.HasPrefix(server, "$") { dlog.Criticalf("Unknown keyword [%s] at line %d", server, 1+lineNo) continue @@ -295,6 +310,22 @@ func (plugin *PluginForward) Eval(pluginsState *PluginsState, msg *dns.Msg) erro dlog.Infof("DHCP didn't provide any DNS server to forward [%s]", qName) continue } + case Resolvconf: + resolvconf, err := dnsconf.FromFile(item.resolvconf) + if err != nil { + dlog.Warnf("Failed to open '%s' while resolving [%s]: %v", item.resolvconf, qName, err) + continue + } + if len(resolvconf.Servers) == 0 { + dlog.Warnf("No nameservers specificied in '%s' while resolving [%s]", item.resolvconf, qName) + continue + } + server = resolvconf.Servers[rand.Intn(len(resolvconf.Servers))] + server, err = normalizeIPAndOptionalPort(server, "53") + if err != nil { + dlog.Warnf("Syntax error in address '%s' while resolving [%s]: %v", item.resolvconf, qName, err) + continue + } } pluginsState.serverName = server if len(server) == 0 { diff --git a/vendor/codeberg.org/miekg/dns/dnsconf/clientconfig.go b/vendor/codeberg.org/miekg/dns/dnsconf/clientconfig.go new file mode 100644 index 0000000000..9233eac7d6 --- /dev/null +++ b/vendor/codeberg.org/miekg/dns/dnsconf/clientconfig.go @@ -0,0 +1,139 @@ +// Package dnsconf is used to get the DNS system configuration, typically stored in /etc/resolv.conf on unix +// systems. +package dnsconf + +import ( + "bufio" + "io" + "os" + "slices" + "strconv" + "strings" + + "codeberg.org/miekg/dns/dnsutil" +) + +// Config wraps the contents of the /etc/resolv.conf file. +type Config struct { + Servers []string // Servers to use. + Search []string // Suffixes to append to local name. + Port string // Port to use. + Ndots int // Number of dots in name to trigger absolute lookup. + Timeout int // Seconds before giving up on packet. + Attempts int // Lost packets before giving up on server. +} + +// FromFile parses a resolv.conf(5) like file and returns a [*Config]. +func FromFile(resolvconf string) (*Config, error) { + file, err := os.Open(resolvconf) + if err != nil { + return nil, err + } + defer file.Close() + return FromReader(file) +} + +// FromReader works like [FromFile] but takes an io.Reader as argument. +func FromReader(resolvconf io.Reader) (*Config, error) { + c := new(Config) + scanner := bufio.NewScanner(resolvconf) + c.Servers = make([]string, 0) + c.Search = make([]string, 0) + c.Port = "53" + c.Ndots = 1 + c.Timeout = 5 + c.Attempts = 2 + + for scanner.Scan() { + if err := scanner.Err(); err != nil { + return nil, err + } + line := scanner.Text() + f := strings.Fields(line) + if len(f) < 1 { + continue + } + switch f[0] { + case "nameserver": // add one name server + if len(f) > 1 { + // One more check: make sure server name is + // just an IP address. Otherwise we need DNS + // to look it up. + name := f[1] + c.Servers = append(c.Servers, name) + } + + case "domain": // set search path to just this domain + if len(f) > 1 { + c.Search = make([]string, 1) + c.Search[0] = f[1] + } else { + c.Search = make([]string, 0) + } + + case "search": // set search path to given servers + c.Search = slices.Clone(f[1:]) + + case "options": // magic options + for _, s := range f[1:] { + switch { + case len(s) >= 6 && s[:6] == "ndots:": + n, _ := strconv.Atoi(s[6:]) + if n < 0 { + n = 0 + } else if n > 15 { + n = 15 + } + c.Ndots = n + case len(s) >= 8 && s[:8] == "timeout:": + n, _ := strconv.Atoi(s[8:]) + if n < 1 { + n = 1 + } + c.Timeout = n + case len(s) >= 9 && s[:9] == "attempts:": + n, _ := strconv.Atoi(s[9:]) + if n < 1 { + n = 1 + } + c.Attempts = n + case s == "rotate": + /* not imp */ + } + } + } + } + return c, nil +} + +// NameList returns all of the names that should be queried based on the +// config. It is based off of go's net/dns name building, but it does not +// check the length of the resulting names. +func (c *Config) NameList(name string) []string { + // if this domain is already fully qualified, no append needed. + if dnsutil.IsFqdn(name) { + return []string{name} + } + + // Check to see if the name has more labels than Ndots. Do this before making + // the domain fully qualified. + hasNdots := dnsutil.Labels(name) > c.Ndots + // Make the domain fully qualified. + name = dnsutil.Fqdn(name) + + // Make a list of names based off search. + names := []string{} + + // If name has enough dots, try that first. + if hasNdots { + names = append(names, name) + } + for _, s := range c.Search { + names = append(names, dnsutil.Fqdn(name+s)) + } + // If we didn't have enough dots, try after suffixes. + if !hasNdots { + names = append(names, name) + } + return names +} diff --git a/vendor/codeberg.org/miekg/dns/dnsutil/common.go b/vendor/codeberg.org/miekg/dns/dnsutil/common.go new file mode 100644 index 0000000000..29c0d4d225 --- /dev/null +++ b/vendor/codeberg.org/miekg/dns/dnsutil/common.go @@ -0,0 +1,37 @@ +package dnsutil + +// Common compares the names a and b and returns how many labels they have in common starting +// from the *right*. The comparison stops at the first inequality. For example: +// +// - www.miek.nl. and miek.nl. have two labels in common: miek and nl +// - www.miek.nl. and www.bla.nl. have one label in common: nl +// - . and . have no labels in common. +// +// a and b must be syntactically valid domain names, see [IsName] and [IsFqdn]. +func Common(a, b string) (n int) { + // copy-ish of CompareName + + if a == "." || b == "." { // shortcut root, as we would return 1. + return 0 + } + + labels := 1 + + lasta, _ := Prev(a, 0) + lastb, _ := Prev(b, 0) + + for { + cura, overshota := Prev(a, labels) + curb, overshotb := Prev(b, labels) + if overshota || overshotb { + return labels - 1 + } + x := compareLabel(a[cura:lasta], b[curb:lastb]) + if x != 0 { + return labels - 1 + } + labels++ + lasta = cura + lastb = curb + } +} diff --git a/vendor/codeberg.org/miekg/dns/dnsutil/compat.go b/vendor/codeberg.org/miekg/dns/dnsutil/compat.go new file mode 100644 index 0000000000..94c313458b --- /dev/null +++ b/vendor/codeberg.org/miekg/dns/dnsutil/compat.go @@ -0,0 +1,66 @@ +package dnsutil + +import "codeberg.org/miekg/dns" + +// SetQuestion set the question section in the message m. +// It generates an ID and sets the RecursionDesired (RD) bit to true. +// If the type t isn't known to this library, nil is returned. Also see [dns.NewMsg]. +func SetQuestion(m *dns.Msg, z string, t uint16) *dns.Msg { + m.ID = dns.ID() + m.RecursionDesired = true + var rr dns.RR + newFn, ok := dns.TypeToRR[t] + if !ok { + return nil + } + rr = newFn() + rr.Header().Name = z + rr.Header().Class = dns.ClassINET + + m.Question = []dns.RR{rr} + return m +} + +// Question returns the question name and the type from the message m. +func Question(m *dns.Msg) (z string, t uint16) { + z = m.Question[0].Header().Name + t = dns.RRToType(m.Question[0]) + return z, t +} + +// SetReply creates a reply message from r. It copies the ID, opcode, rcode and question, r's Data buffer is not copied. +// In the header the RecursionDesired, CheckingDisabled and Security bit are copied. All other sections are +// resliced to length zero. +func SetReply(m, r *dns.Msg) *dns.Msg { + m.ID = r.ID + m.Response = true + m.Opcode = r.Opcode + if m.Opcode == dns.OpcodeQuery { + m.RecursionDesired = r.RecursionDesired + m.CheckingDisabled = r.CheckingDisabled + m.Security = r.Security + } + m.Rcode = dns.RcodeSuccess + m.Question = r.Question + m.Reset() + return m +} + +// IsRRset reports whether a set of RRs is a valid RRset as defined by RFC 2181. +// This means the RRs need to have the same type, name, and class. Duplicate RRs are not detected. +// See [dns.RRset] if you need to sort an RRset. +func IsRRset(rrset []dns.RR) bool { + if len(rrset) == 0 { + return false + } + base := rrset[0].Header() + basetype := dns.RRToType(rrset[0]) + for _, rr := range rrset[1:] { + h := rr.Header() + htype := dns.RRToType(rr) + if htype != basetype || h.Class != base.Class || !dns.EqualName(h.Name, base.Name) { + return false + } + } + return true +} diff --git a/vendor/codeberg.org/miekg/dns/dnsutil/dnsutil.go b/vendor/codeberg.org/miekg/dns/dnsutil/dnsutil.go new file mode 100644 index 0000000000..7e6355d4b9 --- /dev/null +++ b/vendor/codeberg.org/miekg/dns/dnsutil/dnsutil.go @@ -0,0 +1,18 @@ +// Package dnsutil contains function that are useful in the context of working with the DNS. +package dnsutil + +// Trim removes the zone component from s. It returns the trimmed name or the empty string if z is longer than s. +// The trimmed name will be returned without a trailing dot. +// s and z must be syntactically valid domain names, see [IsName] and [IsFqdn]. +func Trim(s, z string) string { + i, overshot := Prev(s, Labels(z)) + if overshot || i-1 < 0 { + return "" + } + // This includes the '.', remove on return. + return s[:i-1] +} + +// IsBelow checks if child sits below parent in the DNS tree, i.e. check if the child is a sub-domain of +// parent. If child and parent are at the same level, true is returned as well. +func IsBelow(parent, child string) bool { return Common(parent, child) == Labels(parent) } diff --git a/vendor/codeberg.org/miekg/dns/dnsutil/labels.go b/vendor/codeberg.org/miekg/dns/dnsutil/labels.go new file mode 100644 index 0000000000..cfe9663303 --- /dev/null +++ b/vendor/codeberg.org/miekg/dns/dnsutil/labels.go @@ -0,0 +1,15 @@ +package dnsutil + +import ( + "strings" +) + +// Join joins the labels in s to form a fully qualified domain name. If the last label is the root label it is +// ignored. No other syntax checks are performed, each label should be a valid, relative name (i.e. not end in +// a dot), see [IsName]. +func Join(ls ...string) string { + if ls[len(ls)-1] == "." { + return Fqdn(strings.Join(ls[:len(ls)-1], ".")) + } + return Fqdn(strings.Join(ls, ".")) +} diff --git a/vendor/codeberg.org/miekg/dns/dnsutil/msg.go b/vendor/codeberg.org/miekg/dns/dnsutil/msg.go new file mode 100644 index 0000000000..1ebed84451 --- /dev/null +++ b/vendor/codeberg.org/miekg/dns/dnsutil/msg.go @@ -0,0 +1,198 @@ +package dnsutil + +import ( + "bufio" + "fmt" + "strconv" + "strings" + + "codeberg.org/miekg/dns" +) + +type state int + +const ( + stateNone state = iota // parse the first line + stateHeader // optional EDNS0 header or the question section + stateQuestion + statePseudo + stateAnswer + stateAuthority + stateAdditional +) + +// StringToMsg convert a string as created by [Msg.String] back to an dns message. If the parsing fails and +// error is returned. +// The ";; QUESTION: 1, PSEUDO: 0, ANSWER: 5, AUTHORITY: 0, ADDITIONAL: 0, DATA SIZE: 0" line is skipped when +// encountered. +func StringToMsg(s string) (*dns.Msg, error) { + m := new(dns.Msg) + state := stateNone + + // We have an RR or + // ;; stuff, stuff2: more, (comma separated) + // ;; SECTION: + // It's line by line, so that simplifies things + scanner := bufio.NewScanner(strings.NewReader(s)) // Maybe not use a scanner? + + for scanner.Scan() { + line := scanner.Text() + if len(line) == 0 { + continue + } + if strings.HasPrefix(line, ";; QUESTION:") { + // this is the section count line, we don't need it + continue + } + + // TODO(miek): dynamic updates? + if strings.HasPrefix(line, ";; QUESTION SECTION:") { + state = stateQuestion + continue + } + if strings.HasPrefix(line, ";; PSEUDO SECTION:") { + state = statePseudo + continue + } + if strings.HasPrefix(line, ";; ANSWER SECTION:") { + state = stateAnswer + continue + } + if strings.HasPrefix(line, ";; AUTHORITY SECTION:") { + state = stateAuthority + continue + } + if strings.HasPrefix(line, ";; ADDITIONAL SECTION:") { + state = stateAdditional + continue + } + + // only here when to parse an RR, header is done above + switch state { + case stateNone: + // parse ;; QUERY, rcode: NOERROR, id, flags: .... + if len(line) < 39 { + return nil, fmt.Errorf("bad opcode: %q", line) + } + // we need 3 ", " in this line. + opcode := strings.Index(line, ", ") + if opcode == -1 || opcode == len(line)-1 { + return nil, fmt.Errorf("bad opcode") + } + rcode := strings.Index(line[opcode+1:], ", ") + if rcode == -1 || rcode == len(line[opcode+1:])-1 { + return nil, fmt.Errorf("bad rcode: %q", line[opcode+1:]) + } + + rcode += opcode + 3 + id := strings.Index(line[rcode:], ", ") + if id == -1 || id == len(line[rcode:])-1 { + return nil, fmt.Errorf("bad id: %q", line[rcode:]) + } + id += rcode + switch line[:opcode] { + case ";; QUERY": + m.Opcode = dns.OpcodeQuery + case ";; NOTIFY": + m.Opcode = dns.OpcodeNotify + default: + return nil, fmt.Errorf("bad opcode") + } + + m.Rcode = dns.StringToRcode[line[opcode+9:rcode-2]] + + val, _ := strconv.Atoi(line[rcode+4 : id]) + m.ID = uint16(val) + + if !strings.HasPrefix(line[id+2:], "flags:") { + return nil, fmt.Errorf("bad flags") + } + flags := line[id+9:] + " " + j := 0 + for i := strings.Index(flags, " "); i > 0; i = strings.Index(flags[j:], " ") { + switch { + case strings.HasPrefix(flags[j:], "qr"): + m.Response = true + case strings.HasPrefix(flags[j:], "aa"): + m.Authoritative = true + case strings.HasPrefix(flags[j:], "tc"): + m.Truncated = true + case strings.HasPrefix(flags[j:], "rd"): + m.RecursionDesired = true + case strings.HasPrefix(flags[j:], "ra"): + m.RecursionAvailable = true + case strings.HasPrefix(flags[j:], "z"): + m.Zero = true + case strings.HasPrefix(flags[j:], "ad"): + m.AuthenticatedData = true + case strings.HasPrefix(flags[j:], "cd"): + m.CheckingDisabled = true + case strings.HasPrefix(flags[j:], "do"): + m.Security = true + case strings.HasPrefix(flags[j:], "co"): + m.CompactAnswers = true + case strings.HasPrefix(flags[j:], "de"): + m.Delegation = true + } + j += i + 1 + } + + state = stateHeader + + case stateHeader: + // only here *if* we have not seen a question so this is the ;; EDNS line + size := strings.Index(line, "udp: ") + if size == -1 || size == len(line)-1 { + return nil, fmt.Errorf("bad udp size") + } + val, _ := strconv.Atoi(line[size+5:]) + m.UDPSize = uint16(val) + + case stateQuestion: + rr, err := dns.New(line) + if err != nil { + return nil, err + } + if rr != nil { + m.Question = append(m.Question, rr) + } + case statePseudo: + rr, err := dns.New(line) + if err != nil { + return nil, err + } + if rr != nil { + m.Pseudo = append(m.Pseudo, rr) + } + case stateAnswer: + rr, err := dns.New(line) + if err != nil { + return nil, err + } + if rr != nil { + m.Answer = append(m.Answer, rr) + } + case stateAuthority: + rr, err := dns.New(line) + if err != nil { + return nil, err + } + if rr != nil { + m.Ns = append(m.Ns, rr) + } + case stateAdditional: + rr, err := dns.New(line) + if err != nil { + return nil, err + } + if rr != nil { + m.Extra = append(m.Extra, rr) + } + } + } + + if err := scanner.Err(); err != nil { + return nil, err + } + return m, nil +} diff --git a/vendor/codeberg.org/miekg/dns/dnsutil/nsec3.go b/vendor/codeberg.org/miekg/dns/dnsutil/nsec3.go new file mode 100644 index 0000000000..b36fe7d745 --- /dev/null +++ b/vendor/codeberg.org/miekg/dns/dnsutil/nsec3.go @@ -0,0 +1,36 @@ +package dnsutil + +import ( + "crypto/sha1" + "encoding/base32" + "encoding/hex" + + "codeberg.org/miekg/dns/internal/pack" +) + +// NSEC3Name returns the hashed owner name according to RFC 5155. +func NSEC3Name(s, salt string, iter uint16) string { + hashdata := make([]byte, hex.DecodedLen(len(salt))+255) + n, err := pack.Name(s, hashdata, 0, nil, false) + if err != nil { + return "" + } + m, err := pack.StringHex(salt, hashdata[n:], 0) + if err != nil { + return "" + } + hashdata = hashdata[:n+m] + + hash := sha1.New() + // k = 0 + hash.Write(hashdata) + nsec3 := hash.Sum(nil) + + for range iter { + hash.Reset() + hash.Write(nsec3) + hash.Write(hashdata[n:]) + nsec3 = hash.Sum(nil) + } + return base32.HexEncoding.WithPadding(base32.NoPadding).EncodeToString(nsec3) +} diff --git a/vendor/codeberg.org/miekg/dns/dnsutil/response.go b/vendor/codeberg.org/miekg/dns/dnsutil/response.go new file mode 100644 index 0000000000..9418250a74 --- /dev/null +++ b/vendor/codeberg.org/miekg/dns/dnsutil/response.go @@ -0,0 +1,89 @@ +package dnsutil + +import ( + "net" + "net/netip" + "strconv" + + "codeberg.org/miekg/dns" +) + +// RemoteIP returns the IP address of the client making the request. +func RemoteIP(w dns.ResponseWriter) string { + switch t := w.RemoteAddr().(type) { + case *net.UDPAddr: + return t.AddrPort().Addr().String() + case *net.TCPAddr: + return t.AddrPort().Addr().String() + } + return "" +} + +// LocalIP gets the IP address of server handling the request. +func LocalIP(w dns.ResponseWriter) string { + switch t := w.LocalAddr().(type) { + case *net.UDPAddr: + return t.AddrPort().Addr().String() + case *net.TCPAddr: + return t.AddrPort().Addr().String() + } + return "" +} + +// RemotePort gets the port of the client making the request. +func RemotePort(w dns.ResponseWriter) string { + switch t := w.RemoteAddr().(type) { + case *net.UDPAddr: + return strconv.Itoa(t.Port) + case *net.TCPAddr: + return strconv.Itoa(t.Port) + } + return "" +} + +// LocalPort gets the local port of the server handling the request. +func LocalPort(w dns.ResponseWriter) string { + switch t := w.LocalAddr().(type) { + case *net.UDPAddr: + return strconv.Itoa(t.Port) + case *net.TCPAddr: + return strconv.Itoa(t.Port) + } + return "" +} + +// Network returns the network used to make the request, this can be udp or tcp. +func Network(w dns.ResponseWriter) string { + switch w.RemoteAddr().(type) { + case *net.UDPAddr: + return "udp" + case *net.TCPAddr: + return "tcp" + } + return "udp" +} + +// Family returns the family of the transport, which is either [IPv4Family] or [IPv6Family] as defined by IANA. +func Family(w dns.ResponseWriter) int { + var a netip.Addr + switch t := w.RemoteAddr().(type) { + case *net.UDPAddr: + a = t.AddrPort().Addr() + case *net.TCPAddr: + a = t.AddrPort().Addr() + } + + if a.Is4In6() { + return IPv4Family + } + if a.Is4() { + return IPv4Family + } + return IPv6Family +} + +// The IP address families are defined by IANA, and can be found at https://www.iana.org/assignments/address-family-numbers/address-family-numbers.xhtml +const ( + IPv4Family = 1 + IPv6Family = 2 +) diff --git a/vendor/codeberg.org/miekg/dns/dnsutil/reverse.go b/vendor/codeberg.org/miekg/dns/dnsutil/reverse.go new file mode 100644 index 0000000000..e30298736d --- /dev/null +++ b/vendor/codeberg.org/miekg/dns/dnsutil/reverse.go @@ -0,0 +1,146 @@ +package dnsutil + +import ( + "net" + "net/netip" + "strconv" + "strings" +) + +const ( + // IP4arpa is the reverse tree suffix for v4 IP addresses. + IP4arpa = ".in-addr.arpa." + // IP6arpa is the reverse tree suffix for v6 IP addresses. + IP6arpa = ".ip6.arpa." +) + +// IsReverse returns 0 if name is not a reverse zone. Anything > 0 indicates +// name is in a reverse zone. The returned integer will be [IPv4Family] for in-addr.arpa, (IPv4). +// and [IPv6Family] for ip6.arpa, (IPv6). see [Family]. A valid name is assumed. +func IsReverse(s string) int { + if strings.HasSuffix(s, IP4arpa) { + return IPv4Family + } + if strings.HasSuffix(s, IP6arpa) { + return IPv6Family + } + return 0 +} + +// ReverseAddr returns the in-addr.arpa. or ip6.arpa. hostname of the IP +// address suitable for reverse DNS ([dns.PTR]) record lookups. Also see [AddrReverse]. +func ReverseAddr(ip netip.Addr) (arpa string) { + const hexDigit = "0123456789abcdef" + + if ip.Is4() { + v4 := ip.As4() + buf := make([]byte, 0, net.IPv4len*4+len(IP4arpa)) + // Add it, in reverse, to the buffer + for i := len(v4) - 1; i >= 0; i-- { + buf = strconv.AppendInt(buf, int64(v4[i]), 10) + buf = append(buf, '.') + } + // Append "in-addr.arpa." and return (buf already has the final .) + buf = append(buf, IP4arpa[1:]...) + return string(buf) + } + // Must be IPv6 + buf := make([]byte, 0, net.IPv6len*4+len(IP6arpa)) + v6 := ip.As16() + // Add it, in reverse, to the buffer + for i := len(v6) - 1; i >= 0; i-- { + v := v6[i] + buf = append(buf, hexDigit[v&0xF], '.', hexDigit[v>>4], '.') + } + // Append "ip6.arpa." and return (buf already has the final .) + buf = append(buf, IP6arpa[1:]...) + return string(buf) +} + +// AddrReverse turns a standard [dns.PTR] reverse record name into an IP address. +// 54.119.58.176.in-addr.arpa. becomes 176.58.119.54. If the conversion +// fails nil is returned. Also see [ReverseAddr]. +func AddrReverse(s string) (ip netip.Addr) { + switch IsReverse(s) { + case IPv4Family: + var v4 [4]byte + idx := 0 + // Loop backwards through the bytes of the IPv4 address (d, c, b, a) + // which appear in forward order in the reverse name (a.b.c.d). + // e.g. 54.119.58.176.in-addr.arpa. -> 176.58.119.54 + // 54 (byte 3) is first, 176 (byte 0) is last. + for i := 3; i >= 0; i-- { + if idx >= len(s) { + return netip.Addr{} + } + if s[idx] < '0' || s[idx] > '9' { + return netip.Addr{} + } + n := 0 + for idx < len(s) && s[idx] >= '0' && s[idx] <= '9' { + n = n*10 + int(s[idx]-'0') + if n > 255 { + return netip.Addr{} + } + idx++ + } + v4[i] = byte(n) + + // Consumed number, expect a dot. + if idx >= len(s) || s[idx] != '.' { + return netip.Addr{} + } + idx++ + } + // The remainder must be exactly "in-addr.arpa." + if s[idx:] != "in-addr.arpa." { + return netip.Addr{} + } + return netip.AddrFrom4(v4) + + case IPv6Family: + var v6 [16]byte + idx := 0 + // 32 nibbles. + // Reverse name: low nibble of byte 15, high nibble of byte 15, ... + for i := range 32 { + if idx >= len(s) { + return netip.Addr{} + } + c := s[idx] + var val byte + switch { + case c >= '0' && c <= '9': + val = c - '0' + case c >= 'a' && c <= 'f': + val = c - 'a' + 10 + case c >= 'A' && c <= 'F': + val = c - 'A' + 10 + default: + return netip.Addr{} + } + + // i=0 -> byte 15, low part + // i=1 -> byte 15, high part + // i=2 -> byte 14, low part + pos := 15 - (i / 2) + if i%2 == 0 { + v6[pos] |= val + } else { + v6[pos] |= val << 4 + } + + idx++ + if idx >= len(s) || s[idx] != '.' { + return netip.Addr{} + } + idx++ + } + if s[idx:] != "ip6.arpa." { + return netip.Addr{} + } + return netip.AddrFrom16(v6) + default: + return netip.Addr{} + } +} diff --git a/vendor/codeberg.org/miekg/dns/dnsutil/shared.go b/vendor/codeberg.org/miekg/dns/dnsutil/shared.go new file mode 100644 index 0000000000..38062c92a9 --- /dev/null +++ b/vendor/codeberg.org/miekg/dns/dnsutil/shared.go @@ -0,0 +1,219 @@ +package dnsutil + +import ( + "strings" + "time" +) + +// This is copied to zdnsutil.go in the main package to also have access to these functions and not have an +// import cycle. See dnsutil_generate.go. +// +// This file SHOULD NOT import dns things, as that leads to the impossibility to use it from svcb/ and/or +// deleg/. + +// Labels returns the number of labels in the name s. +func Labels(s string) (labels int) { + if s == "." { + return + } + off := 0 + end := false + for { + off, end = Next(s, off) + labels++ + if end { + return + } + } +} + +// Next returns the index of the start of the next label in the string s starting at offset. A negative offset +// will cause a panic. The bool end is true when the end of the string has been reached. Also see [Prev]. +func Next(s string, offset int) (i int, end bool) { + if s == "" { + return 0, true + } + for i = offset; i < len(s)-1; i++ { + if s[i] != '.' { + continue + } + return i + 1, false + } + return i + 1, true +} + +// Prev returns the index of the label when starting from the right and jumping n labels to the left. +// The bool start is true when the start of the string has been overshot. Also see [Next]. +func Prev(s string, n int) (i int, start bool) { + if s == "" { + return 0, true + } + if n == 0 { + return len(s), false + } + + l := len(s) - 1 + if s[l] == '.' { + l-- + } + + for ; l >= 0 && n > 0; l-- { + if s[l] != '.' { + continue + } + n-- + if n == 0 { + return l + 1, false + } + } + + return 0, n > 1 +} + +// Fqdn return the fully qualified domain name from s. If s is already fully qualified, it behaves as the +// identity function. +func Fqdn(s string) string { + if IsFqdn(s) { + return s + } + return s + "." +} + +// IsFqdn checks if a domain name is fully qualified. As this library doesn't support escapes in names, this +// simply calls strings.HasSuffix. +func IsFqdn(s string) bool { return strings.HasSuffix(s, ".") } + +// Canonical returns the domain name in canonical form. A name in canonical form is lowercase and fully qualified. +// Only US-ASCII letters are affected. See Section 6.2 in RFC 4034. +func Canonical(s string) string { + return strings.Map(func(r rune) rune { + if r >= 'A' && r <= 'Z' { + r += 'a' - 'A' + } + return r + }, Fqdn(s)) +} + +// IsName checks if s is a valid domain name. A non fully qualified domain name is considered valid. +// Note that this function is extremely liberal; almost any string is a valid domain name as the DNS is 8 bit +// protocol. It checks if each label fits in 63 characters and that the entire name will fit into the 255 +// octet wire-format limit. +func IsName(s string) bool { + // XXX: The logic in this function was copied from pack.Name and should be kept in sync with that function. + const lenmsg = 256 + ls := len(s) + + if ls == 1 && s[0] == '.' { + return true + } + + if ls > 1 && s[0] == '.' { + return false + } + + var ( + off int + begin int + ) + for begin < ls { + i := strings.IndexByte(s[begin:], '.') + if i == -1 { + break + } + i += begin + + labelLen := i - begin + // top two bits of length must be clear and two dots back to back is not legal + if labelLen == 0 || labelLen >= 1<<6 { + return false + } + // off can already (we're in a loop) be bigger than lenmsg this happens when a name isn't fully qualified + off += 1 + labelLen + if off > lenmsg { + return false + } + begin = i + 1 + } + return true +} + +// compareLabel compares a and b while ignoring case. It returns 0 when equal, -1 when a is smaller than b, +// and +1 when a is greater then b. This ends up a compareLabel in the dns package too as generated by +// dnsutil_generate.go. +func compareLabel(a, b string) int { + la, lb := len(a), len(b) + for i := range min(la, lb) { + ai := a[i] + bi := b[i] + if ai >= 'A' && ai <= 'Z' { + ai |= 'a' - 'A' + } + if bi >= 'A' && bi <= 'Z' { + bi |= 'a' - 'A' + } + if ai < bi { + return -1 + } + if ai > bi { + return +1 + } + } + if la < lb { + return -1 + } + if la > lb { + return +1 + } + return 0 +} + +// TimeToString translates the RRSIG's incep. and expir. times to the +// string representation used when printing the record. It takes serial arithmetic (RFC 1982) into account. +func TimeToString(t uint32) string { + mod := max((int64(t)-time.Now().Unix())/maxSerialIncrement-1, 0) + ti := time.Unix(int64(t)-mod*maxSerialIncrement, 0).UTC() + return ti.Format("20060102150405") +} + +// StringToTime translates the RRSIG's incep. and expir. times from string values like "20110403154150" to an 32 bit integer. +// It takes serial arithmetic (RFC 1982) into account. +func StringToTime(s string) (uint32, error) { + t, err := time.Parse("20060102150405", s) + if err != nil { + return 0, err + } + mod := max(t.Unix()/maxSerialIncrement-1, 0) + return uint32(t.Unix() - mod*maxSerialIncrement), nil +} + +// Absolute takes the name and origin and appends the origin to the name. This takes the 1035 presentation +// format into account, i.e. "@" means the origin in a name. +// If s is not a valid domain name, the empty string is returned. If the origin is needed to be appended, +// but is empty the empty string is also returned. +func Absolute(s, origin string) string { + if s == "@" { + if origin == "" { + return "" + } + return origin + } + if s == "\n" || s == "" { // this can happen when a zone is parsed, internal quirk, should not be here... + return "" + } + if IsName(s) == false { // done to make the conversion via dnsutil_generate.go work, instead of !IsName(s) + return "" + } + if IsFqdn(s) { + return s + } + if origin == "" { + return "" + } + if origin == "." { + return s + origin + } + return s + "." + origin +} + +// maxSerialIncrement is the maximum difference between two serial numbers. See RFC 1982. +const maxSerialIncrement = 2147483647 diff --git a/vendor/codeberg.org/miekg/dns/dnsutil/smimea.go b/vendor/codeberg.org/miekg/dns/dnsutil/smimea.go new file mode 100644 index 0000000000..dafbeca5c9 --- /dev/null +++ b/vendor/codeberg.org/miekg/dns/dnsutil/smimea.go @@ -0,0 +1,18 @@ +package dnsutil + +import ( + "crypto/sha256" + "encoding/hex" +) + +// SMIMEAName returns the ownername of a SMIMEA resource record as per the +// format specified in RFC 'draft-ietf-dane-smime-12' Section 2 and 3. +func SMIMEAName(s, mail string) (string, error) { + h := sha256.New() + h.Write([]byte(s)) + + // RFC Section 3: "The local-part is hashed using the SHA2-256 algorithm with the hash truncated to 28 + // octets and represented in its hexadecimal representation to become the left-most label in the prepared + // domain name" + return hex.EncodeToString(h.Sum(nil)[:28]) + "." + "_smimecert." + s, nil +} diff --git a/vendor/codeberg.org/miekg/dns/dnsutil/string.go b/vendor/codeberg.org/miekg/dns/dnsutil/string.go new file mode 100644 index 0000000000..c126340b67 --- /dev/null +++ b/vendor/codeberg.org/miekg/dns/dnsutil/string.go @@ -0,0 +1,47 @@ +package dnsutil + +import ( + "strconv" + + "codeberg.org/miekg/dns" +) + +// TypeToString converts the type to the text presentation, or to "TYPE"+value if the type is unknown. +func TypeToString(t uint16) string { + if t1, ok := dns.TypeToString[t]; ok { + return t1 + } + return "TYPE" + strconv.Itoa(int(t)) +} + +// RcodeToString converts the code to the text presentation, or to "RCODE"+value if the rcode is unknown. +func RcodeToString(r uint16) string { + if r1, ok := dns.RcodeToString[r]; ok { + return r1 + } + return "RCODE" + strconv.Itoa(int(r)) +} + +// ClassToString converts the class to the text presentation, or to "CLASS"+value if the class is unknown. +func ClassToString(c uint16) string { + if c1, ok := dns.ClassToString[c]; ok { + return c1 + } + return "CLASS" + strconv.Itoa(int(c)) +} + +// OpcodeToString converts the opcode to the text presentation, or to "OPCODE"+value if the opcode is unknown. +func OpcodeToString(o uint8) string { + if o1, ok := dns.OpcodeToString[o]; ok { + return o1 + } + return "OPCODE" + strconv.Itoa(int(o)) +} + +// CodeToString converts the ENDS0 code to the text presentation, or to "CODE"+value if the code is unknown. +func CodeToString(c uint16) string { + if c1, ok := dns.CodeToString[c]; ok { + return c1 + } + return "CODE" + strconv.Itoa(int(c)) +} diff --git a/vendor/codeberg.org/miekg/dns/dnsutil/tsla.go b/vendor/codeberg.org/miekg/dns/dnsutil/tsla.go new file mode 100644 index 0000000000..83991cc44a --- /dev/null +++ b/vendor/codeberg.org/miekg/dns/dnsutil/tsla.go @@ -0,0 +1,19 @@ +package dnsutil + +import ( + "fmt" + "net" + "strconv" +) + +// TLSAName returns the ownername of a TLSA resource record as per the rules specified in RFC 6698, Section 3. +func TLSAName(s, service, network string) (string, error) { + if !IsFqdn(s) { + return "", fmt.Errorf("dnsutil: domain must be fully qualified") + } + p, err := net.LookupPort(network, service) + if err != nil { + return "", err + } + return "_" + strconv.Itoa(p) + "._" + network + "." + s, nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 77cc9ee4d8..64df67a3c5 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -2,6 +2,8 @@ ## explicit; go 1.25.0 codeberg.org/miekg/dns codeberg.org/miekg/dns/deleg +codeberg.org/miekg/dns/dnsconf +codeberg.org/miekg/dns/dnsutil codeberg.org/miekg/dns/internal/ddd codeberg.org/miekg/dns/internal/dnslex codeberg.org/miekg/dns/internal/dnsstring