From 09b6eba7d9de4ac9522acad8585d9befe7e2cea1 Mon Sep 17 00:00:00 2001
From: Ainar Garipov <a.garipov@adguard.com>
Date: Mon, 7 Dec 2020 17:58:33 +0300
Subject: [PATCH] Pull request: all: add dnscrypt support

Merge in DNS/adguard-home from 1361-dnscrypt to master

Closes #1361.

Squashed commit of the following:

commit 31b780c16cc6b68336b95275f62381cee2e822a2
Merge: c2ce98aaf 9b963fc77
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Mon Dec 7 17:48:41 2020 +0300

    Merge branch 'master' into 1361-dnscrypt

commit c2ce98aaf24bd5ed5b5cd7da86aae093866ab34e
Merge: 3bf3d7b96 63e513e33
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Fri Dec 4 19:32:40 2020 +0300

    Merge branch 'master' into 1361-dnscrypt

commit 3bf3d7b96530c86b54545462390562ebedc616b2
Merge: 5de451996 4134220c5
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Dec 3 17:31:59 2020 +0300

    Merge branch 'master' into 1361-dnscrypt

commit 5de451996d48ab3792ce78291068f72785303494
Merge: 60d7976f7 ab8defdb0
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Dec 2 19:07:56 2020 +0300

    Merge branch 'master' into 1361-dnscrypt

commit 60d7976f7c7ad0316751b92477a31f882c1e3134
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Mon Nov 30 19:11:14 2020 +0300

    all: add dnscrypt support
---
 CHANGELOG.md                  |  2 +
 go.mod                        |  1 +
 go.sum                        | 11 -----
 internal/dnsforward/config.go | 18 +++++++
 internal/home/config.go       | 10 ++++
 internal/home/dns.go          | 90 ++++++++++++++++++++++++++++++-----
 6 files changed, 109 insertions(+), 23 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2e21e8d8..60167c75 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,11 +15,13 @@ and this project adheres to
 
 ### Added
 
