AdGuardHome/internal/dnsforward/dnsforward.go
Eugene Burkov 80ed8be145 Pull request: 2704 local addresses vol.3
Merge in DNS/adguard-home from 2704-local-addresses-vol.3 to master

Updates #2704.
Updates #2829.
Updates #2928.

Squashed commit of the following:

commit 8c42355c0093a3ac6951f79a5211e7891800f93a
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Wed Apr 7 18:07:41 2021 +0300

    dnsforward: rm errors pkg

commit 7594a21a620239951039454dd5686a872e6f41a8
Merge: 830b0834 908452f8
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Wed Apr 7 18:00:03 2021 +0300

    Merge branch 'master' into 2704-local-addresses-vol.3

commit 830b0834090510096061fed20b600195ab3773b8
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Wed Apr 7 17:47:51 2021 +0300

    dnsforward: reduce local upstream timeout

commit 493e81d9e8bacdc690f88af29a38d211b9733c7e
Author: Ildar Kamalov <ik@adguard.com>
Date:   Tue Apr 6 19:11:00 2021 +0300

    client: private_upstream test

commit a0194ac28f15114578359b8c2460cd9af621e912
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Tue Apr 6 18:36:23 2021 +0300

    all: expand api, fix conflicts

commit 0f4e06836fed958391aa597c8b02453564980ca3
Merge: 89cf93ad 8746005d
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Tue Apr 6 18:35:04 2021 +0300

    Merge branch 'master' into 2704-local-addresses-vol.3

commit 89cf93ad4f26c2bf4f1b18ecaa4d3a1e169f9b06
Author: Ildar Kamalov <ik@adguard.com>
Date:   Tue Apr 6 18:02:40 2021 +0300

    client: add local ptr upstreams to upstream test

commit e6dd869dddd4888474d625cbb005bad6390e4760
Author: Ildar Kamalov <ik@adguard.com>
Date:   Tue Apr 6 15:24:22 2021 +0300

    client: add private DNS form

commit b858057b9a957a416117f22b8bd0025f90e8c758
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Tue Apr 6 13:05:28 2021 +0300

    aghstrings: mk cloning correct

commit 8009ba60a6a7d6ceb7b6483a29f4e68d533af243
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Tue Apr 6 12:37:46 2021 +0300

    aghstrings: fix lil bug

commit 0dd19f2e7cc7c0de21517c37abd8336a907e1c0d
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Mon Apr 5 20:45:01 2021 +0300

    all: log changes

commit eb5558d96fffa6e7bca7e14d3740d26e47382e23
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Mon Apr 5 20:18:53 2021 +0300

    dnsforward: keep the style

commit d6d5fcbde40a633129c0e04887b81cf0b1ce6875
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Mon Apr 5 20:02:52 2021 +0300

    dnsforward: disable redundant filtering for local ptr

commit 4f864c32027d10db9bcb4a264d2338df8c20afac
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Mon Apr 5 17:53:17 2021 +0300

    dnsforward: imp tests

commit 7848e6f2341868f8ba0bb839956a0b7444cf02ca
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Mon Apr 5 14:52:12 2021 +0300

    all: imp code

commit 19ac30653800eebf8aaee499f65560ae2d458a5a
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Sun Apr 4 16:28:05 2021 +0300

    all: mv more logic to aghstrings

commit fac892ec5f0d2e30d6d64def0609267bbae4a202
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Fri Apr 2 20:23:23 2021 +0300

    dnsforward: use filepath

commit 05a3aeef1181b914788d14c7519287d467ab301f
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Fri Apr 2 20:17:54 2021 +0300

    aghstrings: introduce the pkg

commit f24e1b63d6e1bf266a4ed063f46f86d7abf65663
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Fri Apr 2 20:01:23 2021 +0300

    all: imp code

commit 0217a0ebb341f99a90c9b68013bebf6ff73d08ae
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Fri Apr 2 18:04:13 2021 +0300

    openapi: log changes

