From b30b6b1d66c69f37cd97725f18d797a7ff2395d2 Mon Sep 17 00:00:00 2001
From: Andrey Meshkov <am@adguard.com>
Date: Tue, 18 Dec 2018 01:20:38 +0300
Subject: [PATCH] Fix #284

Added DNSCrypt upstreams support
Added DNS Stamps support
---
 client/src/__locales/en.json               |  1 +
 client/src/components/Settings/Upstream.js |  3 ++
 dnsforward/upstream.go                     | 62 ++++++++++++++++++++++
 dnsforward/upstream_test.go                | 27 ++++++++++
 go.mod                                     |  9 ++--
 go.sum                                     | 28 +++++++---
 6 files changed, 118 insertions(+), 12 deletions(-)

diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json
index 57aad1a6..e0eed4ea 100644
--- a/client/src/__locales/en.json
+++ b/client/src/__locales/en.json
@@ -89,6 +89,7 @@
     "example_upstream_regular": "regular DNS (over UDP)",
     "example_upstream_dot": "encrypted <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_TLS' target='_blank'>DNS-over-TLS<\/a>",
     "example_upstream_doh": "encrypted <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS<\/a>",
+    "example_upstream_sdns": "you can use <a href='https:\/\/dnscrypt.info\/stamps\/' target='_blank'>DNS Stamps</a> for <a href='https:\/\/dnscrypt.info\/' target='_blank'>DNSCrypt</a> or <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS</a> resolvers",
     "example_upstream_tcp": "regular DNS (over TCP)",
     "all_filters_up_to_date_toast": "All filters are already up-to-date",
     "updated_upstream_dns_toast": "Updated the upstream DNS servers",
