mirror of
https://github.com/AdguardTeam/AdGuardHome.git
synced 2024-11-27 09:18:58 +03:00
e0b557eda2
Merge in DNS/adguard-home from 4166-udp-upstream to master
Closes #4166.
Squashed commit of the following:
commit b8b6d1c7ac1e11e83c0c68e46e7f66fdc6043839
Merge: e5f01273 ea6e033d
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date: Tue Mar 1 20:36:40 2022 +0300
Merge branch 'master' into 4166-udp-upstream
commit e5f0127384d84c4395da5b79a1fd4a47acbe122c
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date: Tue Mar 1 19:41:33 2022 +0300
client: upd upstream examples
commit bd974f22231f11f4c57e19d6d13bc45dbfdf2fdf
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date: Tue Mar 1 18:36:10 2022 +0300
all: upd proxy
commit badf1325090ecd1dc86e42e7406dfb6653e07bf1
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date: Fri Feb 4 14:36:50 2022 +0300
WIP
728 lines
19 KiB
Go
728 lines
19 KiB
Go
package dnsforward
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
|
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
|
"github.com/AdguardTeam/dnsproxy/proxy"
|
|
"github.com/AdguardTeam/dnsproxy/upstream"
|
|
"github.com/AdguardTeam/golibs/errors"
|
|
"github.com/AdguardTeam/golibs/log"
|
|
"github.com/AdguardTeam/golibs/netutil"
|
|
"github.com/AdguardTeam/golibs/stringutil"
|
|
"github.com/miekg/dns"
|
|
)
|
|
|
|
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 *BlockingMode `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"`
|
|
CacheOptimistic *bool `json:"cache_optimistic"`
|
|
ResolveClients *bool `json:"resolve_clients"`
|
|
UsePrivateRDNS *bool `json:"use_private_ptr_resolvers"`
|
|
LocalPTRUpstreams *[]string `json:"local_ptr_upstreams"`
|
|
}
|
|
|
|
func (s *Server) getDNSConfig() (c *dnsConfig) {
|
|
s.serverLock.RLock()
|
|
defer s.serverLock.RUnlock()
|
|
|
|
upstreams := stringutil.CloneSliceOrEmpty(s.conf.UpstreamDNS)
|
|
upstreamFile := s.conf.UpstreamDNSFileName
|
|
bootstraps := stringutil.CloneSliceOrEmpty(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
|
|
cacheOptimistic := s.conf.CacheOptimistic
|
|
resolveClients := s.conf.ResolveClients
|
|
usePrivateRDNS := s.conf.UsePrivateRDNS
|
|
localPTRUpstreams := stringutil.CloneSliceOrEmpty(s.conf.LocalPTRResolvers)
|
|
var upstreamMode string
|
|
if s.conf.FastestAddr {
|
|
upstreamMode = "fastest_addr"
|
|
} else if s.conf.AllServers {
|
|
upstreamMode = "parallel"
|
|
}
|
|
|
|
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,
|
|
CacheOptimistic: &cacheOptimistic,
|
|
UpstreamMode: &upstreamMode,
|
|
ResolveClients: &resolveClients,
|
|
UsePrivateRDNS: &usePrivateRDNS,
|
|
LocalPTRUpstreams: &localPTRUpstreams,
|
|
}
|
|
}
|
|
|
|
func (s *Server) handleGetConfig(w http.ResponseWriter, r *http.Request) {
|
|
defLocalPTRUps, err := s.filterOurDNSAddrs(s.sysResolvers.Get())
|
|
if err != nil {
|
|
log.Debug("getting dns configuration: %s", err)
|
|
}
|
|
|
|
resp := struct {
|
|
dnsConfig
|
|
// DefautLocalPTRUpstreams is used to pass the addresses from
|
|
// systemResolvers to the front-end. It's not a pointer to the slice
|
|
// since there is no need to omit it while decoding from JSON.
|
|
DefautLocalPTRUpstreams []string `json:"default_local_ptr_upstreams,omitempty"`
|
|
}{
|
|
dnsConfig: *s.getDNSConfig(),
|
|
DefautLocalPTRUpstreams: defLocalPTRUps,
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
if err = json.NewEncoder(w).Encode(resp); err != nil {
|
|
aghhttp.Error(r, w, http.StatusInternalServerError, "json.Encoder: %s", err)
|
|
|
|
return
|
|
}
|
|
}
|
|
|
|
func (req *dnsConfig) checkBlockingMode() bool {
|
|
if req.BlockingMode == nil {
|
|
return true
|
|
}
|
|
|
|
switch bm := *req.BlockingMode; bm {
|
|
case BlockingModeDefault,
|
|
BlockingModeREFUSED,
|
|
BlockingModeNXDOMAIN,
|
|
BlockingModeNullIP:
|
|
return true
|
|
case BlockingModeCustomIP:
|
|
return req.BlockingIPv4.To4() != nil && req.BlockingIPv6 != nil
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (req *dnsConfig) checkUpstreamsMode() bool {
|
|
valid := []string{"", "fastest_addr", "parallel"}
|
|
|
|
return req.UpstreamMode == nil || stringutil.InSlice(valid, *req.UpstreamMode)
|
|
}
|
|
|
|
func (req *dnsConfig) checkBootstrap() (err error) {
|
|
if req.Bootstraps == nil {
|
|
return nil
|
|
}
|
|
|
|
var b string
|
|
defer func() { err = errors.Annotate(err, "checking bootstrap %s: invalid address: %w", b) }()
|
|
|
|
for _, b = range *req.Bootstraps {
|
|
if b == "" {
|
|
return errors.Error("empty")
|
|
}
|
|
|
|
if _, err = upstream.NewResolver(b, nil); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validate returns an error if any field of req is invalid.
|
|
func (req *dnsConfig) validate(snd *aghnet.SubnetDetector) (err error) {
|
|
if req.Upstreams != nil {
|
|
err = ValidateUpstreams(*req.Upstreams)
|
|
if err != nil {
|
|
return fmt.Errorf("validating upstream servers: %w", err)
|
|
}
|
|
}
|
|
|
|
if req.LocalPTRUpstreams != nil {
|
|
err = ValidateUpstreamsPrivate(*req.LocalPTRUpstreams, snd)
|
|
if err != nil {
|
|
return fmt.Errorf("validating private upstream servers: %w", err)
|
|
}
|
|
}
|
|
|
|
err = req.checkBootstrap()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
switch {
|
|
case !req.checkBlockingMode():
|
|
return errors.Error("blocking_mode: incorrect value")
|
|
case !req.checkUpstreamsMode():
|
|
return errors.Error("upstream_mode: incorrect value")
|
|
case !req.checkCacheTTL():
|
|
return errors.Error("cache_ttl_min must be less or equal than cache_ttl_max")
|
|
default:
|
|
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{}
|
|
err := json.NewDecoder(r.Body).Decode(req)
|
|
if err != nil {
|
|
aghhttp.Error(r, w, http.StatusBadRequest, "decoding request: %s", err)
|
|
|
|
return
|
|
}
|
|
|
|
err = req.validate(s.subnetDetector)
|
|
if err != nil {
|
|
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
|
|
|
|
return
|
|
}
|
|
|
|
restart := s.setConfig(req)
|
|
s.conf.ConfigModified()
|
|
|
|
if restart {
|
|
err = s.Reconfigure(nil)
|
|
if err != nil {
|
|
aghhttp.Error(r, w, http.StatusInternalServerError, "%s", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Server) setConfigRestartable(dc *dnsConfig) (restart bool) {
|
|
if dc.Upstreams != nil {
|
|
s.conf.UpstreamDNS = *dc.Upstreams
|
|
restart = true
|
|
}
|
|
|
|
if dc.LocalPTRUpstreams != nil {
|
|
s.conf.LocalPTRResolvers = *dc.LocalPTRUpstreams
|
|
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 && s.conf.Ratelimit != *dc.RateLimit {
|
|
s.conf.Ratelimit = *dc.RateLimit
|
|
restart = true
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
if dc.CacheOptimistic != nil {
|
|
s.conf.CacheOptimistic = *dc.CacheOptimistic
|
|
restart = true
|
|
}
|
|
|
|
return restart
|
|
}
|
|
|
|
func (s *Server) setConfig(dc *dnsConfig) (restart bool) {
|
|
s.serverLock.Lock()
|
|
defer s.serverLock.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"
|
|
}
|
|
|
|
if dc.ResolveClients != nil {
|
|
s.conf.ResolveClients = *dc.ResolveClients
|
|
}
|
|
|
|
if dc.UsePrivateRDNS != nil {
|
|
s.conf.UsePrivateRDNS = *dc.UsePrivateRDNS
|
|
}
|
|
|
|
return s.setConfigRestartable(dc)
|
|
}
|
|
|
|
// upstreamJSON is a request body for handleTestUpstreamDNS endpoint.
|
|
type upstreamJSON struct {
|
|
Upstreams []string `json:"upstream_dns"`
|
|
BootstrapDNS []string `json:"bootstrap_dns"`
|
|
PrivateUpstreams []string `json:"private_upstream"`
|
|
}
|
|
|
|
// IsCommentOrEmpty returns true if s starts with a "#" character or is empty.
|
|
// This function is useful for filtering out non-upstream lines from upstream
|
|
// configs.
|
|
func IsCommentOrEmpty(s string) (ok bool) {
|
|
return len(s) == 0 || s[0] == '#'
|
|
}
|
|
|
|
// LocalNetChecker is used to check if the IP address belongs to a local
|
|
// network.
|
|
type LocalNetChecker interface {
|
|
// IsLocallyServedNetwork returns true if ip is contained in any of address
|
|
// registries defined by RFC 6303.
|
|
IsLocallyServedNetwork(ip net.IP) (ok bool)
|
|
}
|
|
|
|
// type check
|
|
var _ LocalNetChecker = (*aghnet.SubnetDetector)(nil)
|
|
|
|
// newUpstreamConfig validates upstreams and returns an appropriate upstream
|
|
// configuration or nil if it can't be built.
|
|
//
|
|
// TODO(e.burkov): Perhaps proxy.ParseUpstreamsConfig should validate upstreams
|
|
// slice already so that this function may be considered useless.
|
|
func newUpstreamConfig(upstreams []string) (conf *proxy.UpstreamConfig, err error) {
|
|
// No need to validate comments and empty lines.
|
|
upstreams = stringutil.FilterOut(upstreams, IsCommentOrEmpty)
|
|
if len(upstreams) == 0 {
|
|
// Consider this case valid since it means the default server should be
|
|
// used.
|
|
return nil, nil
|
|
}
|
|
|
|
conf, err = proxy.ParseUpstreamsConfig(
|
|
upstreams,
|
|
&upstream.Options{Bootstrap: []string{}, Timeout: DefaultTimeout},
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
} else if len(conf.Upstreams) == 0 {
|
|
return nil, errors.Error("no default upstreams specified")
|
|
}
|
|
|
|
for _, u := range upstreams {
|
|
_, err = validateUpstream(u)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return conf, nil
|
|
}
|
|
|
|
// ValidateUpstreams validates each upstream and returns an error if any
|
|
// upstream is invalid or if there are no default upstreams specified.
|
|
//
|
|
// TODO(e.burkov): Move into aghnet or even into dnsproxy.
|
|
func ValidateUpstreams(upstreams []string) (err error) {
|
|
_, err = newUpstreamConfig(upstreams)
|
|
|
|
return err
|
|
}
|
|
|
|
// stringKeysSorted returns the sorted slice of string keys of m.
|
|
//
|
|
// TODO(e.burkov): Use generics in Go 1.18. Move into golibs.
|
|
func stringKeysSorted(m map[string][]upstream.Upstream) (sorted []string) {
|
|
sorted = make([]string, 0, len(m))
|
|
for s := range m {
|
|
sorted = append(sorted, s)
|
|
}
|
|
|
|
sort.Strings(sorted)
|
|
|
|
return sorted
|
|
}
|
|
|
|
// ValidateUpstreamsPrivate validates each upstream and returns an error if any
|
|
// upstream is invalid or if there are no default upstreams specified. It also
|
|
// checks each domain of domain-specific upstreams for being ARPA pointing to
|
|
// a locally-served network. lnc must not be nil.
|
|
func ValidateUpstreamsPrivate(upstreams []string, lnc LocalNetChecker) (err error) {
|
|
conf, err := newUpstreamConfig(upstreams)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if conf == nil {
|
|
return nil
|
|
}
|
|
|
|
var errs []error
|
|
|
|
for _, domain := range stringKeysSorted(conf.DomainReservedUpstreams) {
|
|
var subnet *net.IPNet
|
|
subnet, err = netutil.SubnetFromReversedAddr(domain)
|
|
if err != nil {
|
|
errs = append(errs, err)
|
|
|
|
continue
|
|
}
|
|
|
|
if !lnc.IsLocallyServedNetwork(subnet.IP) {
|
|
errs = append(
|
|
errs,
|
|
fmt.Errorf("arpa domain %q should point to a locally-served network", domain),
|
|
)
|
|
}
|
|
}
|
|
|
|
if len(errs) > 0 {
|
|
return errors.List("checking domain-specific upstreams", errs...)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
var protocols = []string{"udp://", "tcp://", "tls://", "https://", "sdns://", "quic://"}
|
|
|
|
func validateUpstream(u string) (useDefault bool, err error) {
|
|
// Check if the user tries to specify upstream for domain.
|
|
var isDomainSpec bool
|
|
u, isDomainSpec, err = separateUpstream(u)
|
|
if err != nil {
|
|
return !isDomainSpec, err
|
|
}
|
|
|
|
// The special server address '#' means that default server must be used.
|
|
if useDefault = !isDomainSpec; u == "#" && isDomainSpec {
|
|
return useDefault, nil
|
|
}
|
|
|
|
// Check if the upstream has a valid protocol prefix.
|
|
//
|
|
// TODO(e.burkov): Validate the domain name.
|
|
for _, proto := range protocols {
|
|
if strings.HasPrefix(u, proto) {
|
|
return useDefault, nil
|
|
}
|
|
}
|
|
|
|
if strings.Contains(u, "://") {
|
|
return useDefault, errors.Error("wrong protocol")
|
|
}
|
|
|
|
// Check if upstream is either an IP or IP with port.
|
|
if net.ParseIP(u) != nil {
|
|
return useDefault, nil
|
|
} else if _, err = netutil.ParseIPPort(u); err != nil {
|
|
return useDefault, err
|
|
}
|
|
|
|
return useDefault, nil
|
|
}
|
|
|
|
// separateUpstream returns the upstream without the specified domains.
|
|
// isDomainSpec is true when the upstream is domains-specific.
|
|
func separateUpstream(upstreamStr string) (upstream string, isDomainSpec bool, err error) {
|
|
if !strings.HasPrefix(upstreamStr, "[/") {
|
|
return upstreamStr, false, nil
|
|
}
|
|
defer func() { err = errors.Annotate(err, "bad upstream for domain %q: %w", upstreamStr) }()
|
|
|
|
parts := strings.Split(upstreamStr[2:], "/]")
|
|
switch len(parts) {
|
|
case 2:
|
|
// Go on.
|
|
case 1:
|
|
return "", false, errors.Error("missing separator")
|
|
default:
|
|
return "", true, errors.Error("duplicated separator")
|
|
}
|
|
|
|
var domains string
|
|
domains, upstream = parts[0], parts[1]
|
|
for i, host := range strings.Split(domains, "/") {
|
|
if host == "" {
|
|
continue
|
|
}
|
|
|
|
err = netutil.ValidateDomainName(host)
|
|
if err != nil {
|
|
return "", true, fmt.Errorf("domain at index %d: %w", i, err)
|
|
}
|
|
}
|
|
|
|
return upstream, true, nil
|
|
}
|
|
|
|
// excFunc is a signature of function to check if upstream exchanges correctly.
|
|
type excFunc func(u upstream.Upstream) (err error)
|
|
|
|
// checkDNSUpstreamExc checks if the DNS upstream exchanges correctly.
|
|
func checkDNSUpstreamExc(u upstream.Upstream) (err error) {
|
|
req := &dns.Msg{
|
|
MsgHdr: dns.MsgHdr{
|
|
Id: dns.Id(),
|
|
RecursionDesired: true,
|
|
},
|
|
Question: []dns.Question{{
|
|
Name: "google-public-dns-a.google.com.",
|
|
Qtype: dns.TypeA,
|
|
Qclass: dns.ClassINET,
|
|
}},
|
|
}
|
|
|
|
var reply *dns.Msg
|
|
reply, err = u.Exchange(req)
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't communicate with upstream: %w", err)
|
|
}
|
|
|
|
if len(reply.Answer) != 1 {
|
|
return fmt.Errorf("wrong response")
|
|
} else if a, ok := reply.Answer[0].(*dns.A); !ok || !a.A.Equal(net.IP{8, 8, 8, 8}) {
|
|
return fmt.Errorf("wrong response")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// checkPrivateUpstreamExc checks if the upstream for resolving private
|
|
// addresses exchanges correctly.
|
|
func checkPrivateUpstreamExc(u upstream.Upstream) (err error) {
|
|
req := &dns.Msg{
|
|
MsgHdr: dns.MsgHdr{
|
|
Id: dns.Id(),
|
|
RecursionDesired: true,
|
|
},
|
|
Question: []dns.Question{{
|
|
Name: "1.0.0.127.in-addr.arpa.",
|
|
Qtype: dns.TypePTR,
|
|
Qclass: dns.ClassINET,
|
|
}},
|
|
}
|
|
|
|
if _, err = u.Exchange(req); err != nil {
|
|
return fmt.Errorf("couldn't communicate with upstream: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func checkDNS(input string, bootstrap []string, timeout time.Duration, ef excFunc) (err error) {
|
|
if IsCommentOrEmpty(input) {
|
|
return nil
|
|
}
|
|
|
|
// Separate upstream from domains list.
|
|
var useDefault bool
|
|
if useDefault, err = validateUpstream(input); err != nil {
|
|
return fmt.Errorf("wrong upstream format: %w", err)
|
|
}
|
|
|
|
// No need to check this DNS server.
|
|
if !useDefault {
|
|
return nil
|
|
}
|
|
|
|
if input, _, err = separateUpstream(input); err != nil {
|
|
return fmt.Errorf("wrong upstream format: %w", err)
|
|
}
|
|
|
|
if len(bootstrap) == 0 {
|
|
bootstrap = defaultBootstrap
|
|
}
|
|
|
|
log.Debug("checking if upstream %s works", input)
|
|
|
|
var u upstream.Upstream
|
|
u, err = upstream.AddressToUpstream(input, &upstream.Options{
|
|
Bootstrap: bootstrap,
|
|
Timeout: timeout,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to choose upstream for %q: %w", input, err)
|
|
}
|
|
|
|
if err = ef(u); err != nil {
|
|
return fmt.Errorf("upstream %q fails to exchange: %w", input, err)
|
|
}
|
|
|
|
log.Debug("upstream %s is ok", input)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) handleTestUpstreamDNS(w http.ResponseWriter, r *http.Request) {
|
|
req := &upstreamJSON{}
|
|
err := json.NewDecoder(r.Body).Decode(req)
|
|
if err != nil {
|
|
aghhttp.Error(r, w, http.StatusBadRequest, "Failed to read request body: %s", err)
|
|
|
|
return
|
|
}
|
|
|
|
result := map[string]string{}
|
|
bootstraps := req.BootstrapDNS
|
|
|
|
timeout := s.conf.UpstreamTimeout
|
|
for _, host := range req.Upstreams {
|
|
err = checkDNS(host, bootstraps, timeout, checkDNSUpstreamExc)
|
|
if err != nil {
|
|
log.Info("%v", err)
|
|
result[host] = err.Error()
|
|
|
|
continue
|
|
}
|
|
|
|
result[host] = "OK"
|
|
}
|
|
|
|
for _, host := range req.PrivateUpstreams {
|
|
err = checkDNS(host, bootstraps, timeout, checkPrivateUpstreamExc)
|
|
if err != nil {
|
|
log.Info("%v", err)
|
|
// TODO(e.burkov): If passed upstream have already written an error
|
|
// above, we rewriting the error for it. These cases should be
|
|
// handled properly instead.
|
|
result[host] = err.Error()
|
|
|
|
continue
|
|
}
|
|
|
|
result[host] = "OK"
|
|
}
|
|
|
|
jsonVal, err := json.Marshal(result)
|
|
if err != nil {
|
|
aghhttp.Error(
|
|
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 {
|
|
aghhttp.Error(r, w, http.StatusInternalServerError, "Couldn't write body: %s", err)
|
|
}
|
|
}
|
|
|
|
// 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 {
|
|
aghhttp.Error(r, w, http.StatusNotFound, "Not Found")
|
|
return
|
|
}
|
|
|
|
if !s.IsRunning() {
|
|
aghhttp.Error(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)
|
|
}
|