3045da1742
Merge in DNS/adguard-home from 2271-handle-nolint to master Closes #2271. Squashed commit of the following: commit fde5c8795ac79e1f7d02ba8c8e369b5a724a000e Merge: fc2acd898642dcd647
Author: Eugene Burkov <e.burkov@adguard.com> Date: Fri Nov 20 17:12:28 2020 +0300 Merge branch 'master' into 2271-handle-nolint commit fc2acd89871de08c39e80ace9e5bb8a7acb7afba Author: Eugene Burkov <e.burkov@adguard.com> Date: Tue Nov 17 11:55:29 2020 +0300 dnsforward: fix test output strings commit c4ebae6ea9c293bad239519c44ca5a6c576bb921 Author: Eugene Burkov <e.burkov@adguard.com> Date: Mon Nov 16 22:43:20 2020 +0300 dnsfilter: make package pass tests commit f2d98c6acabd8977f3b1b361987eaa31eb6eb9ad Author: Eugene Burkov <e.burkov@adguard.com> Date: Mon Nov 16 20:05:00 2020 +0300 querylog: make decoding pass tests commit ab5850d24c50d53b8393f2de448cc340241351d7 Merge: 6ed2066bf8a9c6e8a0
Author: Eugene Burkov <e.burkov@adguard.com> Date: Mon Nov 16 19:48:31 2020 +0300 Merge branch 'master' into 2271-handle-nolint commit 6ed2066bf567e13dd14cfa16fc7b109b59fa39ef Author: Eugene Burkov <e.burkov@adguard.com> Date: Mon Nov 16 18:13:45 2020 +0300 home: fix tests naming commit af691081fb02b7500a746b16492f01f7f9befe9a Author: Eugene Burkov <e.burkov@adguard.com> Date: Mon Nov 16 12:15:49 2020 +0300 home: impove code quality commit 2914cd3cd23ef2a1964116baab9187d89b377f86 Author: Eugene Burkov <e.burkov@adguard.com> Date: Wed Nov 11 15:46:39 2020 +0300 * querylog: remove useless check commit 9996840650e784ccc76d1f29964560435ba27dc7 Author: Eugene Burkov <e.burkov@adguard.com> Date: Wed Nov 11 13:18:34 2020 +0300 * all: fix noticed defects commit 2b15293e59337f70302fbc0db81ebb26bee0bed2 Author: Eugene Burkov <e.burkov@adguard.com> Date: Tue Nov 10 20:15:53 2020 +0300 * stats: remove last nolint directive commit b2e1ddf7b58196a2fdbf879f084edb41ca1aa1eb Author: Eugene Burkov <e.burkov@adguard.com> Date: Tue Nov 10 18:35:41 2020 +0300 * all: remove another nolint directive commit c6fc5cfcc9c95ab9e570a95ab41c3e5c0125e62e Author: Eugene Burkov <e.burkov@adguard.com> Date: Tue Nov 10 18:11:28 2020 +0300 * querylog: remove nolint directive commit 226ddbf2c92f737f085b44a4ddf6daec7b602153 Author: Eugene Burkov <e.burkov@adguard.com> Date: Tue Nov 10 16:35:26 2020 +0300 * home: remove nolint directive commit 2ea3086ad41e9003282add7e996ae722d72d878b Author: Eugene Burkov <e.burkov@adguard.com> Date: Tue Nov 10 16:13:57 2020 +0300 * home: reduce cyclomatic complexity of run function commit f479b480c48e0bb832ddef8f57586f56b8a55bab Author: Eugene Burkov <e.burkov@adguard.com> Date: Tue Nov 10 15:35:46 2020 +0300 * home: use crypto/rand instead of math/rand commit a28d4a53e3b930136b036606fc7e78404f1d208b Author: Eugene Burkov <e.burkov@adguard.com> Date: Tue Nov 10 14:11:07 2020 +0300 * dnsforward: remove gocyclo nolint directive commit 64a0a324cc2b20614ceec3ccc6505e960fe526e9 Author: Eugene Burkov <e.burkov@adguard.com> Date: Tue Nov 10 11:45:49 2020 +0300 all *: remove some nolint directives Updates #2271.
541 lines
13 KiB
Go
541 lines
13 KiB
Go
package dnsforward
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"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 *string `json:"blocking_ipv4"`
|
|
BlockingIPv6 *string `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 == nil || req.BlockingIPv6 == nil {
|
|
return false
|
|
}
|
|
|
|
ip4 := net.ParseIP(*req.BlockingIPv4)
|
|
if ip4 == nil || ip4.To4() == nil {
|
|
return false
|
|
}
|
|
|
|
ip6 := net.ParseIP(*req.BlockingIPv6)
|
|
return ip6 != 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, 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
|
|
}
|
|
|
|
if s.setConfig(req) {
|
|
if err := s.Reconfigure(nil); err != nil {
|
|
httpError(r, w, http.StatusInternalServerError, "%s", err)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Server) setConfig(dc dnsConfig) (restart bool) {
|
|
s.Lock()
|
|
|
|
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.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
|
|
s.conf.BlockingIPAddrv4 = net.ParseIP(*dc.BlockingIPv4)
|
|
s.conf.BlockingIPv6 = *dc.BlockingIPv6
|
|
s.conf.BlockingIPAddrv6 = net.ParseIP(*dc.BlockingIPv6)
|
|
}
|
|
}
|
|
|
|
if dc.RateLimit != nil {
|
|
if s.conf.Ratelimit != *dc.RateLimit {
|
|
restart = true
|
|
}
|
|
s.conf.Ratelimit = *dc.RateLimit
|
|
}
|
|
|
|
if dc.EDNSCSEnabled != nil {
|
|
s.conf.EnableEDNSClientSubnet = *dc.EDNSCSEnabled
|
|
restart = true
|
|
}
|
|
|
|
if dc.DNSSECEnabled != nil {
|
|
s.conf.EnableDNSSEC = *dc.DNSSECEnabled
|
|
}
|
|
|
|
if dc.DisableIPv6 != nil {
|
|
s.conf.AAAADisabled = *dc.DisableIPv6
|
|
}
|
|
|
|
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.UpstreamMode != nil {
|
|
switch *dc.UpstreamMode {
|
|
case "parallel":
|
|
s.conf.AllServers = true
|
|
s.conf.FastestAddr = false
|
|
case "fastest_addr":
|
|
s.conf.AllServers = false
|
|
s.conf.FastestAddr = true
|
|
default:
|
|
s.conf.AllServers = false
|
|
s.conf.FastestAddr = false
|
|
}
|
|
}
|
|
s.Unlock()
|
|
s.conf.ConfigModified()
|
|
return restart
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
var defaultUpstreamFound bool
|
|
for _, u := range upstreams {
|
|
d, err := validateUpstream(u)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Check this flag until default upstream will not be found
|
|
if !defaultUpstreamFound {
|
|
defaultUpstreamFound = d
|
|
}
|
|
}
|
|
|
|
// Return error if there are no default upstreams
|
|
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("GET", "/control/dns_info", s.handleGetConfig)
|
|
s.conf.HTTPRegister("POST", "/control/dns_config", s.handleSetConfig)
|
|
s.conf.HTTPRegister("POST", "/control/test_upstream_dns", s.handleTestUpstreamDNS)
|
|
|
|
s.conf.HTTPRegister("GET", "/control/access/list", s.handleAccessList)
|
|
s.conf.HTTPRegister("POST", "/control/access/set", s.handleAccessSet)
|
|
|
|
s.conf.HTTPRegister("", "/dns-query", s.handleDOH)
|
|
}
|