diff --git a/client/src/components/Settings/Upstream.js b/client/src/components/Settings/Upstream.js
index c0d0abf3..4ae0598a 100644
--- a/client/src/components/Settings/Upstream.js
+++ b/client/src/components/Settings/Upstream.js
@@ -73,6 +73,9 @@ class Upstream extends Component {
                                 <li>
                                     <code>tcp://1.1.1.1</code> - { t('example_upstream_tcp') }
                                 </li>
+                                <li>
+                                    <code>sdns://...</code> - <span dangerouslySetInnerHTML={{ __html: t('example_upstream_sdns') }} />
+                                </li>
                             </ol>
                         </div>
                     </div>
diff --git a/dnsforward/upstream.go b/dnsforward/upstream.go
index 89016951..a1bdd1a2 100644
--- a/dnsforward/upstream.go
+++ b/dnsforward/upstream.go
@@ -13,6 +13,9 @@ import (
 	"sync"
 	"time"
 
+	"github.com/jedisct1/go-dnsstamps"
+
+	"github.com/ameshkov/dnscrypt"
 	"github.com/joomcode/errorx"
 	"github.com/miekg/dns"
 )
@@ -177,6 +180,51 @@ func (p *dnsOverHTTPS) Exchange(m *dns.Msg) (*dns.Msg, error) {
 	return &response, nil
 }
 
+//
+// DNSCrypt
+//
+type dnsCrypt struct {
+	boot       bootstrapper
+	client     *dnscrypt.Client     // DNSCrypt client properties
+	serverInfo *dnscrypt.ServerInfo // DNSCrypt server info
+
+	sync.RWMutex // protects DNSCrypt client
+}
+
+func (p *dnsCrypt) Address() string { return p.boot.address }
+
+func (p *dnsCrypt) Exchange(m *dns.Msg) (*dns.Msg, error) {
+
+	var client *dnscrypt.Client
+	var serverInfo *dnscrypt.ServerInfo
+
+	p.RLock()
+	client = p.client
+	serverInfo = p.serverInfo
+	p.RUnlock()
+
+	if client == nil || serverInfo == nil {
+		p.Lock()
+
+		// Using "udp" for DNSCrypt upstreams by default
+		client = &dnscrypt.Client{Proto: "udp", Timeout: defaultTimeout}
+		si, _, err := client.Dial(p.boot.address)
+
+		if err != nil {
+			p.Unlock()
+			return nil, errorx.Decorate(err, "Failed to fetch certificate info from %s", p.Address())
+		}
+
+		p.client = client
+		p.serverInfo = si
+		serverInfo = si
+		p.Unlock()
+	}
+
+	reply, _, err := client.Exchange(m, serverInfo)
+	return reply, err
+}
+
 func (s *Server) chooseUpstream() Upstream {
 	upstreams := s.Upstreams
 	if upstreams == nil {
@@ -200,6 +248,20 @@ func AddressToUpstream(address string, bootstrap string) (Upstream, error) {
 			return nil, errorx.Decorate(err, "Failed to parse %s", address)
 		}
 		switch url.Scheme {
+		case "sdns":
+			stamp, err := dnsstamps.NewServerStampFromString(address)
+			if err != nil {
+				return nil, errorx.Decorate(err, "Failed to parse %s", address)
+			}
+
+			switch stamp.Proto {
+			case dnsstamps.StampProtoTypeDNSCrypt:
+				return &dnsCrypt{boot: toBoot(url.String(), bootstrap)}, nil
+			case dnsstamps.StampProtoTypeDoH:
+				return AddressToUpstream(fmt.Sprintf("https://%s%s", stamp.ProviderName, stamp.Path), bootstrap)
+			}
+
+			return nil, fmt.Errorf("Unsupported protocol %v in %s", stamp.Proto, address)
 		case "dns":
 			if url.Port() == "" {
 				url.Host += ":53"
diff --git a/dnsforward/upstream_test.go b/dnsforward/upstream_test.go
index 0b83670f..3db97fbe 100644
--- a/dnsforward/upstream_test.go
+++ b/dnsforward/upstream_test.go
@@ -8,6 +8,7 @@ import (
 )
 
 func TestUpstreams(t *testing.T) {
+
 	upstreams := []struct {
 		address   string
 		bootstrap string
@@ -56,8 +57,34 @@ func TestUpstreams(t *testing.T) {
 			address:   "https://doh.cleanbrowsing.org/doh/security-filter/",
 			bootstrap: "",
 		},
+		{
+			// AdGuard DNS (DNSCrypt)
+			address:   "sdns://AQIAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20",
+			bootstrap: "",
+		},
+		{
+			// Cisco OpenDNS (DNSCrypt)
+			address:   "sdns://AQAAAAAAAAAADjIwOC42Ny4yMjAuMjIwILc1EUAgbyJdPivYItf9aR6hwzzI1maNDL4Ev6vKQ_t5GzIuZG5zY3J5cHQtY2VydC5vcGVuZG5zLmNvbQ",
+			bootstrap: "8.8.8.8:53",
+		},
+		{
+			// Cloudflare DNS (DoH)
+			address:   "sdns://AgcAAAAAAAAABzEuMC4wLjGgENk8mGSlIfMGXMOlIlCcKvq7AVgcrZxtjon911-ep0cg63Ul-I8NlFj4GplQGb_TTLiczclX57DvMV8Q-JdjgRgSZG5zLmNsb3VkZmxhcmUuY29tCi9kbnMtcXVlcnk",
+			bootstrap: "8.8.8.8:53",
+		},
+		{
+			// doh-cleanbrowsing-security (https://doh.cleanbrowsing.org/doh/security-filter/)
+			address:   "sdns://AgMAAAAAAAAAAAAVZG9oLmNsZWFuYnJvd3Npbmcub3JnFS9kb2gvc2VjdXJpdHktZmlsdGVyLw",
+			bootstrap: "8.8.8.8:53",
+		},
+		{
+			// Google (DNS-over-HTTPS)
+			address:   "sdns://AgUAAAAAAAAAACAe9iTP_15r07rd8_3b_epWVGfjdymdx-5mdRZvMAzBuQ5kbnMuZ29vZ2xlLmNvbQ0vZXhwZXJpbWVudGFs",
+			bootstrap: "8.8.8.8:53",
+		},
 	}
 	for _, test := range upstreams {
+
 		t.Run(test.address, func(t *testing.T) {
 			u, err := AddressToUpstream(test.address, test.bootstrap)
 			if err != nil {
diff --git a/go.mod b/go.mod
index 166e3cce..9acaa0dc 100644
--- a/go.mod
+++ b/go.mod
@@ -2,20 +2,21 @@ module github.com/AdguardTeam/AdGuardHome
 
 require (
 	github.com/StackExchange/wmi v0.0.0-20180725035823-b12b22c5341f // indirect
+	github.com/ameshkov/dnscrypt v0.0.0-20181217090431-1215bb8b150f
 	github.com/beefsack/go-rate v0.0.0-20180408011153-efa7637bb9b6
 	github.com/bluele/gcache v0.0.0-20171010155617-472614239ac7
 	github.com/go-ole/go-ole v1.2.1 // indirect
 	github.com/go-test/deep v1.0.1
 	github.com/gobuffalo/packr v1.19.0
+	github.com/jedisct1/go-dnsstamps v0.0.0-20180418170050-1e4999280f86
 	github.com/joomcode/errorx v0.1.0
-	github.com/miekg/dns v1.0.15
+	github.com/miekg/dns v1.1.1
 	github.com/patrickmn/go-cache v2.1.0+incompatible
 	github.com/shirou/gopsutil v2.18.10+incompatible
 	github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 // indirect
 	go.uber.org/goleak v0.10.0
-	golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd
-	golang.org/x/net v0.0.0-20181108082009-03003ca0c849
-	golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8 // indirect
+	golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9
+	golang.org/x/net v0.0.0-20181213202711-891ebc4b82d6
 	gopkg.in/asaskevich/govalidator.v4 v4.0.0-20160518190739-766470278477
 	gopkg.in/yaml.v2 v2.2.1
 )
diff --git a/go.sum b/go.sum
index af10df24..33ffb8cf 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,11 @@
 github.com/StackExchange/wmi v0.0.0-20180725035823-b12b22c5341f h1:5ZfJxyXo8KyX8DgGXC5B7ILL8y51fci/qYz2B4j8iLY=
 github.com/StackExchange/wmi v0.0.0-20180725035823-b12b22c5341f/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
+github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
+github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
+github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 h1:52m0LGchQBBVqJRyYYufQuIbVqRawmubW3OFGqK1ekw=
+github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635/go.mod h1:lmLxL+FV291OopO93Bwf9fQLQeLyt33VJRUg5VJ30us=
+github.com/ameshkov/dnscrypt v0.0.0-20181217090431-1215bb8b150f h1:vOaSvI9B3wqzV1g8raDeVzRJnq5RHQxsz0MVXudxdNU=
+github.com/ameshkov/dnscrypt v0.0.0-20181217090431-1215bb8b150f/go.mod h1:EC7Z1GguyEEwhuLXrcgkRTE3GdyPDSWq2OXefhydGWo=
 github.com/beefsack/go-rate v0.0.0-20180408011153-efa7637bb9b6 h1:KXlsf+qt/X5ttPGEjR0tPH1xaWWoKBEg9Q1THAj2h3I=
 github.com/beefsack/go-rate v0.0.0-20180408011153-efa7637bb9b6/go.mod h1:6YNgTHLutezwnBvyneBbwvB8C82y3dcoOj5EQJIdGXA=
 github.com/bluele/gcache v0.0.0-20171010155617-472614239ac7 h1:NpQ+gkFOH27AyDypSCJ/LdsIi/b4rdnEb1N5+IpFfYs=
@@ -17,14 +23,18 @@ github.com/gobuffalo/packd v0.0.0-20181031195726-c82734870264/go.mod h1:Yf2toFaI
 github.com/gobuffalo/packr v1.19.0 h1:3UDmBDxesCOPF8iZdMDBBWKfkBoYujIMIZePnobqIUI=
 github.com/gobuffalo/packr v1.19.0/go.mod h1:MstrNkfCQhd5o+Ct4IJ0skWlxN8emOq8DsoT1G98VIU=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/jedisct1/go-dnsstamps v0.0.0-20180418170050-1e4999280f86 h1:Olj4M6T1omUfx7yTTcnhLf4xo6gYMmRHSJIfeA1NZy0=
+github.com/jedisct1/go-dnsstamps v0.0.0-20180418170050-1e4999280f86/go.mod h1:j/ONpSHHmPgDwmFKXg9vhQvIjADe/ft1X4a3TVOmp9g=
+github.com/jedisct1/xsecretbox v0.0.0-20180508184500-7a679c0bcd9a h1:2nyBWKszM41RO/gt5ElUXigAFiRgJ9KifHDlWOlw0lc=
+github.com/jedisct1/xsecretbox v0.0.0-20180508184500-7a679c0bcd9a/go.mod h1:YlN58h704uRFD0BwsEGTq+7Wx+WG2i7P49bc+HwHyAY=
 github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
 github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
 github.com/joomcode/errorx v0.1.0 h1:QmJMiI1DE1UFje2aI1ZWO/VMT5a32qBoXUclGOt8vsc=
 github.com/joomcode/errorx v0.1.0/go.mod h1:kgco15ekB6cs+4Xjzo7SPeXzx38PbJzBwbnu9qfVNHQ=
 github.com/markbates/oncer v0.0.0-20181014194634-05fccaae8fc4 h1:Mlji5gkcpzkqTROyE4ZxZ8hN7osunMb2RuGVrbvMvCc=
 github.com/markbates/oncer v0.0.0-20181014194634-05fccaae8fc4/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
-github.com/miekg/dns v1.0.15 h1:9+UupePBQCG6zf1q/bGmTO1vumoG13jsrbWOSX1W6Tw=
-github.com/miekg/dns v1.0.15/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
+github.com/miekg/dns v1.1.1 h1:DVkblRdiScEnEr0LR9nTnEQqHYycjkXW9bOjd+2EL2o=
+github.com/miekg/dns v1.1.1/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
 github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
 github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
 github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
@@ -43,15 +53,17 @@ github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 go.uber.org/goleak v0.10.0 h1:G3eWbSNIskeRqtsN/1uI5B+eP73y3JUuBsv9AZjehb4=
 go.uber.org/goleak v0.10.0/go.mod h1:VCZuO8V8mFPlL0F5J5GK1rtHV3DrFcQ1R8ryq7FK0aI=
-golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd h1:VtIkGDhk0ph3t+THbvXHfMZ8QHgsBO39Nh52+74pq7w=
-golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0=
+golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20181108082009-03003ca0c849 h1:FSqE2GGG7wzsYUsWiQ8MZrvEd1EOyU3NCF0AW3Wtltg=
-golang.org/x/net v0.0.0-20181108082009-03003ca0c849/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181213202711-891ebc4b82d6 h1:gT0Y6H7hbVPUtvtk0YGxMXPgN+p8fYlqWkgJeUCZcaQ=
+golang.org/x/net v0.0.0-20181213202711-891ebc4b82d6/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8 h1:YoY1wS6JYVRpIfFngRf2HHo9R9dAne3xbkGOQ5rJXjU=
-golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20181213200352-4d1cda033e06 h1:0oC8rFnE+74kEmuHZ46F6KHsMr5Gx2gUQPuNz28iQZM=
+golang.org/x/sys v0.0.0-20181213200352-4d1cda033e06/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 gopkg.in/asaskevich/govalidator.v4 v4.0.0-20160518190739-766470278477 h1:5xUJw+lg4zao9W4HIDzlFbMYgSgtvNVHh00MEHvbGpQ=
 gopkg.in/asaskevich/govalidator.v4 v4.0.0-20160518190739-766470278477/go.mod h1:QDV1vrFSrowdoOba0UM8VJPUZONT7dnfdLsM+GG53Z8=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=