... and 3 more commits
2021-04-07 20:16:06 +03:00

551 lines
14 KiB
Go

// Package dnsforward contains a DNS forwarding server.
package dnsforward
import (
"errors"
"fmt"
"net"
"net/http"
"os"
"runtime"
"strings"
"sync"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/agherr"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/aghstrings"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
"github.com/AdguardTeam/AdGuardHome/internal/dnsfilter"
"github.com/AdguardTeam/AdGuardHome/internal/querylog"
"github.com/AdguardTeam/AdGuardHome/internal/stats"
"github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/golibs/log"
"github.com/miekg/dns"
)
// DefaultTimeout is the default upstream timeout
const DefaultTimeout = 10 * time.Second
const (
safeBrowsingBlockHost = "standard-block.dns.adguard.com"
parentalBlockHost = "family-block.dns.adguard.com"
)
var defaultDNS = []string{
"https://dns10.quad9.net/dns-query",
}
var defaultBootstrap = []string{"9.9.9.10", "149.112.112.10", "2620:fe::10", "2620:fe::fe:10"}
// Often requested by all kinds of DNS probes
var defaultBlockedHosts = []string{"version.bind", "id.server", "hostname.bind"}
var webRegistered bool
// Server is the main way to start a DNS server.
//
// Example:
// s := dnsforward.Server{}
// err := s.Start(nil) // will start a DNS server listening on default port 53, in a goroutine
// err := s.Reconfigure(ServerConfig{UDPListenAddr: &net.UDPAddr{Port: 53535}}) // will reconfigure running DNS server to listen on UDP port 53535
// err := s.Stop() // will stop listening on port 53535 and cancel all goroutines
// err := s.Start(nil) // will start listening again, on port 53535, in a goroutine
//
// The zero Server is empty and ready for use.
type Server struct {
dnsProxy *proxy.Proxy // DNS proxy instance
dnsFilter *dnsfilter.DNSFilter // DNS filter instance
dhcpServer dhcpd.ServerInterface // DHCP server instance (optional)
queryLog querylog.QueryLog // Query log instance
stats stats.Stats
access *accessCtx
// autohostSuffix is the suffix used to detect internal hosts. It must
// be a valid top-level domain plus dots on each side.
autohostSuffix string
ipset ipsetCtx
subnetDetector *aghnet.SubnetDetector
localResolvers aghnet.Exchanger
tableHostToIP map[string]net.IP // "hostname -> IP" table for internal addresses (DHCP)
tableHostToIPLock sync.Mutex
tablePTR map[string]string // "IP -> hostname" table for reverse lookup
tablePTRLock sync.Mutex
// DNS proxy instance for internal usage
// We don't Start() it and so no listen port is required.
internalProxy *proxy.Proxy
isRunning bool
sync.RWMutex
conf ServerConfig
}
// defaultAutohostSuffix is the default suffix used to detect internal hosts
// when no suffix is provided. See the documentation for Server.autohostSuffix.
const defaultAutohostSuffix = ".lan."
// DNSCreateParams are parameters to create a new server.
type DNSCreateParams struct {
DNSFilter *dnsfilter.DNSFilter
Stats stats.Stats
QueryLog querylog.QueryLog
DHCPServer dhcpd.ServerInterface
SubnetDetector *aghnet.SubnetDetector
AutohostTLD string
}
// tldToSuffix converts a top-level domain into an autohost suffix.
func tldToSuffix(tld string) (suffix string) {
l := len(tld) + 2
b := make([]byte, l)
b[0] = '.'
copy(b[1:], tld)
b[l-1] = '.'
return string(b)
}
// NewServer creates a new instance of the dnsforward.Server
// Note: this function must be called only once
func NewServer(p DNSCreateParams) (s *Server, err error) {
var autohostSuffix string
if p.AutohostTLD == "" {
autohostSuffix = defaultAutohostSuffix
} else {
err = aghnet.ValidateDomainNameLabel(p.AutohostTLD)
if err != nil {
return nil, fmt.Errorf("autohost tld: %w", err)
}
autohostSuffix = tldToSuffix(p.AutohostTLD)
}
s = &Server{
dnsFilter: p.DNSFilter,
stats: p.Stats,
queryLog: p.QueryLog,
subnetDetector: p.SubnetDetector,
autohostSuffix: autohostSuffix,
}
if p.DHCPServer != nil {
s.dhcpServer = p.DHCPServer
s.dhcpServer.SetOnLeaseChanged(s.onDHCPLeaseChanged)
s.onDHCPLeaseChanged(dhcpd.LeaseChangedAdded)
}
if runtime.GOARCH == "mips" || runtime.GOARCH == "mipsle" {
// Use plain DNS on MIPS, encryption is too slow
defaultDNS = defaultBootstrap
}
return s, nil
}
// NewCustomServer creates a new instance of *Server with custom internal proxy.
func NewCustomServer(internalProxy *proxy.Proxy) *Server {
s := &Server{}
if internalProxy != nil {
s.internalProxy = internalProxy
}
return s
}
// Close - close object
func (s *Server) Close() {
s.Lock()
s.dnsFilter = nil
s.stats = nil
s.queryLog = nil
s.dnsProxy = nil
err := s.ipset.Close()
if err != nil {
log.Error("closing ipset: %s", err)
}
s.Unlock()
}
// WriteDiskConfig - write configuration
func (s *Server) WriteDiskConfig(c *FilteringConfig) {
s.RLock()
sc := s.conf.FilteringConfig
*c = sc
c.RatelimitWhitelist = aghstrings.CloneSlice(sc.RatelimitWhitelist)
c.BootstrapDNS = aghstrings.CloneSlice(sc.BootstrapDNS)
c.AllowedClients = aghstrings.CloneSlice(sc.AllowedClients)
c.DisallowedClients = aghstrings.CloneSlice(sc.DisallowedClients)
c.BlockedHosts = aghstrings.CloneSlice(sc.BlockedHosts)
c.UpstreamDNS = aghstrings.CloneSlice(sc.UpstreamDNS)
s.RUnlock()
}
// RDNSSettings returns the copy of actual RDNS configuration.
func (s *Server) RDNSSettings() (localPTRResolvers []string, resolveClients bool) {
s.RLock()
defer s.RUnlock()
return aghstrings.CloneSlice(s.conf.LocalPTRResolvers), s.conf.ResolveClients
}
// Resolve - get IP addresses by host name from an upstream server.
// No request/response filtering is performed.
// Query log and Stats are not updated.
// This method may be called before Start().
func (s *Server) Resolve(host string) ([]net.IPAddr, error) {
s.RLock()
defer s.RUnlock()
return s.internalProxy.LookupIPAddr(host)
}
// RDNSExchanger is a resolver for clients' addresses.
type RDNSExchanger interface {
// Exchange tries to resolve the ip in a suitable way, e.g. either as
// local or as external.
Exchange(ip net.IP) (host string, err error)
}
const (
// rDNSEmptyAnswerErr is returned by Exchange method when the answer
// section of respond is empty.
rDNSEmptyAnswerErr agherr.Error = "the answer section is empty"
// rDNSNotPTRErr is returned by Exchange method when the response is not
// of PTR type.
rDNSNotPTRErr agherr.Error = "the response is not a ptr"
)
// Exchange implements the RDNSExchanger interface for *Server.
func (s *Server) Exchange(ip net.IP) (host string, err error) {
s.RLock()
defer s.RUnlock()
if !s.conf.ResolveClients {
return "", nil
}
arpa := dns.Fqdn(aghnet.ReverseAddr(ip))
req := &dns.Msg{
MsgHdr: dns.MsgHdr{
Id: dns.Id(),
RecursionDesired: true,
},
Compress: true,
Question: []dns.Question{{
Name: arpa,
Qtype: dns.TypePTR,
Qclass: dns.ClassINET,
}},
}
var resp *dns.Msg
if s.subnetDetector.IsLocallyServedNetwork(ip) {
resp, err = s.localResolvers.Exchange(req)
} else {
ctx := &proxy.DNSContext{
Proto: "udp",
Req: req,
StartTime: time.Now(),
}
err = s.internalProxy.Resolve(ctx)
resp = ctx.Res
}
if err != nil {
return "", err
}
if len(resp.Answer) == 0 {
return "", fmt.Errorf("lookup for %q: %w", arpa, rDNSEmptyAnswerErr)
}
ptr, ok := resp.Answer[0].(*dns.PTR)
if !ok {
return "", fmt.Errorf("type checking: %w", rDNSNotPTRErr)
}
return strings.TrimSuffix(ptr.Ptr, "."), nil
}
// Start starts the DNS server.
func (s *Server) Start() error {
s.Lock()
defer s.Unlock()
return s.startLocked()
}
// startLocked starts the DNS server without locking. For internal use only.
func (s *Server) startLocked() error {
err := s.dnsProxy.Start()
if err == nil {
s.isRunning = true
}
return err
}
// defaultLocalTimeout is the default timeout for resolving addresses from
// locally-served networks. It is assumed that local resolvers should work much
// faster than ordinary upstreams.
const defaultLocalTimeout = 1 * time.Second
// collectDNSIPAddrs returns the slice of IP addresses without port number which
// we are listening on. For internal use only.
func (s *Server) collectDNSIPAddrs() (addrs []string, err error) {
addrs = make([]string, len(s.conf.TCPListenAddrs)+len(s.conf.UDPListenAddrs))
var i int
var ip net.IP
for _, addr := range s.conf.TCPListenAddrs {
if addr == nil {
continue
}
if ip = addr.IP; ip.IsUnspecified() {
return aghnet.CollectAllIfacesAddrs()
}
addrs[i] = ip.String()
i++
}
for _, addr := range s.conf.UDPListenAddrs {
if addr == nil {
continue
}
if ip = addr.IP; ip.IsUnspecified() {
return aghnet.CollectAllIfacesAddrs()
}
addrs[i] = ip.String()
i++
}
return addrs[:i], nil
}
// stringSetSubtract subtracts b from a interpreted as sets.
func stringSetSubtract(a, b []string) (c []string) {
// unit is an object to be used as value in set.
type unit = struct{}
cSet := make(map[string]unit)
for _, k := range a {
cSet[k] = unit{}
}
for _, k := range b {
delete(cSet, k)
}
c = make([]string, len(cSet))
i := 0
for k := range cSet {
c[i] = k
i++
}
return c
}
// setupResolvers initializes the resolvers for local addresses. For internal
// use only.
func (s *Server) setupResolvers(localAddrs []string) (err error) {
bootstraps := s.conf.BootstrapDNS
if len(localAddrs) == 0 {
var sysRes aghnet.SystemResolvers
// TODO(e.burkov): Enable the refresher after the actual
// implementation passes the public testing.
sysRes, err = aghnet.NewSystemResolvers(0, nil)
if err != nil {
return err
}
localAddrs = sysRes.Get()
bootstraps = nil
}
log.Debug("upstreams to resolve PTR for local addresses: %v", localAddrs)
var ourAddrs []string
ourAddrs, err = s.collectDNSIPAddrs()
if err != nil {
return err
}
// TODO(e.burkov): The approach of subtracting sets of strings
// is not really applicable here since in case of listening on
// all network interfaces we should check the whole interface's
// network to cut off all the loopback addresses as well.
localAddrs = stringSetSubtract(localAddrs, ourAddrs)
if s.localResolvers, err = aghnet.NewMultiAddrExchanger(
localAddrs,
bootstraps,
defaultLocalTimeout,
); err != nil {
return err
}
return nil
}
// Prepare the object
func (s *Server) Prepare(config *ServerConfig) error {
// Initialize the server configuration
// --
if config != nil {
s.conf = *config
if s.conf.BlockingMode == "custom_ip" {
if s.conf.BlockingIPv4 == nil || s.conf.BlockingIPv6 == nil {
return fmt.Errorf("dns: invalid custom blocking IP address specified")
}
}
}
// Set default values in the case if nothing is configured
// --
s.initDefaultSettings()
// Initialize IPSET configuration
// --
err := s.ipset.init(s.conf.IPSETList)
if err != nil {
if !errors.Is(err, os.ErrInvalid) && !errors.Is(err, os.ErrPermission) {
return fmt.Errorf("cannot initialize ipset: %w", err)
}
// ipset cannot currently be initialized if the server was
// installed from Snap or when the user or the binary doesn't
// have the required permissions, or when the kernel doesn't
// support netfilter.
//
// Log and go on.
//
// TODO(a.garipov): The Snap problem can probably be solved if
// we add the netlink-connector interface plug.
log.Error("cannot initialize ipset: %s", err)
}
// Prepare DNS servers settings
// --
err = s.prepareUpstreamSettings()
if err != nil {
return err
}
// Create DNS proxy configuration
// --
var proxyConfig proxy.Config
proxyConfig, err = s.createProxyConfig()
if err != nil {
return err
}
// Prepare a DNS proxy instance that we use for internal DNS queries
// --
s.prepareIntlProxy()
// Initialize DNS access module
// --
s.access = &accessCtx{}
err = s.access.Init(s.conf.AllowedClients, s.conf.DisallowedClients, s.conf.BlockedHosts)
if err != nil {
return err
}
// Register web handlers if necessary
// --
if !webRegistered && s.conf.HTTPRegister != nil {
webRegistered = true
s.registerHandlers()
}
// Create the main DNS proxy instance
// --
s.dnsProxy = &proxy.Proxy{Config: proxyConfig}
err = s.setupResolvers(s.conf.LocalPTRResolvers)
if err != nil {
return fmt.Errorf("setting up resolvers: %w", err)
}
return nil
}
// Stop stops the DNS server.
func (s *Server) Stop() error {
s.Lock()
defer s.Unlock()
return s.stopLocked()
}
// stopLocked stops the DNS server without locking. For internal use only.
func (s *Server) stopLocked() error {
if s.dnsProxy != nil {
err := s.dnsProxy.Stop()
if err != nil {
return fmt.Errorf("could not stop the DNS server properly: %w", err)
}
}
s.isRunning = false
return nil
}
// IsRunning returns true if the DNS server is running
func (s *Server) IsRunning() bool {
s.RLock()
defer s.RUnlock()
return s.isRunning
}
// Reconfigure applies the new configuration to the DNS server
func (s *Server) Reconfigure(config *ServerConfig) error {
s.Lock()
defer s.Unlock()
log.Print("Start reconfiguring the server")
err := s.stopLocked()
if err != nil {
return fmt.Errorf("could not reconfigure the server: %w", err)
}
// It seems that net.Listener.Close() doesn't close file descriptors right away.
// We wait for some time and hope that this fd will be closed.
time.Sleep(100 * time.Millisecond)
err = s.Prepare(config)
if err != nil {
return fmt.Errorf("could not reconfigure the server: %w", err)
}
err = s.startLocked()
if err != nil {
return fmt.Errorf("could not reconfigure the server: %w", err)
}
return nil
}
// ServeHTTP is a HTTP handler method we use to provide DNS-over-HTTPS
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.RLock()
p := s.dnsProxy
s.RUnlock()
if p != nil { // an attempt to protect against race in case we're here after Close() was called
p.ServeHTTP(w, r)
}
}
// IsBlockedIP - return TRUE if this client should be blocked
func (s *Server) IsBlockedIP(ip net.IP) (bool, string) {
if ip == nil {
return false, ""
}
return s.access.IsBlockedIP(ip)
}