+- DNSCrypt protocol support [#1361].
 - A 5 second wait period until a DHCP server's network interface gets an IP
   address ([#2304]).
 - `$dnstype` modifier for filters ([#2337]).
 - HTTP API request body size limit ([#2305]).
 
+[#1361]: https://github.com/AdguardTeam/AdGuardHome/issues/1361
 [#2304]: https://github.com/AdguardTeam/AdGuardHome/issues/2304
 [#2305]: https://github.com/AdguardTeam/AdGuardHome/issues/2305
 [#2337]: https://github.com/AdguardTeam/AdGuardHome/issues/2337
diff --git a/go.mod b/go.mod
index 03dd3446..8b232972 100644
--- a/go.mod
+++ b/go.mod
@@ -7,6 +7,7 @@ require (
 	github.com/AdguardTeam/golibs v0.4.4
 	github.com/AdguardTeam/urlfilter v0.13.0
 	github.com/NYTimes/gziphandler v1.1.1
+	github.com/ameshkov/dnscrypt/v2 v2.0.0
 	github.com/beefsack/go-rate v0.0.0-20200827232406-6cde80facd47 // indirect
 	github.com/fsnotify/fsnotify v1.4.9
 	github.com/go-ping/ping v0.0.0-20201115131931-3300c582a663
diff --git a/go.sum b/go.sum
index 7a7c1876..af30b9bd 100644
--- a/go.sum
+++ b/go.sum
@@ -28,8 +28,6 @@ github.com/AdguardTeam/golibs v0.4.3/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKU
 github.com/AdguardTeam/golibs v0.4.4 h1:cM9UySQiYFW79zo5XRwnaIWVzfW4eNXmZktMrWbthpw=
 github.com/AdguardTeam/golibs v0.4.4/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
 github.com/AdguardTeam/gomitmproxy v0.2.0/go.mod h1:Qdv0Mktnzer5zpdpi5rAwixNJzW2FN91LjKJCkVbYGU=
-github.com/AdguardTeam/urlfilter v0.12.3 h1:FMjQG0eTgrr8xA3z2zaLVcCgGdpzoECPGWwgPjtwPNs=
-github.com/AdguardTeam/urlfilter v0.12.3/go.mod h1:1fcCQx5TGJANrQN6sHNNM9KPBl7qx7BJml45ko6vru0=
 github.com/AdguardTeam/urlfilter v0.13.0 h1:MfO46K81JVTkhgP6gRu/buKl5wAOSfusjiDwjT1JN1c=
 github.com/AdguardTeam/urlfilter v0.13.0/go.mod h1:klx4JbOfc4EaNb5lWLqOwfg+pVcyRukmoJRvO55lL5U=
 github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
@@ -109,8 +107,6 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9
 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
 github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
 github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
-github.com/go-ping/ping v0.0.0-20201022122018-3977ed72668a h1:O9xspHB2yrvKfMQ1m6OQhqe37i5yvg0dXAYMuAjugmM=
-github.com/go-ping/ping v0.0.0-20201022122018-3977ed72668a/go.mod h1:35JbSyV/BYqHwwRA6Zr1uVDm1637YlNOU61wI797NPI=
 github.com/go-ping/ping v0.0.0-20201115131931-3300c582a663 h1:jI2GiiRh+pPbey52EVmbU6kuLiXqwy4CXZ4gwUBj8Y0=
 github.com/go-ping/ping v0.0.0-20201115131931-3300c582a663/go.mod h1:35JbSyV/BYqHwwRA6Zr1uVDm1637YlNOU61wI797NPI=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
@@ -236,8 +232,6 @@ github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCV
 github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
-github.com/kardianos/service v1.1.0 h1:QV2SiEeWK42P0aEmGcsAgjApw/lRxkwopvT+Gu6t1/0=
-github.com/kardianos/service v1.1.0/go.mod h1:RrJI2xn5vve/r32U5suTbeaSGoMU6GbNPoj36CVYcHc=
 github.com/kardianos/service v1.2.0 h1:bGuZ/epo3vrt8IPC7mnKQolqFeYJb7Cs8Rk4PSOBB/g=
 github.com/kardianos/service v1.2.0/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM=
 github.com/karrick/godirwalk v1.10.12 h1:BqUm+LuJcXjGv1d2mj3gBiQyrQ57a0rYoAmhvJQ7RDU=
@@ -259,8 +253,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/lucas-clemente/quic-go v0.18.1 h1:DMR7guC0NtVS8zNZR3IO7NARZvZygkSC56GGtC6cyys=
 github.com/lucas-clemente/quic-go v0.18.1/go.mod h1:yXttHsSNxQi8AWijC/vLP+OJczXqzHSOcJrM5ITUlCg=
-github.com/lucas-clemente/quic-go v0.19.0 h1:IG5lB7DfHl6eZ7WTBVL8bnbDg0JGwDv906l6JffQbyg=
-github.com/lucas-clemente/quic-go v0.19.0/go.mod h1:ZUygOqIoai0ASXXLJ92LTnKdbqh9MHCLTX6Nr1jUrK0=
 github.com/lucas-clemente/quic-go v0.19.1 h1:J9TkQJGJVOR3UmGhd4zdVYwKSA0EoXbLRf15uQJ6gT4=
 github.com/lucas-clemente/quic-go v0.19.1/go.mod h1:ZUygOqIoai0ASXXLJ92LTnKdbqh9MHCLTX6Nr1jUrK0=
 github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
@@ -467,8 +459,6 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnk
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E=
 golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9 h1:umElSU9WZirRdgu2yFHY0ayQkEnKiOC1TtM3fWXFnoU=
-golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9 h1:phUcVbl53swtrUN8kQEXFhUxPlIlWyBfKmidCu7P95o=
 golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -554,7 +544,6 @@ golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5h
 golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
diff --git a/internal/dnsforward/config.go b/internal/dnsforward/config.go
index 73f5cb6d..881174d1 100644
--- a/internal/dnsforward/config.go
+++ b/internal/dnsforward/config.go
@@ -15,6 +15,7 @@ import (
 	"github.com/AdguardTeam/dnsproxy/proxy"
 	"github.com/AdguardTeam/dnsproxy/upstream"
 	"github.com/AdguardTeam/golibs/log"
+	"github.com/ameshkov/dnscrypt/v2"
 )
 
 // FilteringConfig represents the DNS filtering configuration of AdGuard Home
@@ -114,6 +115,15 @@ type TLSConfig struct {
 	dnsNames []string
 }
 
+// DNSCryptConfig is the DNSCrypt server configuration struct.
+type DNSCryptConfig struct {
+	UDPListenAddr *net.UDPAddr
+	TCPListenAddr *net.TCPAddr
+	ProviderName  string
+	ResolverCert  *dnscrypt.Cert
+	Enabled       bool
+}
+
 // ServerConfig represents server configuration.
 // The zero ServerConfig is empty and ready for use.
 type ServerConfig struct {
@@ -124,6 +134,7 @@ type ServerConfig struct {
 
 	FilteringConfig
 	TLSConfig
+	DNSCryptConfig
 	TLSAllowUnencryptedDOH bool
 
 	TLSv12Roots *x509.CertPool // list of root CAs for TLSv1.2
@@ -189,6 +200,13 @@ func (s *Server) createProxyConfig() (proxy.Config, error) {
 		return proxyConfig, err
 	}
 
+	if s.conf.DNSCryptConfig.Enabled {
+		proxyConfig.DNSCryptUDPListenAddr = []*net.UDPAddr{s.conf.DNSCryptConfig.UDPListenAddr}
+		proxyConfig.DNSCryptTCPListenAddr = []*net.TCPAddr{s.conf.DNSCryptConfig.TCPListenAddr}
+		proxyConfig.DNSCryptProviderName = s.conf.DNSCryptConfig.ProviderName
+		proxyConfig.DNSCryptResolverCert = s.conf.DNSCryptConfig.ResolverCert
+	}
+
 	// Validate proxy config
 	if proxyConfig.UpstreamConfig == nil || len(proxyConfig.UpstreamConfig.Upstreams) == 0 {
 		return proxyConfig, errors.New("no default upstream servers configured")
diff --git a/internal/home/config.go b/internal/home/config.go
index 2f1e7da7..ed81c56e 100644
--- a/internal/home/config.go
+++ b/internal/home/config.go
@@ -99,6 +99,16 @@ type tlsConfigSettings struct {
 	PortDNSOverTLS  int    `yaml:"port_dns_over_tls" json:"port_dns_over_tls,omitempty"`   // DNS-over-TLS port. If 0, DOT will be disabled
 	PortDNSOverQUIC uint16 `yaml:"port_dns_over_quic" json:"port_dns_over_quic,omitempty"` // DNS-over-QUIC port. If 0, DoQ will be disabled
 
+	// PortDNSCrypt is the port for DNSCrypt requests.  If it's zero,
+	// DNSCrypt is disabled.
+	PortDNSCrypt int `yaml:"port_dnscrypt" json:"port_dnscrypt"`
+	// DNSCryptConfigFile is the path to the DNSCrypt config file.  Must be
+	// set if PortDNSCrypt is not zero.
+	//
+	// See https://github.com/AdguardTeam/dnsproxy and
+	// https://github.com/ameshkov/dnscrypt.
+	DNSCryptConfigFile string `yaml:"dnscrypt_config_file" json:"dnscrypt_config_file"`
+
 	// Allow DOH queries via unencrypted HTTP (e.g. for reverse proxying)
 	AllowUnencryptedDOH bool `yaml:"allow_unencrypted_doh" json:"allow_unencrypted_doh"`
 
diff --git a/internal/home/dns.go b/internal/home/dns.go
index 9c420e94..1090d9be 100644
--- a/internal/home/dns.go
+++ b/internal/home/dns.go
@@ -3,8 +3,10 @@ package home
 import (
 	"fmt"
 	"net"
+	"os"
 	"path/filepath"
 
+	"github.com/AdguardTeam/AdGuardHome/internal/agherr"
 	"github.com/AdguardTeam/AdGuardHome/internal/dnsfilter"
 	"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
 	"github.com/AdguardTeam/AdGuardHome/internal/querylog"
@@ -12,6 +14,8 @@ import (
 	"github.com/AdguardTeam/AdGuardHome/internal/util"
 	"github.com/AdguardTeam/dnsproxy/proxy"
 	"github.com/AdguardTeam/golibs/log"
+	"github.com/ameshkov/dnscrypt/v2"
+	yaml "gopkg.in/yaml.v2"
 )
 
 // Called by other modules when configuration is changed
@@ -70,7 +74,12 @@ func initDNSServer() error {
 	}
 	Context.dnsServer = dnsforward.NewServer(p)
 	Context.clients.dnsServer = Context.dnsServer
-	dnsConfig := generateServerConfig()
+	dnsConfig, err := generateServerConfig()
+	if err != nil {
+		closeDNSServer()
+		return fmt.Errorf("generateServerConfig: %w", err)
+	}
+
 	err = Context.dnsServer.Prepare(&dnsConfig)
 	if err != nil {
 		closeDNSServer()
@@ -104,10 +113,11 @@ func onDNSRequest(d *proxy.DNSContext) {
 	}
 }
 
-func generateServerConfig() dnsforward.ServerConfig {
-	newconfig := dnsforward.ServerConfig{
-		UDPListenAddr:   &net.UDPAddr{IP: net.ParseIP(config.DNS.BindHost), Port: config.DNS.Port},
-		TCPListenAddr:   &net.TCPAddr{IP: net.ParseIP(config.DNS.BindHost), Port: config.DNS.Port},
+func generateServerConfig() (newconfig dnsforward.ServerConfig, err error) {
+	bindHost := net.ParseIP(config.DNS.BindHost)
+	newconfig = dnsforward.ServerConfig{
+		UDPListenAddr:   &net.UDPAddr{IP: bindHost, Port: config.DNS.Port},
+		TCPListenAddr:   &net.TCPAddr{IP: bindHost, Port: config.DNS.Port},
 		FilteringConfig: config.DNS.FilteringConfig,
 		ConfigModified:  onConfigModified,
 		HTTPRegister:    httpRegister,
@@ -121,25 +131,76 @@ func generateServerConfig() dnsforward.ServerConfig {
 
 		if tlsConf.PortDNSOverTLS != 0 {
 			newconfig.TLSListenAddr = &net.TCPAddr{
-				IP:   net.ParseIP(config.DNS.BindHost),
+				IP:   bindHost,
 				Port: tlsConf.PortDNSOverTLS,
 			}
 		}
 
 		if tlsConf.PortDNSOverQUIC != 0 {
 			newconfig.QUICListenAddr = &net.UDPAddr{
-				IP:   net.ParseIP(config.DNS.BindHost),
+				IP:   bindHost,
 				Port: int(tlsConf.PortDNSOverQUIC),
 			}
 		}
+
+		if tlsConf.PortDNSCrypt != 0 {
+			newconfig.DNSCryptConfig, err = newDNSCrypt(bindHost, tlsConf)
+			if err != nil {
+				// Don't wrap the error, because it's already
+				// wrapped by newDNSCrypt.
+				return dnsforward.ServerConfig{}, err
+			}
+		}
 	}
+
 	newconfig.TLSv12Roots = Context.tlsRoots
 	newconfig.TLSCiphers = Context.tlsCiphers
 	newconfig.TLSAllowUnencryptedDOH = tlsConf.AllowUnencryptedDOH
 
 	newconfig.FilterHandler = applyAdditionalFiltering
 	newconfig.GetCustomUpstreamByClient = Context.clients.FindUpstreams
-	return newconfig
+
+	return newconfig, nil
+}
+
+func newDNSCrypt(bindHost net.IP, tlsConf tlsConfigSettings) (dnscc dnsforward.DNSCryptConfig, err error) {
+	if tlsConf.DNSCryptConfigFile == "" {
+		return dnscc, agherr.Error("no dnscrypt_config_file")
+	}
+
+	f, err := os.Open(tlsConf.DNSCryptConfigFile)
+	if err != nil {
+		return dnscc, fmt.Errorf("opening dnscrypt config: %w", err)
+	}
+	defer f.Close()
+
+	rc := &dnscrypt.ResolverConfig{}
+	err = yaml.NewDecoder(f).Decode(rc)
+	if err != nil {
+		return dnscc, fmt.Errorf("decoding dnscrypt config: %w", err)
+	}
+
+	cert, err := rc.CreateCert()
+	if err != nil {
+		return dnscc, fmt.Errorf("creating dnscrypt cert: %w", err)
+	}
+
+	udpAddr := &net.UDPAddr{
+		IP:   bindHost,
+		Port: tlsConf.PortDNSCrypt,
+	}
+	tcpAddr := &net.TCPAddr{
+		IP:   bindHost,
+		Port: tlsConf.PortDNSCrypt,
+	}
+
+	return dnsforward.DNSCryptConfig{
+		UDPListenAddr: udpAddr,
+		TCPListenAddr: tcpAddr,
+		ResolverCert:  cert,
+		ProviderName:  rc.ProviderName,
+		Enabled:       true,
+	}, nil
 }
 
 type dnsEncryption struct {
@@ -281,11 +342,16 @@ func startDNSServer() error {
 	return nil
 }
 
-func reconfigureDNSServer() error {
-	newconfig := generateServerConfig()
-	err := Context.dnsServer.Reconfigure(&newconfig)
+func reconfigureDNSServer() (err error) {
+	var newconfig dnsforward.ServerConfig
+	newconfig, err = generateServerConfig()
 	if err != nil {
-		return fmt.Errorf("couldn't start forwarding DNS server: %w", err)
+		return fmt.Errorf("generating forwarding dns server config: %w", err)
+	}
+
+	err = Context.dnsServer.Reconfigure(&newconfig)
+	if err != nil {
+		return fmt.Errorf("starting forwarding dns server: %w", err)
 	}
 
 	return nil