badguardhome/internal/dnsforward/http.go
Ainar Garipov c133b01ef7 Pull request: all: add idna handling, imp domain validation
Updates #2915.

Squashed commit of the following:

commit b907324426c87ee7334edbd61e43c44444ad27a9
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Apr 7 16:26:41 2021 +0300

    all: imp docs, upd

commit c022f75cac006e077095cad283fea0a91d3a0eea
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Apr 7 15:51:30 2021 +0300

    all: add idna handling, imp domain validation
2021-04-07 16:36:38 +03:00

555 lines
14 KiB
Go

package dnsforward
import (
"encoding/json"
"fmt"
"net"
"net/http"
"strconv"
"strings"
"github.com/AdguardTeam/AdGuardHome/internal/agherr"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/log"
"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) (err 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 the user tries to specify upstream for domain.
u, useDefault, err := separateUpstream(u)
if err != nil {
return useDefault, err
}
// The special server address '#' means "use the default servers"
if u == "#" && !useDefault {
return useDefault, nil
}
// Check if the upstream has a valid protocol prefix
for _, proto := range protocols {
if strings.HasPrefix(u, proto) {
return useDefault, nil
}
}
// Return error if the upstream contains '://' without any valid protocol
if strings.Contains(u, "://") {
return useDefault, fmt.Errorf("wrong protocol")
}
// Check if upstream is valid plain DNS
return useDefault, checkPlainDNS(u)
}
// separateUpstream returns the upstream without the specified domains.
// useDefault is true when a default upstream must be used.
func separateUpstream(upstreamStr string) (upstream string, useDefault bool, err error) {
defer agherr.Annotate("bad upstream for domain spec %q: %w", &err, upstreamStr)
if !strings.HasPrefix(upstreamStr, "[/") {
return upstreamStr, true, nil
}
parts := strings.Split(upstreamStr[2:], "/]")
if len(parts) != 2 {
return "", false, agherr.Error("duplicated separator")
}
domains := parts[0]
upstream = parts[1]
for i, host := range strings.Split(domains, "/") {
if host == "" {
continue
}
err = aghnet.ValidateDomainName(host)
if err != nil {
return "", false, fmt.Errorf("domain at index %d: %w", i, err)
}
}
return upstream, false, 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, useDefault, err := separateUpstream(input)
if err != nil {
return fmt.Errorf("wrong upstream format: %w", err)
}
// No need to check this DNS server
if !useDefault {
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)
}