diff --git a/dnscrypt-proxy/example-forwarding-rules.txt b/dnscrypt-proxy/example-forwarding-rules.txt index fbef4bc1..4577b28b 100644 --- a/dnscrypt-proxy/example-forwarding-rules.txt +++ b/dnscrypt-proxy/example-forwarding-rules.txt @@ -7,21 +7,31 @@ ## [:port] [, [:port]...] ## IPv6 addresses can be specified by enclosing the address in square brackets. +## 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 + ## In order to enable this feature, the "forwarding_rules" property needs to ## be set to this file name inside the main configuration file. ## Blocking IPv6 may prevent local devices from being discovered. ## If this happens, set `block_ipv6` to `false` in the main config file. -## Forward *.lan, *.local, *.home, *.home.arpa, *.internal and *.localdomain to 192.168.1.1 +## Forward *.lan, *.home, *.home.arpa, and *.localdomain to 192.168.1.1 # lan 192.168.1.1 -# local 192.168.1.1 # home 192.168.1.1 # home.arpa 192.168.1.1 -# internal 192.168.1.1 # localdomain 192.168.1.1 # 192.in-addr.arpa 192.168.1.1 +## Forward *.local to the resolvers provided by the DHCP server +# local $DHCP + +## 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 + ## 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 cadba60f..d5d91cc1 100644 --- a/dnscrypt-proxy/plugin_forward.go +++ b/dnscrypt-proxy/plugin_forward.go @@ -7,16 +7,32 @@ import ( "strings" "github.com/jedisct1/dlog" + "github.com/lifenjoiner/dhcpdns" "github.com/miekg/dns" ) -type PluginForwardEntry struct { - domain string +type SearchSequenceItemType int + +const ( + Explicit SearchSequenceItemType = iota + Bootstrap + DHCP +) + +type SearchSequenceItem struct { + typ SearchSequenceItemType servers []string } +type PluginForwardEntry struct { + domain string + sequence []SearchSequenceItem +} + type PluginForward struct { - forwardMap []PluginForwardEntry + forwardMap []PluginForwardEntry + bootstrapResolvers []string + dhcpdns []*dhcpdns.Detector } func (plugin *PluginForward) Name() string { @@ -29,6 +45,11 @@ func (plugin *PluginForward) Description() string { func (plugin *PluginForward) Init(proxy *Proxy) error { dlog.Noticef("Loading the set of forwarding rules from [%s]", proxy.forwardFile) + + if proxy.xTransport != nil { + plugin.bootstrapResolvers = proxy.xTransport.bootstrapResolvers + } + lines, err := ReadTextFile(proxy.forwardFile) if err != nil { return err @@ -46,27 +67,77 @@ func (plugin *PluginForward) Init(proxy *Proxy) error { ) } domain = strings.ToLower(domain) - var servers []string + requiresDHCP := false + var sequence []SearchSequenceItem for _, server := range strings.Split(serversStr, ",") { server = strings.TrimSpace(server) - server = strings.TrimPrefix(server, "[") - server = strings.TrimSuffix(server, "]") - if ip := net.ParseIP(server); ip != nil { - if ip.To4() != nil { - server = fmt.Sprintf("%s:%d", server, 53) - } else { - server = fmt.Sprintf("[%s]:%d", server, 53) + switch server { + case "$BOOTSTRAP": + if len(plugin.bootstrapResolvers) == 0 { + return fmt.Errorf( + "Syntax error for a forwarding rule at line %d. No bootstrap resolvers available", + 1+lineNo, + ) } + if len(sequence) > 0 && sequence[len(sequence)-1].typ == Bootstrap { + // Ignore repetitions + } else { + sequence = append(sequence, SearchSequenceItem{typ: Bootstrap}) + dlog.Infof("Forwarding [%s] to the bootstrap servers", domain) + } + case "$DHCP": + if len(sequence) > 0 && sequence[len(sequence)-1].typ == DHCP { + // Ignore repetitions + } else { + sequence = append(sequence, SearchSequenceItem{typ: Bootstrap}) + dlog.Infof("Forwarding [%s] to the DHCP servers", domain) + } + requiresDHCP = true + default: + if strings.HasPrefix(server, "$") { + dlog.Criticalf("Unknown keyword [%s] at line %d", server, 1+lineNo) + continue + } + server = strings.TrimPrefix(server, "[") + server = strings.TrimSuffix(server, "]") + if ip := net.ParseIP(server); ip != nil { + if ip.To4() != nil { + server = fmt.Sprintf("%s:%d", server, 53) + } else { + server = fmt.Sprintf("[%s]:%d", server, 53) + } + } + idxServers := -1 + for i, item := range sequence { + if item.typ == Explicit { + idxServers = i + } + } + if idxServers == -1 { + sequence = append(sequence, SearchSequenceItem{typ: Explicit, servers: []string{server}}) + } else { + sequence[idxServers].servers = append(sequence[idxServers].servers, server) + } + dlog.Infof("Forwarding [%s] to [%s]", domain, server) } - dlog.Infof("Forwarding [%s] to %s", domain, server) - servers = append(servers, server) } - if len(servers) == 0 { - continue + if requiresDHCP { + if proxy.SourceIPv6 { + dlog.Info("Starting a DHCP/DNS detector for IPv6") + d6 := &dhcpdns.Detector{RemoteIPPort: "[2001:DB8::53]:80"} + go d6.Serve(9, 10) + plugin.dhcpdns = append(plugin.dhcpdns, d6) + } + if proxy.SourceIPv4 { + dlog.Info("Starting a DHCP/DNS detector for IPv4") + d4 := &dhcpdns.Detector{RemoteIPPort: "192.0.2.53:80"} + go d4.Serve(9, 10) + plugin.dhcpdns = append(plugin.dhcpdns, d4) + } } plugin.forwardMap = append(plugin.forwardMap, PluginForwardEntry{ - domain: domain, - servers: servers, + domain: domain, + sequence: sequence, }) } return nil @@ -83,7 +154,7 @@ func (plugin *PluginForward) Reload() error { func (plugin *PluginForward) Eval(pluginsState *PluginsState, msg *dns.Msg) error { qName := pluginsState.qName qNameLen := len(qName) - var servers []string + var sequence []SearchSequenceItem for _, candidate := range plugin.forwardMap { candidateLen := len(candidate.domain) if candidateLen > qNameLen { @@ -92,33 +163,78 @@ func (plugin *PluginForward) Eval(pluginsState *PluginsState, msg *dns.Msg) erro if (qName[qNameLen-candidateLen:] == candidate.domain && (candidateLen == qNameLen || (qName[qNameLen-candidateLen-1] == '.'))) || (candidate.domain == ".") { - servers = candidate.servers + sequence = candidate.sequence break } } - if len(servers) == 0 { + if len(sequence) == 0 { return nil } - server := servers[rand.Intn(len(servers))] - pluginsState.serverName = server - client := dns.Client{Net: pluginsState.serverProto, Timeout: pluginsState.timeout} - respMsg, _, err := client.Exchange(msg, server) - if err != nil { - return err - } - if respMsg.Truncated { - client.Net = "tcp" + var err error + var respMsg *dns.Msg + var tries = 4 + for _, item := range sequence { + var server string + switch item.typ { + case Explicit: + server = item.servers[rand.Intn(len(item.servers))] + pluginsState.serverName = server + case Bootstrap: + server = plugin.bootstrapResolvers[rand.Intn(len(plugin.bootstrapResolvers))] + pluginsState.serverName = "[BOOTSTRAP]" + case DHCP: + const maxInconsistency = 9 + for _, dhcpdns := range plugin.dhcpdns { + inconsistency, ip, dhcpDNS, err := dhcpdns.Status() + if err != nil && ip != "" && inconsistency > maxInconsistency { + dhcpDNS = nil + } + if len(dhcpDNS) > 0 { + server = net.JoinHostPort(dhcpDNS[rand.Intn(len(dhcpDNS))].String(), "53") + break + } + } + if len(server) == 0 { + dlog.Warn("DHCP didn't provide any DNS server") + continue + } + pluginsState.serverName = "[DHCP]" + } + if len(server) == 0 { + continue + } + + if tries == 0 { + break + } + tries-- + dlog.Debugf("Forwarding [%s] to [%s]", qName, server) + client := dns.Client{Net: pluginsState.serverProto, Timeout: pluginsState.timeout} respMsg, _, err = client.Exchange(msg, server) if err != nil { - return err + continue } + if respMsg.Truncated { + client.Net = "tcp" + respMsg, _, err = client.Exchange(msg, server) + if err != nil { + continue + } + } + if len(sequence) > 0 { + switch respMsg.Rcode { + case dns.RcodeNameError, dns.RcodeRefused, dns.RcodeNotAuth: + continue + } + } + if edns0 := respMsg.IsEdns0(); edns0 == nil || !edns0.Do() { + respMsg.AuthenticatedData = false + } + respMsg.Id = msg.Id + pluginsState.synthResponse = respMsg + pluginsState.action = PluginsActionSynth + pluginsState.returnCode = PluginsReturnCodeForward + return nil } - if edns0 := respMsg.IsEdns0(); edns0 == nil || !edns0.Do() { - respMsg.AuthenticatedData = false - } - respMsg.Id = msg.Id - pluginsState.synthResponse = respMsg - pluginsState.action = PluginsActionSynth - pluginsState.returnCode = PluginsReturnCodeForward - return nil + return err } diff --git a/go.mod b/go.mod index d8d8924a..60b7bf22 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/jedisct1/xsecretbox v0.0.0-20241212092125-3afc4917ac41 github.com/k-sone/critbitgo v1.4.0 github.com/kardianos/service v1.2.2 + github.com/lifenjoiner/dhcpdns v0.0.6 github.com/miekg/dns v1.1.62 github.com/opencoff/go-sieve v0.2.1 github.com/powerman/check v1.8.0 diff --git a/go.sum b/go.sum index a23c1016..d5b3486d 100644 --- a/go.sum +++ b/go.sum @@ -51,6 +51,8 @@ github.com/k-sone/critbitgo v1.4.0 h1:l71cTyBGeh6X5ATh6Fibgw3+rtNT80BA0uNNWgkPrb github.com/k-sone/critbitgo v1.4.0/go.mod h1:7E6pyoyADnFxlUBEKcnfS49b7SUAQGMK+OAp/UQvo0s= github.com/kardianos/service v1.2.2 h1:ZvePhAHfvo0A7Mftk/tEzqEZ7Q4lgnR8sGz4xu1YX60= github.com/kardianos/service v1.2.2/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= +github.com/lifenjoiner/dhcpdns v0.0.6 h1:rn4Y5RRR5sgQ6RjWenwhA7i/uHzHW9hbZpCobA4CAJs= +github.com/lifenjoiner/dhcpdns v0.0.6/go.mod h1:BixeaGeafYzDIuDCYIUbSOdi4m+TScpzI9cZGYgzgSk= github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU= diff --git a/vendor/modules.txt b/vendor/modules.txt index 607ec78c..4058f8c8 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -65,6 +65,9 @@ github.com/k-sone/critbitgo # github.com/kardianos/service v1.2.2 ## explicit; go 1.12 github.com/kardianos/service +# github.com/lifenjoiner/dhcpdns v0.0.6 +## explicit; go 1.20 +github.com/lifenjoiner/dhcpdns # github.com/miekg/dns v1.1.62 ## explicit; go 1.19 github.com/miekg/dns