mirror of
https://github.com/AdguardTeam/AdGuardHome.git
synced 2024-12-15 19:31:45 +03:00
756f97ede1
Close #2083
Squashed commit of the following:
commit 113ad3c4ae2ca184b3945dcaa357b57303ee5fd6
Merge: 4ca1f005 bf23aa4d
Author: ArtemBaskal <a.baskal@adguard.com>
Date: Tue Sep 22 14:41:30 2020 +0300
Merge branch 'master' into feature/2083
commit 4ca1f0056708eb23bb751587a0ec284508f35edf
Author: ArtemBaskal <a.baskal@adguard.com>
Date: Tue Sep 22 13:14:31 2020 +0300
Simplify filterOutComments, use assert in tests
commit bba03568aa979300e0534a2bd2f03086e25b3f87
Author: ArtemBaskal <a.baskal@adguard.com>
Date: Tue Sep 22 12:05:00 2020 +0300
Add ValidateUpstreams test cases
commit 181de508cf266e3a47058f2b7e1b4b4accbab827
Author: ArtemBaskal <a.baskal@adguard.com>
Date: Tue Sep 22 11:47:03 2020 +0300
Refactor testUpstream
commit 19c189cf7b64f4d252428dec5a608595c8e4cbc7
Author: ArtemBaskal <a.baskal@adguard.com>
Date: Tue Sep 22 10:38:37 2020 +0300
Move functions to utils
commit 003937e90e0ff02e696d45c21045a27a49cd0202
Author: ArtemBaskal <a.baskal@adguard.com>
Date: Mon Sep 21 19:00:49 2020 +0300
Review changes
commit b26bf64d8cef0266f33bce51c5bad324c74bb6da
Author: ArtemBaskal <a.baskal@adguard.com>
Date: Mon Sep 21 17:58:01 2020 +0300
+ upstream: Filter out the upstream comments
commit 920975d2ba14fade07282cdb5c72a699c8083463
Author: ArtemBaskal <a.baskal@adguard.com>
Date: Mon Sep 21 17:51:00 2020 +0300
Trim upstreams, extract comment token
commit a9958eb305ed9af10de68ef3bffe63f216805efe
Author: ArtemBaskal <a.baskal@adguard.com>
Date: Mon Sep 21 10:34:11 2020 +0300
Fix markup styles
commit 6efa41d944c7b09454a4011d2c9ea52b5ce91bbf
Author: ArtemBaskal <a.baskal@adguard.com>
Date: Fri Sep 18 17:59:57 2020 +0300
Fix upstream form positioning
commit 6eb12158d0bca49d4b41eb65a3ebed44eafbe486
Author: ArtemBaskal <a.baskal@adguard.com>
Date: Fri Sep 18 17:16:49 2020 +0300
Update example_upstream_comment locale
commit aa9317b0243f5d30f0fcb9cbfcdf502547a8e954
Author: ArtemBaskal <a.baskal@adguard.com>
Date: Fri Sep 18 13:28:30 2020 +0300
Highlight comments in custom rules form, extract highlight logic
commit dc55245d3db9edbde60fda0a0e50e1e045e71403
Author: ArtemBaskal <a.baskal@adguard.com>
Date: Thu Sep 17 22:48:29 2020 +0300
+ client: Allow entering comments to the Upstreams box
487 lines
12 KiB
Go
487 lines
12 KiB
Go
package dnsforward
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/AdguardTeam/dnsproxy/upstream"
|
|
"github.com/AdguardTeam/golibs/jsonutil"
|
|
"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 dnsConfigJSON 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) handleGetConfig(w http.ResponseWriter, r *http.Request) {
|
|
resp := dnsConfigJSON{}
|
|
s.RLock()
|
|
resp.Upstreams = stringArrayDup(s.conf.UpstreamDNS)
|
|
resp.UpstreamsFile = s.conf.UpstreamDNSFileName
|
|
resp.Bootstraps = stringArrayDup(s.conf.BootstrapDNS)
|
|
|
|
resp.ProtectionEnabled = s.conf.ProtectionEnabled
|
|
resp.BlockingMode = s.conf.BlockingMode
|
|
resp.BlockingIPv4 = s.conf.BlockingIPv4
|
|
resp.BlockingIPv6 = s.conf.BlockingIPv6
|
|
resp.RateLimit = s.conf.Ratelimit
|
|
resp.EDNSCSEnabled = s.conf.EnableEDNSClientSubnet
|
|
resp.DNSSECEnabled = s.conf.EnableDNSSEC
|
|
resp.DisableIPv6 = s.conf.AAAADisabled
|
|
resp.CacheSize = s.conf.CacheSize
|
|
resp.CacheMinTTL = s.conf.CacheMinTTL
|
|
resp.CacheMaxTTL = s.conf.CacheMaxTTL
|
|
if s.conf.FastestAddr {
|
|
resp.UpstreamMode = "fastest_addr"
|
|
} else if s.conf.AllServers {
|
|
resp.UpstreamMode = "parallel"
|
|
}
|
|
s.RUnlock()
|
|
|
|
js, err := json.Marshal(resp)
|
|
if err != nil {
|
|
httpError(r, w, http.StatusInternalServerError, "json.Marshal: %s", err)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write(js)
|
|
}
|
|
|
|
func checkBlockingMode(req dnsConfigJSON) bool {
|
|
bm := req.BlockingMode
|
|
if !(bm == "default" || bm == "refused" || bm == "nxdomain" || bm == "null_ip" || bm == "custom_ip") {
|
|
return false
|
|
}
|
|
|
|
if bm == "custom_ip" {
|
|
ip := net.ParseIP(req.BlockingIPv4)
|
|
if ip == nil || ip.To4() == nil {
|
|
return false
|
|
}
|
|
|
|
ip = net.ParseIP(req.BlockingIPv6)
|
|
if ip == nil {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// Validate bootstrap server address
|
|
func checkBootstrap(addr string) error {
|
|
if addr == "" { // additional check is required because NewResolver() allows empty address
|
|
return fmt.Errorf("invalid bootstrap server address: empty")
|
|
}
|
|
_, err := upstream.NewResolver(addr, 0)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid bootstrap server address: %s", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// nolint(gocyclo) - we need to check each JSON field separately
|
|
func (s *Server) handleSetConfig(w http.ResponseWriter, r *http.Request) {
|
|
req := dnsConfigJSON{}
|
|
js, err := jsonutil.DecodeObject(&req, r.Body)
|
|
if err != nil {
|
|
httpError(r, w, http.StatusBadRequest, "json.Decode: %s", err)
|
|
return
|
|
}
|
|
|
|
if js.Exists("upstream_dns") {
|
|
err = ValidateUpstreams(req.Upstreams)
|
|
if err != nil {
|
|
httpError(r, w, http.StatusBadRequest, "wrong upstreams specification: %s", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
if js.Exists("bootstrap_dns") {
|
|
for _, boot := range req.Bootstraps {
|
|
if err := checkBootstrap(boot); err != nil {
|
|
httpError(r, w, http.StatusBadRequest, "%s can not be used as bootstrap dns cause: %s", boot, err)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
if js.Exists("blocking_mode") && !checkBlockingMode(req) {
|
|
httpError(r, w, http.StatusBadRequest, "blocking_mode: incorrect value")
|
|
return
|
|
}
|
|
|
|
if js.Exists("upstream_mode") &&
|
|
!(req.UpstreamMode == "" || req.UpstreamMode == "fastest_addr" || req.UpstreamMode == "parallel") {
|
|
httpError(r, w, http.StatusBadRequest, "upstream_mode: incorrect value")
|
|
return
|
|
}
|
|
|
|
if req.CacheMinTTL > req.CacheMaxTTL {
|
|
httpError(r, w, http.StatusBadRequest, "cache_ttl_min must be less or equal than cache_ttl_max")
|
|
return
|
|
}
|
|
|
|
restart := false
|
|
s.Lock()
|
|
|
|
if js.Exists("upstream_dns") {
|
|
s.conf.UpstreamDNS = req.Upstreams
|
|
restart = true
|
|
}
|
|
|
|
if js.Exists("upstream_dns_file") {
|
|
s.conf.UpstreamDNSFileName = req.UpstreamsFile
|
|
restart = true
|
|
}
|
|
|
|
if js.Exists("bootstrap_dns") {
|
|
s.conf.BootstrapDNS = req.Bootstraps
|
|
restart = true
|
|
}
|
|
|
|
if js.Exists("protection_enabled") {
|
|
s.conf.ProtectionEnabled = req.ProtectionEnabled
|
|
}
|
|
|
|
if js.Exists("blocking_mode") {
|
|
s.conf.BlockingMode = req.BlockingMode
|
|
if req.BlockingMode == "custom_ip" {
|
|
if js.Exists("blocking_ipv4") {
|
|
s.conf.BlockingIPv4 = req.BlockingIPv4
|
|
s.conf.BlockingIPAddrv4 = net.ParseIP(req.BlockingIPv4)
|
|
}
|
|
if js.Exists("blocking_ipv6") {
|
|
s.conf.BlockingIPv6 = req.BlockingIPv6
|
|
s.conf.BlockingIPAddrv6 = net.ParseIP(req.BlockingIPv6)
|
|
}
|
|
}
|
|
}
|
|
|
|
if js.Exists("ratelimit") {
|
|
if s.conf.Ratelimit != req.RateLimit {
|
|
restart = true
|
|
}
|
|
s.conf.Ratelimit = req.RateLimit
|
|
}
|
|
|
|
if js.Exists("edns_cs_enabled") {
|
|
s.conf.EnableEDNSClientSubnet = req.EDNSCSEnabled
|
|
restart = true
|
|
}
|
|
|
|
if js.Exists("dnssec_enabled") {
|
|
s.conf.EnableDNSSEC = req.DNSSECEnabled
|
|
}
|
|
|
|
if js.Exists("disable_ipv6") {
|
|
s.conf.AAAADisabled = req.DisableIPv6
|
|
}
|
|
|
|
if js.Exists("cache_size") {
|
|
s.conf.CacheSize = req.CacheSize
|
|
restart = true
|
|
}
|
|
|
|
if js.Exists("cache_ttl_min") {
|
|
s.conf.CacheMinTTL = req.CacheMinTTL
|
|
restart = true
|
|
}
|
|
|
|
if js.Exists("cache_ttl_max") {
|
|
s.conf.CacheMaxTTL = req.CacheMaxTTL
|
|
restart = true
|
|
}
|
|
|
|
if js.Exists("upstream_mode") {
|
|
s.conf.FastestAddr = false
|
|
s.conf.AllServers = false
|
|
switch req.UpstreamMode {
|
|
case "":
|
|
//
|
|
|
|
case "parallel":
|
|
s.conf.AllServers = true
|
|
|
|
case "fastest_addr":
|
|
s.conf.FastestAddr = true
|
|
}
|
|
}
|
|
|
|
s.Unlock()
|
|
s.conf.ConfigModified()
|
|
|
|
if restart {
|
|
err = s.Reconfigure(nil)
|
|
if err != nil {
|
|
httpError(r, w, http.StatusInternalServerError, "%s", err)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
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: %s", 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
|
|
}
|
|
|
|
if len(req.Upstreams) == 0 {
|
|
httpError(r, w, http.StatusBadRequest, "No servers specified")
|
|
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: %s", err)
|
|
}
|
|
|
|
// No need to check this DNS server
|
|
if !defaultUpstream {
|
|
return nil
|
|
}
|
|
|
|
if _, err := validateUpstream(input); err != nil {
|
|
return fmt.Errorf("wrong upstream format: %s", 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: %s", 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: %s", 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)
|
|
}
|