package dnsforward import ( "encoding/json" "fmt" "net" "net/http" "strconv" "strings" "github.com/AdguardTeam/dnsproxy/proxy" "github.com/AdguardTeam/dnsproxy/upstream" "github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/utils" "github.com/miekg/dns" ) func httpError(r *http.Request, w http.ResponseWriter, code int, format string, args ...interface{}) { text := fmt.Sprintf(format, args...) log.Info("dns: %s %s: %s", r.Method, r.URL, text) http.Error(w, text, code) } type dnsConfig struct { Upstreams *[]string `json:"upstream_dns"` UpstreamsFile *string `json:"upstream_dns_file"` Bootstraps *[]string `json:"bootstrap_dns"` ProtectionEnabled *bool `json:"protection_enabled"` RateLimit *uint32 `json:"ratelimit"` BlockingMode *string `json:"blocking_mode"` BlockingIPv4 net.IP `json:"blocking_ipv4"` BlockingIPv6 net.IP `json:"blocking_ipv6"` EDNSCSEnabled *bool `json:"edns_cs_enabled"` DNSSECEnabled *bool `json:"dnssec_enabled"` DisableIPv6 *bool `json:"disable_ipv6"` UpstreamMode *string `json:"upstream_mode"` CacheSize *uint32 `json:"cache_size"` CacheMinTTL *uint32 `json:"cache_ttl_min"` CacheMaxTTL *uint32 `json:"cache_ttl_max"` } func (s *Server) getDNSConfig() dnsConfig { s.RLock() upstreams := stringArrayDup(s.conf.UpstreamDNS) upstreamFile := s.conf.UpstreamDNSFileName bootstraps := stringArrayDup(s.conf.BootstrapDNS) protectionEnabled := s.conf.ProtectionEnabled blockingMode := s.conf.BlockingMode BlockingIPv4 := s.conf.BlockingIPv4 BlockingIPv6 := s.conf.BlockingIPv6 Ratelimit := s.conf.Ratelimit EnableEDNSClientSubnet := s.conf.EnableEDNSClientSubnet EnableDNSSEC := s.conf.EnableDNSSEC AAAADisabled := s.conf.AAAADisabled CacheSize := s.conf.CacheSize CacheMinTTL := s.conf.CacheMinTTL CacheMaxTTL := s.conf.CacheMaxTTL var upstreamMode string if s.conf.FastestAddr { upstreamMode = "fastest_addr" } else if s.conf.AllServers { upstreamMode = "parallel" } s.RUnlock() return dnsConfig{ Upstreams: &upstreams, UpstreamsFile: &upstreamFile, Bootstraps: &bootstraps, ProtectionEnabled: &protectionEnabled, BlockingMode: &blockingMode, BlockingIPv4: BlockingIPv4, BlockingIPv6: BlockingIPv6, RateLimit: &Ratelimit, EDNSCSEnabled: &EnableEDNSClientSubnet, DNSSECEnabled: &EnableDNSSEC, DisableIPv6: &AAAADisabled, CacheSize: &CacheSize, CacheMinTTL: &CacheMinTTL, CacheMaxTTL: &CacheMaxTTL, UpstreamMode: &upstreamMode, } } func (s *Server) handleGetConfig(w http.ResponseWriter, r *http.Request) { resp := s.getDNSConfig() w.Header().Set("Content-Type", "application/json") enc := json.NewEncoder(w) if err := enc.Encode(resp); err != nil { httpError(r, w, http.StatusInternalServerError, "json.Encoder: %s", err) return } } func (req *dnsConfig) checkBlockingMode() bool { if req.BlockingMode == nil { return true } bm := *req.BlockingMode if bm == "custom_ip" { if req.BlockingIPv4.To4() == nil { return false } return req.BlockingIPv6 != nil } for _, valid := range []string{ "default", "refused", "nxdomain", "null_ip", } { if bm == valid { return true } } return false } func (req *dnsConfig) checkUpstreamsMode() bool { if req.UpstreamMode == nil { return true } for _, valid := range []string{ "", "fastest_addr", "parallel", } { if *req.UpstreamMode == valid { return true } } return false } func (req *dnsConfig) checkBootstrap() (string, error) { if req.Bootstraps == nil { return "", nil } for _, boot := range *req.Bootstraps { if boot == "" { return boot, fmt.Errorf("invalid bootstrap server address: empty") } if _, err := upstream.NewResolver(boot, upstream.Options{Timeout: 0}); err != nil { return boot, fmt.Errorf("invalid bootstrap server address: %w", err) } } return "", nil } func (req *dnsConfig) checkCacheTTL() bool { if req.CacheMinTTL == nil && req.CacheMaxTTL == nil { return true } var min, max uint32 if req.CacheMinTTL != nil { min = *req.CacheMinTTL } if req.CacheMaxTTL != nil { max = *req.CacheMaxTTL } return min <= max } func (s *Server) handleSetConfig(w http.ResponseWriter, r *http.Request) { req := dnsConfig{} dec := json.NewDecoder(r.Body) if err := dec.Decode(&req); err != nil { httpError(r, w, http.StatusBadRequest, "json Encode: %s", err) return } if req.Upstreams != nil { if err := ValidateUpstreams(*req.Upstreams); err != nil { httpError(r, w, http.StatusBadRequest, "wrong upstreams specification: %s", err) return } } if errBoot, err := req.checkBootstrap(); err != nil { httpError(r, w, http.StatusBadRequest, "%s can not be used as bootstrap dns cause: %s", errBoot, err) return } if !req.checkBlockingMode() { httpError(r, w, http.StatusBadRequest, "blocking_mode: incorrect value") return } if !req.checkUpstreamsMode() { httpError(r, w, http.StatusBadRequest, "upstream_mode: incorrect value") return } if !req.checkCacheTTL() { httpError(r, w, http.StatusBadRequest, "cache_ttl_min must be less or equal than cache_ttl_max") return } restart := s.setConfig(req) s.conf.ConfigModified() if restart { if err := s.Reconfigure(nil); err != nil { httpError(r, w, http.StatusInternalServerError, "%s", err) return } } } func (s *Server) setConfigRestartable(dc dnsConfig) (restart bool) { if dc.Upstreams != nil { s.conf.UpstreamDNS = *dc.Upstreams restart = true } if dc.UpstreamsFile != nil { s.conf.UpstreamDNSFileName = *dc.UpstreamsFile restart = true } if dc.Bootstraps != nil { s.conf.BootstrapDNS = *dc.Bootstraps restart = true } if dc.RateLimit != nil { restart = restart || s.conf.Ratelimit != *dc.RateLimit s.conf.Ratelimit = *dc.RateLimit } if dc.EDNSCSEnabled != nil { s.conf.EnableEDNSClientSubnet = *dc.EDNSCSEnabled restart = true } if dc.CacheSize != nil { s.conf.CacheSize = *dc.CacheSize restart = true } if dc.CacheMinTTL != nil { s.conf.CacheMinTTL = *dc.CacheMinTTL restart = true } if dc.CacheMaxTTL != nil { s.conf.CacheMaxTTL = *dc.CacheMaxTTL restart = true } return restart } func (s *Server) setConfig(dc dnsConfig) (restart bool) { s.Lock() defer s.Unlock() if dc.ProtectionEnabled != nil { s.conf.ProtectionEnabled = *dc.ProtectionEnabled } if dc.BlockingMode != nil { s.conf.BlockingMode = *dc.BlockingMode if *dc.BlockingMode == "custom_ip" { s.conf.BlockingIPv4 = dc.BlockingIPv4.To4() s.conf.BlockingIPv6 = dc.BlockingIPv6.To16() } } if dc.DNSSECEnabled != nil { s.conf.EnableDNSSEC = *dc.DNSSECEnabled } if dc.DisableIPv6 != nil { s.conf.AAAADisabled = *dc.DisableIPv6 } if dc.UpstreamMode != nil { s.conf.AllServers = *dc.UpstreamMode == "parallel" s.conf.FastestAddr = *dc.UpstreamMode == "fastest_addr" } return s.setConfigRestartable(dc) } type upstreamJSON struct { Upstreams []string `json:"upstream_dns"` // Upstreams BootstrapDNS []string `json:"bootstrap_dns"` // Bootstrap DNS } // ValidateUpstreams validates each upstream and returns an error if any upstream is invalid or if there are no default upstreams specified func ValidateUpstreams(upstreams []string) error { // No need to validate comments upstreams = filterOutComments(upstreams) // Consider this case valid because defaultDNS will be used if len(upstreams) == 0 { return nil } _, err := proxy.ParseUpstreamsConfig( upstreams, upstream.Options{ Bootstrap: []string{}, Timeout: DefaultTimeout, }, ) if err != nil { return err } var defaultUpstreamFound bool for _, u := range upstreams { var ok bool ok, err = validateUpstream(u) if err != nil { return err } if !defaultUpstreamFound { defaultUpstreamFound = ok } } if !defaultUpstreamFound { return fmt.Errorf("no default upstreams specified") } return nil } var protocols = []string{"tls://", "https://", "tcp://", "sdns://", "quic://"} func validateUpstream(u string) (bool, error) { // Check if user tries to specify upstream for domain u, defaultUpstream, err := separateUpstream(u) if err != nil { return defaultUpstream, err } // The special server address '#' means "use the default servers" if u == "#" && !defaultUpstream { return defaultUpstream, nil } // Check if the upstream has a valid protocol prefix for _, proto := range protocols { if strings.HasPrefix(u, proto) { return defaultUpstream, nil } } // Return error if the upstream contains '://' without any valid protocol if strings.Contains(u, "://") { return defaultUpstream, fmt.Errorf("wrong protocol") } // Check if upstream is valid plain DNS return defaultUpstream, checkPlainDNS(u) } // separateUpstream returns upstream without specified domains and a bool flag that indicates if no domains were specified // error will be returned if upstream per domain specification is invalid func separateUpstream(upstream string) (string, bool, error) { defaultUpstream := true if strings.HasPrefix(upstream, "[/") { defaultUpstream = false // split domains and upstream string domainsAndUpstream := strings.Split(strings.TrimPrefix(upstream, "[/"), "/]") if len(domainsAndUpstream) != 2 { return "", defaultUpstream, fmt.Errorf("wrong dns upstream per domain specification: %s", upstream) } // split domains list and validate each one for _, host := range strings.Split(domainsAndUpstream[0], "/") { if host != "" { if err := utils.IsValidHostname(host); err != nil { return "", defaultUpstream, err } } } upstream = domainsAndUpstream[1] } return upstream, defaultUpstream, nil } // checkPlainDNS checks if host is plain DNS func checkPlainDNS(upstream string) error { // Check if host is ip without port if net.ParseIP(upstream) != nil { return nil } // Check if host is ip with port ip, port, err := net.SplitHostPort(upstream) if err != nil { return err } if net.ParseIP(ip) == nil { return fmt.Errorf("%s is not a valid IP", ip) } _, err = strconv.ParseInt(port, 0, 64) if err != nil { return fmt.Errorf("%s is not a valid port: %w", port, err) } return nil } func (s *Server) handleTestUpstreamDNS(w http.ResponseWriter, r *http.Request) { req := upstreamJSON{} err := json.NewDecoder(r.Body).Decode(&req) if err != nil { httpError(r, w, http.StatusBadRequest, "Failed to read request body: %s", err) return } result := map[string]string{} for _, host := range req.Upstreams { err = checkDNS(host, req.BootstrapDNS) if err != nil { log.Info("%v", err) result[host] = err.Error() } else { result[host] = "OK" } } jsonVal, err := json.Marshal(result) if err != nil { httpError(r, w, http.StatusInternalServerError, "Unable to marshal status json: %s", err) return } w.Header().Set("Content-Type", "application/json") _, err = w.Write(jsonVal) if err != nil { httpError(r, w, http.StatusInternalServerError, "Couldn't write body: %s", err) return } } func checkDNS(input string, bootstrap []string) error { if !isUpstream(input) { return nil } // separate upstream from domains list input, defaultUpstream, err := separateUpstream(input) if err != nil { return fmt.Errorf("wrong upstream format: %w", err) } // No need to check this DNS server if !defaultUpstream { return nil } if _, err = validateUpstream(input); err != nil { return fmt.Errorf("wrong upstream format: %w", err) } if len(bootstrap) == 0 { bootstrap = defaultBootstrap } log.Debug("checking if dns %s works...", input) u, err := upstream.AddressToUpstream(input, upstream.Options{Bootstrap: bootstrap, Timeout: DefaultTimeout}) if err != nil { return fmt.Errorf("failed to choose upstream for %s: %w", input, err) } req := dns.Msg{} req.Id = dns.Id() req.RecursionDesired = true req.Question = []dns.Question{ {Name: "google-public-dns-a.google.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}, } reply, err := u.Exchange(&req) if err != nil { return fmt.Errorf("couldn't communicate with dns server %s: %w", input, err) } if len(reply.Answer) != 1 { return fmt.Errorf("dns server %s returned wrong answer", input) } if t, ok := reply.Answer[0].(*dns.A); ok { if !net.IPv4(8, 8, 8, 8).Equal(t.A) { return fmt.Errorf("dns server %s returned wrong answer: %v", input, t.A) } } log.Debug("dns %s works OK", input) return nil } // Control flow: // web // -> dnsforward.handleDOH -> dnsforward.ServeHTTP // -> proxy.ServeHTTP -> proxy.handleDNSRequest // -> dnsforward.handleDNSRequest func (s *Server) handleDOH(w http.ResponseWriter, r *http.Request) { if !s.conf.TLSAllowUnencryptedDOH && r.TLS == nil { httpError(r, w, http.StatusNotFound, "Not Found") return } if !s.IsRunning() { httpError(r, w, http.StatusInternalServerError, "dns server is not running") return } s.ServeHTTP(w, r) } func (s *Server) registerHandlers() { s.conf.HTTPRegister(http.MethodGet, "/control/dns_info", s.handleGetConfig) s.conf.HTTPRegister(http.MethodPost, "/control/dns_config", s.handleSetConfig) s.conf.HTTPRegister(http.MethodPost, "/control/test_upstream_dns", s.handleTestUpstreamDNS) s.conf.HTTPRegister(http.MethodGet, "/control/access/list", s.handleAccessList) s.conf.HTTPRegister(http.MethodPost, "/control/access/set", s.handleAccessSet) // Register both versions, with and without the trailing slash, to // prevent a 301 Moved Permanently redirect when clients request the // path without the trailing slash. Those redirects break some clients. // // See go doc net/http.ServeMux. // // See also https://github.com/AdguardTeam/AdGuardHome/issues/2628. s.conf.HTTPRegister("", "/dns-query", s.handleDOH) s.conf.HTTPRegister("", "/dns-query/", s.handleDOH) }