From 050e996a35b1f26f1c03af7eda75584afad9bfc1 Mon Sep 17 00:00:00 2001
From: ArtemBaskal <a.baskal@adguard.com>
Date: Thu, 3 Sep 2020 19:33:49 +0300
Subject: [PATCH 01/19] - client: Fix superfluous character in de locale

---
 client/src/__locales/de.json | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/client/src/__locales/de.json b/client/src/__locales/de.json
index 2a9784ad..72f7989f 100644
--- a/client/src/__locales/de.json
+++ b/client/src/__locales/de.json
@@ -45,7 +45,7 @@
     "dhcp_warning": "Wenn Sie den DHCP-Server trotzdem aktivieren möchten, stellen Sie sicher, dass sich in Ihrem Netzwerk kein anderer aktiver DHCP-Server befindet. Andernfalls kann es bei angeschlossenen Geräten zu einem Ausfall des Internets kommen!",
     "dhcp_error": "Es konnte nicht ermittelt werden, ob es einen anderen DHCP-Server im Netzwerk gibt.",
     "dhcp_static_ip_error": "Um den DHCP-Server nutzen zu können, muss eine statische IP-Adresse festgelegt werden. Es konnte nicht ermittelt werden, ob diese Netzwerkschnittstelle mit statischer IP-Adresse konfiguriert ist. Bitte legen Sie eine statische IP-Adresse manuell fest.",
-    "dhcp_dynamic_ip_found": "Ihr System verwendet die dynamische Konfiguration der IP-Adresse für die Schnittstelle <0>{{interfaceName}}</0>. Um den DHCP-Server nutzen zu können, muss eine statische IP-Adresse festgelegt werden. Ihre aktuelle IP-Adresse ist <0>{{ipAddress}}}</0>. Diese IP-Adresse wird automatisch als statisch festgelegt, sobald Sie auf die Schaltfläche „DHCP aktivieren” klicken.",
+    "dhcp_dynamic_ip_found": "Ihr System verwendet die dynamische Konfiguration der IP-Adresse für die Schnittstelle <0>{{interfaceName}}</0>. Um den DHCP-Server nutzen zu können, muss eine statische IP-Adresse festgelegt werden. Ihre aktuelle IP-Adresse ist <0>{{ipAddress}}</0>. Diese IP-Adresse wird automatisch als statisch festgelegt, sobald Sie auf die Schaltfläche „DHCP aktivieren” klicken.",
     "dhcp_lease_added": "Statischer Lease „{{key}}” erfolgreich hinzugefügt",
     "dhcp_lease_deleted": "Statischer Lease „{{key}}” erfolgreich entfernt",
     "dhcp_new_static_lease": "Neuer statischer Lease",
@@ -99,7 +99,7 @@
     "no_clients_found": "Keine Clients gefunden",
     "general_statistics": "Allgemeine Statistiken",
     "number_of_dns_query_days": "Anzahl der in den letzten {{count}} Tagen verarbeiteten DNS-Anfragen",
-    "number_of_dns_query_days_plural": "Anzahl der DNS-Abfragen, die in den letzten {{count}}} Tagen verarbeitet wurden",
+    "number_of_dns_query_days_plural": "Anzahl der DNS-Abfragen, die in den letzten {{count}} Tagen verarbeitet wurden",
     "number_of_dns_query_24_hours": "Anzahl der in den letzten 24 Stunden durchgeführten DNS-Anfragen",
     "number_of_dns_query_blocked_24_hours": "Anzahl der durch Werbefilter und Host-Blocklisten geblockten DNS-Anfragen",
     "number_of_dns_query_blocked_24_hours_by_sec": "Anzahl der durch das AdGuard-Modul für Internet-Sicherheit blockierten DNS-Anfragen",

From 9e33bd52599c2039603d99f93db461cc6a6a23f4 Mon Sep 17 00:00:00 2001
From: ArtemBaskal <a.baskal@adguard.com>
Date: Thu, 3 Sep 2020 20:35:20 +0300
Subject: [PATCH 02/19] - client: Display service name for blocked services

---
 client/src/components/Logs/Cells/DomainCell.js | 8 +++++++-
 client/src/helpers/helpers.js                  | 2 +-
 2 files changed, 8 insertions(+), 2 deletions(-)

diff --git a/client/src/components/Logs/Cells/DomainCell.js b/client/src/components/Logs/Cells/DomainCell.js
index 4333089c..47d14846 100644
--- a/client/src/components/Logs/Cells/DomainCell.js
+++ b/client/src/components/Logs/Cells/DomainCell.js
@@ -14,6 +14,7 @@ import IconTooltip from './IconTooltip';
 
 const DomainCell = ({
     answer_dnssec,
+    service_name,
     client_proto,
     domain,
     time,
@@ -49,6 +50,10 @@ const DomainCell = ({
         protocol,
     };
 
+    if (service_name) {
+        requestDetailsObj.check_service = service_name;
+    }
+
     const sourceData = getSourceData(tracker);
 
     const knownTrackerDataObj = {
@@ -98,7 +103,7 @@ const DomainCell = ({
                      xlinkHref='privacy' contentItemClass='key-colon' renderContent={renderContent}
                      place='bottom' />
         <div className={valueClass}>
-            <div className="text-truncate" title={domain}>{domain}</div>
+            <div className="text-truncate" title={domain}>{service_name || domain}</div>
             {details && isDetailed
             && <div className="detailed-info d-none d-sm-block text-truncate"
                     title={details}>{details}</div>}
@@ -112,6 +117,7 @@ DomainCell.propTypes = {
     domain: propTypes.string.isRequired,
     time: propTypes.string.isRequired,
     type: propTypes.string.isRequired,
+    service_name: propTypes.string,
     tracker: propTypes.object,
 };
 
diff --git a/client/src/helpers/helpers.js b/client/src/helpers/helpers.js
index 9fdd9fad..fa3a5046 100644
--- a/client/src/helpers/helpers.js
+++ b/client/src/helpers/helpers.js
@@ -97,7 +97,7 @@ export const normalizeLogs = (logs) => logs.map((log) => {
         filterId,
         rule,
         status,
-        serviceName: service_name,
+        service_name,
         originalAnswer: original_answer,
         originalResponse: processResponse(original_answer),
         tracker: getTrackerData(domain),

From b54ce85d3d5f39957d0a2c3625c04e4936c40d94 Mon Sep 17 00:00:00 2001
From: ArtemBaskal <a.baskal@adguard.com>
Date: Thu, 3 Sep 2020 20:57:22 +0300
Subject: [PATCH 03/19] - client: Fix top clients alignment

---
 client/src/components/ui/Card.css | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/client/src/components/ui/Card.css b/client/src/components/ui/Card.css
index cef0e71d..5930d881 100644
--- a/client/src/components/ui/Card.css
+++ b/client/src/components/ui/Card.css
@@ -16,7 +16,11 @@
 
 .card-table-overflow--limited {
     overflow-y: auto;
-    max-height: 280px;
+    max-height: 17.5rem;
+}
+
+.card-table-overflow--limited.clients__table {
+    max-height: 18rem;
 }
 
 .card-actions {

From 07b6cc24b77c2bc300278a518072bb66280173b0 Mon Sep 17 00:00:00 2001
From: Simon Zolin <s.zolin@adguard.com>
Date: Thu, 27 Aug 2020 14:20:17 +0300
Subject: [PATCH 04/19] * dnsproxy v0.32.0

---
 go.mod |   5 +-
 go.sum | 214 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
 2 files changed, 210 insertions(+), 9 deletions(-)

diff --git a/go.mod b/go.mod
index 1a81eba8..476a496f 100644
--- a/go.mod
+++ b/go.mod
@@ -3,13 +3,12 @@ module github.com/AdguardTeam/AdGuardHome
 go 1.14
 
 require (
-	github.com/AdguardTeam/dnsproxy v0.31.1
+	github.com/AdguardTeam/dnsproxy v0.32.0
 	github.com/AdguardTeam/golibs v0.4.2
 	github.com/AdguardTeam/urlfilter v0.12.2
 	github.com/NYTimes/gziphandler v1.1.1
 	github.com/fsnotify/fsnotify v1.4.9
 	github.com/gobuffalo/packr v1.30.1
-	github.com/google/go-cmp v0.4.0 // indirect
 	github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714
 	github.com/insomniacslk/dhcp v0.0.0-20200621044212-d74cd86ad5b8
 	github.com/joomcode/errorx v1.0.1
@@ -24,7 +23,7 @@ require (
 	github.com/u-root/u-root v6.0.0+incompatible
 	go.etcd.io/bbolt v1.3.4
 	golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
-	golang.org/x/net v0.0.0-20200625001655-4c5254603344
+	golang.org/x/net v0.0.0-20200707034311-ab3426394381
 	golang.org/x/sys v0.0.0-20200519105757-fe76b779f299
 	gopkg.in/natefinch/lumberjack.v2 v2.0.0
 	gopkg.in/yaml.v2 v2.3.0
diff --git a/go.sum b/go.sum
index 794cd232..d1e535fa 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,14 @@
-github.com/AdguardTeam/dnsproxy v0.31.1 h1:kYnlLGM20LjPlEH+fqwCy08gMP5EVdp1FRaJ7uzyIJ0=
-github.com/AdguardTeam/dnsproxy v0.31.1/go.mod h1:hOYFV9TW+pd5XKYz7KZf2FFD8SvSPqjyGTxUae86s58=
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo=
+dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
+dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
+dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
+dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
+git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
+github.com/AdguardTeam/dnsproxy v0.32.0 h1:taULDOMubiQSvRLynn8GlfMunhKaVryCBd/OkM++YFU=
+github.com/AdguardTeam/dnsproxy v0.32.0/go.mod h1:ZLDrKIypYxBDz2N9FQHgeehuHrwTbuhZXdGwNySshbw=
 github.com/AdguardTeam/golibs v0.4.0/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
 github.com/AdguardTeam/golibs v0.4.2 h1:7M28oTZFoFwNmp8eGPb3ImmYbxGaJLyQXeIFVHjME0o=
 github.com/AdguardTeam/golibs v0.4.2/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
@@ -20,21 +29,35 @@ github.com/ameshkov/dnscrypt v1.1.0 h1:2vAt5dD6ZmqlAxEAfzRcLBnkvdf8NI46Kn9InSwQb
 github.com/ameshkov/dnscrypt v1.1.0/go.mod h1:ikduAxNLCTEfd1AaCgpIA5TgroIVQ8JY3Vb095fiFJg=
 github.com/ameshkov/dnsstamps v1.0.1 h1:LhGvgWDzhNJh+kBQd/AfUlq1vfVe109huiXw4JhnPug=
 github.com/ameshkov/dnsstamps v1.0.1/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A=
+github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
 github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
 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/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
+github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
+github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE=
+github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
 github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
 github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
 github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
+github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
 github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
+github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
 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-test/deep v1.0.5 h1:AKODKU3pDH1RzZzm6YZu77YWtEAq6uh1rLIAQlay2qc=
@@ -49,12 +72,43 @@ github.com/gobuffalo/packr v1.30.1 h1:hu1fuVR3fXEZR7rXNW3h8rqSML8EVAf6KNm0NKO/wK
 github.com/gobuffalo/packr v1.30.1/go.mod h1:ljMyFO2EcrnzsHsN99cvbq055Y9OhRrIaviy289eRuk=
 github.com/gobuffalo/packr/v2 v2.5.1 h1:TFOeY2VoGamPjQLiNDT3mn//ytzk236VMO2j7iHxJR4=
 github.com/gobuffalo/packr/v2 v2.5.1/go.mod h1:8f9c96ITobJlPzI44jj+4tHnEKNt0xXWSVlXRN9X1Iw=
+github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
+github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
+github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
+github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
+github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
+github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
+github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
 github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714 h1:/jC7qQFrv8CrSJVmaolDVOxTfS9kc36uB6H40kdbQq8=
 github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis=
 github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
@@ -62,16 +116,20 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 github.com/insomniacslk/dhcp v0.0.0-20200621044212-d74cd86ad5b8 h1:u+vle+5E78+cT/CSMD5/Y3NUpMgA83Yu2KhG+Zbco/k=
 github.com/insomniacslk/dhcp v0.0.0-20200621044212-d74cd86ad5b8/go.mod h1:CfMdguCK66I5DAUJgGKyNz8aB6vO5dZzkm9Xep6WGvw=
+github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
 github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
 github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
 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 v1.0.1 h1:CalpDWz14ZHd68fIqluJasJosAewpz2TFaJALrUxjrk=
 github.com/joomcode/errorx v1.0.1/go.mod h1:kgco15ekB6cs+4Xjzo7SPeXzx38PbJzBwbnu9qfVNHQ=
+github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
 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/karrick/godirwalk v1.10.12 h1:BqUm+LuJcXjGv1d2mj3gBiQyrQ57a0rYoAmhvJQ7RDU=
 github.com/karrick/godirwalk v1.10.12/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
 github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -79,38 +137,94 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJ
 github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 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.0 h1:JhQDdqxdwdmGdKsKgXi1+coHRoGhvU6z0rNzOJqZ/4o=
+github.com/lucas-clemente/quic-go v0.18.0/go.mod h1:yXttHsSNxQi8AWijC/vLP+OJczXqzHSOcJrM5ITUlCg=
+github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
 github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
+github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/marten-seemann/qpack v0.2.0/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc=
+github.com/marten-seemann/qtls v0.10.0 h1:ECsuYUKalRL240rRD4Ri33ISb7kAQ3qGDlrrl55b2pc=
+github.com/marten-seemann/qtls v0.10.0/go.mod h1:UvMd1oaYDACI99/oZUYLzMCkBXQVT0aGm99sJhbT8hs=
+github.com/marten-seemann/qtls-go1-15 v0.1.0 h1:i/YPXVxz8q9umso/5y474CNcHmTpA+5DH+mFPjx6PZg=
+github.com/marten-seemann/qtls-go1-15 v0.1.0/go.mod h1:GyFwywLKkRt+6mfU99csTEY1joMZz5vmB1WNZH3P81I=
+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7 h1:lez6TS6aAau+8wXUP3G9I3TGlmPFEq2CTxBaRqY6AGE=
 github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7/go.mod h1:U6ZQobyTjI/tJyq2HG+i/dfSoFUt8/aZCM+GKtmFk/Y=
 github.com/mdlayher/raw v0.0.0-20190606142536-fef19f00fc18/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg=
 github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065 h1:aFkJ6lx4FPip+S+Uw4aTegFMct9shDvP+79PsSxpm3w=
 github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg=
+github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
 github.com/miekg/dns v1.1.29 h1:xHBEhR+t5RzcFJjBLJlax2daXOrTYtr9z4WdKEfWFzg=
 github.com/miekg/dns v1.1.29/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
+github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
+github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
+github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
+github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA=
+github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
+github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
+github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE=
+github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
+github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
 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/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
+github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rogpeppe/go-internal v1.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
+github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
 github.com/shirou/gopsutil v2.20.3+incompatible h1:0JVooMPsT7A7HqEYdydp/OfjSOYSjhXV7w1hkKj/NPQ=
 github.com/shirou/gopsutil v2.20.3+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
+github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
+github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
+github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
+github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
+github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
+github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw=
+github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI=
+github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
+github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
+github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg=
+github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw=
+github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y=
+github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
+github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q=
+github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ=
+github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I=
+github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0=
+github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
+github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk=
+github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
+github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
+github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
 github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
 github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
 github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
+github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
+github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
 github.com/sparrc/go-ping v0.0.0-20190613174326-4e5b6552494c h1:gqEdF4VwBu3lTKGHS9rXE9x1/pEaSwCXRLOZRF6qtlw=
 github.com/sparrc/go-ping v0.0.0-20190613174326-4e5b6552494c/go.mod h1:eMyUVp6f/5jnzM+3zahzl7q6UXLbgSc3MKg/+ow9QW0=
 github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
@@ -126,25 +240,47 @@ github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
 github.com/u-root/u-root v6.0.0+incompatible h1:YqPGmRoRyYmeg17KIWFRSyVq6LX5T6GSzawyA6wG6EE=
 github.com/u-root/u-root v6.0.0+incompatible/go.mod h1:RYkpo8pTHrNjW08opNd/U6p/RJE7K0D8fXO0d47+3YY=
 github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
+github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
+github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
 github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
 go.etcd.io/bbolt v1.3.4 h1:hi1bXHMVrlQh6WwxAy+qZCV/SYIlqo+Ushwdpa4tAKg=
 go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
+go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
+go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
+golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
+golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20200403201458-baeed622b8d8 h1:fpnn/HnJONpIu6hkXi1u/7rR0NzilgWr4T0JmWkEitk=
-golang.org/x/crypto v0.0.0-20200403201458-baeed622b8d8/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
 golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190419010253-1f3472d942ba/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
@@ -153,46 +289,112 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
 golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k=
 golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4=
-golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o=
 golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/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-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190418153312-f0ce4c0180be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190606122018-79a91cf218c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200331124033-c3d80250170d h1:nc5K6ox/4lTFbMVSL9WRR81ixkcwXThoiF6yf+R9scA=
 golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 h1:DYfZAGf2WMFjMxbgTjaC+2HC7NkNAQs+6Q8b9WEB/F4=
 golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 golang.org/x/tools v0.0.0-20190624180213-70d37148ca0c/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
 golang.org/x/tools v0.0.0-20191216052735-49a3e744a425 h1:VvQyQJN0tSuecqgcIxMWnnfG5kSmgy9KZR9sW3W5QeA=
 golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
+google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
+google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
+google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
+google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
+google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
 gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
 gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
 gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
 gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
 gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
+honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
+rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
+sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
+sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=

From d53e32259a3a1b5f55b3e1730339f1a0410eb1be Mon Sep 17 00:00:00 2001
From: Simon Zolin <s.zolin@adguard.com>
Date: Thu, 27 Aug 2020 15:03:07 +0300
Subject: [PATCH 05/19] + DNS: "port_dns_over_quic" setting

---
 AGHTechDoc.md                 |  2 ++
 dnsforward/config.go          |  5 +++++
 dnsforward/dnsforward_http.go |  2 +-
 home/config.go                | 16 +++++++++-------
 home/control_update.go        |  4 +++-
 home/dns.go                   | 13 +++++++++++++
 home/tls.go                   |  2 ++
 openapi/openapi.yaml          |  5 +++++
 8 files changed, 40 insertions(+), 9 deletions(-)

diff --git a/AGHTechDoc.md b/AGHTechDoc.md
index e5e582ec..0dcb001b 100644
--- a/AGHTechDoc.md
+++ b/AGHTechDoc.md
@@ -743,6 +743,7 @@ Response:
 	"server_name":"...",
 	"port_https":443,
 	"port_dns_over_tls":853,
+	"port_dns_over_quic":784,
 	"certificate_chain":"...",
 	"private_key":"...",
 	"certificate_path":"...",
@@ -774,6 +775,7 @@ Request:
 	"force_https":false,
 	"port_https":443,
 	"port_dns_over_tls":853,
+	"port_dns_over_quic":784,
 	"certificate_chain":"...",
 	"private_key":"...",
 	"certificate_path":"...", // if set, certificate_chain must be empty
diff --git a/dnsforward/config.go b/dnsforward/config.go
index 69af11eb..5d06b9dc 100644
--- a/dnsforward/config.go
+++ b/dnsforward/config.go
@@ -92,6 +92,7 @@ type FilteringConfig struct {
 // TLSConfig is the TLS configuration for HTTPS, DNS-over-HTTPS, and DNS-over-TLS
 type TLSConfig struct {
 	TLSListenAddr  *net.TCPAddr `yaml:"-" json:"-"`
+	QUICListenAddr *net.UDPAddr `yaml:"-" json:"-"`
 	StrictSNICheck bool         `yaml:"strict_sni_check" json:"-"` // Reject connection if the client uses server name (in SNI) that doesn't match the certificate
 
 	CertificateChain string `yaml:"certificate_chain" json:"certificate_chain"` // PEM-encoded certificates chain
@@ -153,6 +154,10 @@ func (s *Server) createProxyConfig() (proxy.Config, error) {
 		MaxGoroutines:          int(s.conf.MaxGoroutines),
 	}
 
+	if s.conf.QUICListenAddr != nil {
+		proxyConfig.QUICListenAddr = []*net.UDPAddr{s.conf.QUICListenAddr}
+	}
+
 	if s.conf.CacheSize != 0 {
 		proxyConfig.CacheEnabled = true
 		proxyConfig.CacheSizeBytes = int(s.conf.CacheSize)
diff --git a/dnsforward/dnsforward_http.go b/dnsforward/dnsforward_http.go
index 63e82d95..79e4ded7 100644
--- a/dnsforward/dnsforward_http.go
+++ b/dnsforward/dnsforward_http.go
@@ -270,7 +270,7 @@ func ValidateUpstreams(upstreams []string) error {
 	return nil
 }
 
-var protocols = []string{"tls://", "https://", "tcp://", "sdns://"}
+var protocols = []string{"tls://", "https://", "tcp://", "sdns://", "quic://"}
 
 func validateUpstream(u string) (bool, error) {
 	// Check if user tries to specify upstream for domain
diff --git a/home/config.go b/home/config.go
index d0fc6396..348e72fa 100644
--- a/home/config.go
+++ b/home/config.go
@@ -92,11 +92,12 @@ type dnsConfig struct {
 }
 
 type tlsConfigSettings struct {
-	Enabled        bool   `yaml:"enabled" json:"enabled"`                               // Enabled is the encryption (DOT/DOH/HTTPS) status
-	ServerName     string `yaml:"server_name" json:"server_name,omitempty"`             // ServerName is the hostname of your HTTPS/TLS server
-	ForceHTTPS     bool   `yaml:"force_https" json:"force_https,omitempty"`             // ForceHTTPS: if true, forces HTTP->HTTPS redirect
-	PortHTTPS      int    `yaml:"port_https" json:"port_https,omitempty"`               // HTTPS port. If 0, HTTPS will be disabled
-	PortDNSOverTLS int    `yaml:"port_dns_over_tls" json:"port_dns_over_tls,omitempty"` // DNS-over-TLS port. If 0, DOT will be disabled
+	Enabled         bool   `yaml:"enabled" json:"enabled"`                                 // Enabled is the encryption (DOT/DOH/HTTPS) status
+	ServerName      string `yaml:"server_name" json:"server_name,omitempty"`               // ServerName is the hostname of your HTTPS/TLS server
+	ForceHTTPS      bool   `yaml:"force_https" json:"force_https,omitempty"`               // ForceHTTPS: if true, forces HTTP->HTTPS redirect
+	PortHTTPS       int    `yaml:"port_https" json:"port_https,omitempty"`                 // HTTPS port. If 0, HTTPS will be disabled
+	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
 
 	// Allow DOH queries via unencrypted HTTP (e.g. for reverse proxying)
 	AllowUnencryptedDOH bool `yaml:"allow_unencrypted_doh" json:"allow_unencrypted_doh"`
@@ -124,8 +125,9 @@ var config = configuration{
 		FiltersUpdateIntervalHours: 24,
 	},
 	TLS: tlsConfigSettings{
-		PortHTTPS:      443,
-		PortDNSOverTLS: 853, // needs to be passed through to dnsproxy
+		PortHTTPS:       443,
+		PortDNSOverTLS:  853, // needs to be passed through to dnsproxy
+		PortDNSOverQUIC: 784,
 	},
 	logSettings: logSettings{
 		LogCompress:   false,
diff --git a/home/control_update.go b/home/control_update.go
index fb160900..b8f6bcbe 100644
--- a/home/control_update.go
+++ b/home/control_update.go
@@ -99,7 +99,9 @@ func getVersionResp(info update.VersionInfo) []byte {
 		Context.tls.WriteDiskConfig(&tlsConf)
 
 		if runtime.GOOS != "windows" &&
-			((tlsConf.Enabled && (tlsConf.PortHTTPS < 1024 || tlsConf.PortDNSOverTLS < 1024)) ||
+			((tlsConf.Enabled && (tlsConf.PortHTTPS < 1024 ||
+				tlsConf.PortDNSOverTLS < 1024 ||
+				tlsConf.PortDNSOverQUIC < 1024)) ||
 				config.BindPort < 1024 ||
 				config.DNS.Port < 1024) {
 			// On UNIX, if we're running under a regular user,
diff --git a/home/dns.go b/home/dns.go
index 2d647f94..820b441a 100644
--- a/home/dns.go
+++ b/home/dns.go
@@ -172,12 +172,20 @@ func generateServerConfig() dnsforward.ServerConfig {
 	Context.tls.WriteDiskConfig(&tlsConf)
 	if tlsConf.Enabled {
 		newconfig.TLSConfig = tlsConf.TLSConfig
+
 		if tlsConf.PortDNSOverTLS != 0 {
 			newconfig.TLSListenAddr = &net.TCPAddr{
 				IP:   net.ParseIP(config.DNS.BindHost),
 				Port: tlsConf.PortDNSOverTLS,
 			}
 		}
+
+		if tlsConf.PortDNSOverQUIC != 0 {
+			newconfig.QUICListenAddr = &net.UDPAddr{
+				IP:   net.ParseIP(config.DNS.BindHost),
+				Port: int(tlsConf.PortDNSOverQUIC),
+			}
+		}
 	}
 	newconfig.TLSv12Roots = Context.tlsRoots
 	newconfig.TLSCiphers = Context.tlsCiphers
@@ -225,6 +233,11 @@ func getDNSAddresses() []string {
 			addr := fmt.Sprintf("tls://%s:%d", tlsConf.ServerName, tlsConf.PortDNSOverTLS)
 			dnsAddresses = append(dnsAddresses, addr)
 		}
+
+		if tlsConf.PortDNSOverQUIC != 0 {
+			addr := fmt.Sprintf("quic://%s:%d", tlsConf.ServerName, tlsConf.PortDNSOverQUIC)
+			dnsAddresses = append(dnsAddresses, addr)
+		}
 	}
 
 	return dnsAddresses
diff --git a/home/tls.go b/home/tls.go
index 157cda2a..0b9e8f90 100644
--- a/home/tls.go
+++ b/home/tls.go
@@ -45,6 +45,7 @@ func tlsCreate(conf tlsConfigSettings) *TLSMod {
 				ServerName:          conf.ServerName,
 				PortHTTPS:           conf.PortHTTPS,
 				PortDNSOverTLS:      conf.PortDNSOverTLS,
+				PortDNSOverQUIC:     conf.PortDNSOverQUIC,
 				AllowUnencryptedDOH: conf.AllowUnencryptedDOH,
 			}}
 		}
@@ -267,6 +268,7 @@ func (t *TLSMod) handleTLSConfigure(w http.ResponseWriter, r *http.Request) {
 	t.conf.ForceHTTPS = data.ForceHTTPS
 	t.conf.PortHTTPS = data.PortHTTPS
 	t.conf.PortDNSOverTLS = data.PortDNSOverTLS
+	t.conf.PortDNSOverQUIC = data.PortDNSOverQUIC
 	t.conf.CertificateChain = data.CertificateChain
 	t.conf.CertificatePath = data.CertificatePath
 	t.conf.CertificateChainData = data.CertificateChainData
diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml
index 57d63c70..7777c069 100644
--- a/openapi/openapi.yaml
+++ b/openapi/openapi.yaml
@@ -1563,6 +1563,11 @@ components:
                     format: int32
                     example: 853
                     description: DNS-over-TLS port. If 0, DOT will be disabled.
+                port_dns_over_quic:
+                    type: integer
+                    format: int32
+                    example: 784
+                    description: DNS-over-QUIC port. If 0, DOQ will be disabled.
                 certificate_chain:
                     type: string
                     description: Base64 string with PEM-encoded certificates chain

From 8dc010886867de62340d933df7edc31314234abc Mon Sep 17 00:00:00 2001
From: Artem Baskal <a.baskal@adguard.com>
Date: Sat, 5 Sep 2020 10:22:47 +0300
Subject: [PATCH 06/19] + client: Redesign query logs block/unblock buttons

Close #2050

Squashed commit of the following:

commit 3bc6a409034989b914306e1c33da274730ca623e
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Fri Sep 4 20:58:09 2020 +0300

    Change dashboard block confirm message

commit d4d47c3557e2166ee04db25a71b782bfbfe3b865
Merge: e8865827 fc43e2ac
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Fri Sep 4 14:56:34 2020 +0300

    Merge branch 'master' into feature/2050

commit e8865827879955b1ef62c9ff85798d07bfa4627d
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Fri Sep 4 13:46:10 2020 +0300

    Rename classname

commit 648151c54e493c63622e014cb9cd1cb450f25478
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Thu Sep 3 19:09:21 2020 +0300

    Decrease arrow size

commit 4feadab707c613d31225dfa9443a9a836db37ba1
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Thu Sep 3 18:27:41 2020 +0300

    Rename button class

commit c3919d8ae8d1431657ce61afad2c20e5806f279a
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Thu Sep 3 10:35:15 2020 +0300

    Review changes: extract variables

commit 0ac809584c391e41a1749a844bc1075e05a92345
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Thu Sep 3 10:13:57 2020 +0300

    Display dashboard button on hover

commit 1395287c2383e2248a2a5d39451403bd73141e55
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Wed Sep 2 21:24:04 2020 +0300

    Do not hide button on option open

commit 947f254b7aea26f289b66b66fac46dba11ea3952
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Wed Sep 2 21:20:19 2020 +0300

    Add buttons for mobile screen

commit df05697f87163a2b716d82653884e631f2fa6cf3
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Wed Sep 2 20:18:20 2020 +0300

    Change dashboard button styles

commit 16655f2d6b0d79d1fa027ec2310bb0268fffaf6a
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Wed Sep 2 20:04:28 2020 +0300

    Change button styles, rename button options

commit 1ac22e875d8b26c16830bf6edb85dadcc19ff287
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Wed Sep 2 19:30:16 2020 +0300

    Review changes

commit c590119875439d85927bdd334658e003bc1f0563
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Wed Sep 2 17:58:08 2020 +0300

    Remove default query logs form values

commit 141329563417f5337f5659d5500f4cbe16d64bd2
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Wed Sep 2 17:41:23 2020 +0300

    Update blocking buttons options logic, fix button svg size

commit 9e4f39aa6cb8e134d80d496b8a248b2fe6aceb99
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Wed Sep 2 16:30:48 2020 +0300

    Fix button position

commit 8aabff7daccb87ae02c2302e62e296b3cfc17608
Merge: 415a0334 6b614295
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Tue Sep 1 17:29:55 2020 +0300

    Merge branch 'master' into feature/2050

commit 415a0334561733d92a0f7badd68101ef554dc689
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Tue Sep 1 17:05:51 2020 +0300

    Add blocking options

commit bc6aed92b6e12f27c2604501275b53bb8159d5bc
Merge: 0de4fb3a 40b74522
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Tue Sep 1 15:49:06 2020 +0300

    Merge branch 'feature/infinite_scroll_query_logs' into feature/2050

commit 40b745225112cf8d664220ed8f484b0aa16e997c
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Tue Sep 1 15:46:27 2020 +0300

    Remove dynamic translation of toasts

commit 0de4fb3a4cd785c6b52e860e204c6e13d356b178
Merge: 1ab14471 f08fa7b8
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Tue Sep 1 15:07:30 2020 +0300

    Merge branch 'feature/infinite_scroll_query_logs' into feature/2050

... and 51 more commits
---
 client/src/__locales/en.json                  |   7 +-
 client/src/actions/index.js                   |  19 ++-
 client/src/components/App/index.css           |   9 ++
 client/src/components/Dashboard/Clients.js    |  15 +--
 client/src/components/Header/Header.css       |   4 +
 .../src/components/Logs/Cells/ClientCell.js   | 101 ++++++++++++---
 .../src/components/Logs/Cells/IconTooltip.js  |  16 ++-
 .../components/Logs/Cells/helpers/index.js    |  19 +++
 client/src/components/Logs/Cells/index.js     |  42 ++++++-
 client/src/components/Logs/Logs.css           | 115 +++++++++++++++---
 client/src/components/Logs/index.js           |   5 +-
 client/src/components/ui/Icons.js             | Bin 42118 -> 42479 bytes
 client/src/components/ui/Tooltip.js           |   7 +-
 client/src/helpers/helpers.js                 |  18 +++
 14 files changed, 319 insertions(+), 58 deletions(-)
 create mode 100644 client/src/components/Logs/Cells/helpers/index.js

diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json
index e8e984e2..6cce0d91 100644
--- a/client/src/__locales/en.json
+++ b/client/src/__locales/en.json
@@ -194,6 +194,10 @@
     "dns_test_not_ok_toast": "Server \"{{key}}\": could not be used, please check that you've written it correctly",
     "unblock": "Unblock",
     "block": "Block",
+    "disallow_this_client": "Disallow this client",
+    "allow_this_client": "Allow this client",
+    "block_for_this_client_only": "Block for this client only",
+    "unblock_for_this_client_only": "Unblock for this client only",
     "time_table_header": "Time",
     "date": "Date",
     "domain_name_table_header": "Domain name",
@@ -569,5 +573,6 @@
     "setup_config_to_enable_dhcp_server": "Setup config to enable DHCP server",
     "original_response": "Original response",
     "click_to_view_queries": "Click to view queries",
-    "port_53_faq_link": "Port 53 is often occupied by \"DNSStubListener\" or \"systemd-resolved\" services. Please read <0>this instruction</0> on how to resolve this."
+    "port_53_faq_link": "Port 53 is often occupied by \"DNSStubListener\" or \"systemd-resolved\" services. Please read <0>this instruction</0> on how to resolve this.",
+    "adg_will_drop_dns_queries": "AdGuard Home will be dropping all DNS queries from this client."
 }
diff --git a/client/src/actions/index.js b/client/src/actions/index.js
index ff512883..d4018bb0 100644
--- a/client/src/actions/index.js
+++ b/client/src/actions/index.js
@@ -545,15 +545,17 @@ export const removeStaticLease = (config) => async (dispatch) => {
 
 export const removeToast = createAction('REMOVE_TOAST');
 
-export const toggleBlocking = (type, domain) => async (dispatch, getState) => {
+export const toggleBlocking = (
+    type, domain, baseRule, baseUnblocking,
+) => async (dispatch, getState) => {
+    const baseBlockingRule = baseRule || `||${domain}^$important`;
+    const baseUnblockingRule = baseUnblocking || `@@${baseBlockingRule}`;
     const { userRules } = getState().filtering;
 
     const lineEnding = !endsWith(userRules, '\n') ? '\n' : '';
-    const baseRule = `||${domain}^$important`;
-    const baseUnblocking = `@@${baseRule}`;
 
-    const blockingRule = type === BLOCK_ACTIONS.BLOCK ? baseUnblocking : baseRule;
-    const unblockingRule = type === BLOCK_ACTIONS.BLOCK ? baseRule : baseUnblocking;
+    const blockingRule = type === BLOCK_ACTIONS.BLOCK ? baseUnblockingRule : baseBlockingRule;
+    const unblockingRule = type === BLOCK_ACTIONS.BLOCK ? baseBlockingRule : baseUnblockingRule;
     const preparedBlockingRule = new RegExp(`(^|\n)${escapeRegExp(blockingRule)}($|\n)`);
     const preparedUnblockingRule = new RegExp(`(^|\n)${escapeRegExp(unblockingRule)}($|\n)`);
 
@@ -576,3 +578,10 @@ export const toggleBlocking = (type, domain) => async (dispatch, getState) => {
 
     dispatch(getFilteringStatus());
 };
+
+export const toggleBlockingForClient = (type, domain, client) => {
+    const baseRule = `||${domain}^$client='${client.replace(/'/g, '/\'')}'`;
+    const baseUnblocking = `@@${baseRule}`;
+
+    return toggleBlocking(type, domain, baseRule, baseUnblocking);
+};
diff --git a/client/src/components/App/index.css b/client/src/components/App/index.css
index 091a6612..e2b0304d 100644
--- a/client/src/components/App/index.css
+++ b/client/src/components/App/index.css
@@ -66,3 +66,12 @@ body {
 .select--no-warning {
     margin-bottom: 1.375rem;
 }
+
+.button-action {
+    visibility: hidden;
+}
+
+.logs__row:hover .button-action,
+.button-action--active {
+    visibility: visible;
+}
diff --git a/client/src/components/Dashboard/Clients.js b/client/src/components/Dashboard/Clients.js
index 24b278f4..3c163035 100644
--- a/client/src/components/Dashboard/Clients.js
+++ b/client/src/components/Dashboard/Clients.js
@@ -51,15 +51,16 @@ const renderBlockingButton = (ip) => {
     const type = isNotFound ? BLOCK_ACTIONS.BLOCK : BLOCK_ACTIONS.UNBLOCK;
     const text = type;
 
-    const className = classNames('btn btn-sm', {
-        'btn-outline-danger': isNotFound,
-        'btn-outline-secondary': !isNotFound,
+    const buttonClass = classNames('button-action button-action--main', {
+        'button-action--unblock': !isNotFound,
     });
 
     const toggleClientStatus = (type, ip) => {
-        const confirmMessage = type === BLOCK_ACTIONS.BLOCK ? 'client_confirm_block' : 'client_confirm_unblock';
+        const confirmMessage = type === BLOCK_ACTIONS.BLOCK
+            ? `${t('adg_will_drop_dns_queries')} ${t('client_confirm_block', { ip })}`
+            : t('client_confirm_unblock', { ip });
 
-        if (window.confirm(t(confirmMessage, { ip }))) {
+        if (window.confirm(confirmMessage)) {
             dispatch(toggleClientBlock(type, ip));
         }
     };
@@ -69,7 +70,7 @@ const renderBlockingButton = (ip) => {
     return <div className="table__action pl-4">
                 <button
                         type="button"
-                        className={className}
+                        className={buttonClass}
                         onClick={onClick}
                         disabled={processingSet}
                 >
@@ -82,7 +83,7 @@ const ClientCell = (row) => {
     const { value, original: { info } } = row;
 
     return <>
-        <div className="logs__row logs__row--overflow logs__row--column d-flex">
+        <div className="logs__row logs__row--overflow logs__row--column d-flex align-items-center">
             {renderFormattedClientCell(value, info, true)}
             {renderBlockingButton(value)}
         </div>
diff --git a/client/src/components/Header/Header.css b/client/src/components/Header/Header.css
index b67840f3..a5fe802e 100644
--- a/client/src/components/Header/Header.css
+++ b/client/src/components/Header/Header.css
@@ -164,6 +164,10 @@
         color: #9aa0ac;
     }
 
+    .nav-icon--white {
+        color: #fff;
+    }
+
     .header-brand-img {
         height: 32px;
     }
diff --git a/client/src/components/Logs/Cells/ClientCell.js b/client/src/components/Logs/Cells/ClientCell.js
index 93830faa..fd9875bf 100644
--- a/client/src/components/Logs/Cells/ClientCell.js
+++ b/client/src/components/Logs/Cells/ClientCell.js
@@ -1,14 +1,16 @@
-import React from 'react';
+import React, { useState } from 'react';
 import { shallowEqual, useDispatch, useSelector } from 'react-redux';
 import { nanoid } from 'nanoid';
 import classNames from 'classnames';
 import { useTranslation } from 'react-i18next';
 import propTypes from 'prop-types';
-import { checkFiltered } from '../../../helpers/helpers';
+import { checkFiltered, getBlockingClientName } from '../../../helpers/helpers';
 import { BLOCK_ACTIONS } from '../../../helpers/constants';
-import { toggleBlocking } from '../../../actions';
+import { toggleBlocking, toggleBlockingForClient } from '../../../actions';
 import IconTooltip from './IconTooltip';
 import { renderFormattedClientCell } from '../../../helpers/renderFormattedClientCell';
+import { toggleClientBlock } from '../../../actions/access';
+import { getBlockClientInfo } from './helpers';
 
 const ClientCell = ({
     client,
@@ -22,6 +24,12 @@ const ClientCell = ({
     const autoClients = useSelector((state) => state.dashboard.autoClients, shallowEqual);
     const processingRules = useSelector((state) => state.filtering.processingRules);
     const isDetailed = useSelector((state) => state.queryLogs.isDetailed);
+    const [isOptionsOpened, setOptionsOpened] = useState(false);
+
+    const disallowed_clients = useSelector(
+        (state) => state.access.disallowed_clients,
+        shallowEqual,
+    );
 
     const autoClient = autoClients.find((autoClient) => autoClient.name === client);
     const source = autoClient?.source;
@@ -53,22 +61,81 @@ const ClientCell = ({
 
     const renderBlockingButton = (isFiltered, domain) => {
         const buttonType = isFiltered ? BLOCK_ACTIONS.UNBLOCK : BLOCK_ACTIONS.BLOCK;
+        const clients = useSelector((state) => state.dashboard.clients);
 
-        const buttonClass = classNames('btn btn-sm logs__cell--block-button', {
-            'btn-outline-secondary': isFiltered,
-            'btn-outline-danger': !isFiltered,
-        });
+        const {
+            confirmMessage,
+            buttonKey: blockingClientKey,
+            type,
+        } = getBlockClientInfo(client, disallowed_clients);
+
+        const blockingForClientKey = isFiltered ? 'unblock_for_this_client_only' : 'block_for_this_client_only';
+        const clientNameBlockingFor = getBlockingClientName(clients, client);
+
+        const BUTTON_OPTIONS_TO_ACTION_MAP = {
+            [blockingForClientKey]: () => {
+                dispatch(toggleBlockingForClient(buttonType, domain, clientNameBlockingFor));
+            },
+            [blockingClientKey]: () => {
+                const message = `${type === BLOCK_ACTIONS.BLOCK ? t('adg_will_drop_dns_queries') : ''} ${t(confirmMessage, { ip: client })}`;
+                if (window.confirm(message)) {
+                    dispatch(toggleClientBlock(type, client));
+                }
+            },
+        };
 
         const onClick = () => dispatch(toggleBlocking(buttonType, domain));
 
-        return <button
-                type="button"
-                className={buttonClass}
-                onClick={onClick}
-                disabled={processingRules}
-        >
-            {t(buttonType)}
-        </button>;
+        const getOptions = (optionToActionMap) => {
+            const options = Object.entries(optionToActionMap);
+            if (options.length === 0) {
+                return null;
+            }
+            return <>{options
+                .map(([name, onClick]) => <div
+                    key={name}
+                    className="button-action--arrow-option px-4 py-2"
+                    onClick={onClick}
+            >{t(name)}
+            </div>)}</>;
+        };
+
+        const content = getOptions(BUTTON_OPTIONS_TO_ACTION_MAP);
+
+        const buttonClass = classNames('button-action button-action--main', {
+            'button-action--unblock': isFiltered,
+            'button-action--with-options': content,
+            'button-action--active': isOptionsOpened,
+        });
+
+        const buttonArrowClass = classNames('button-action button-action--arrow', {
+            'button-action--unblock': isFiltered,
+            'button-action--active': isOptionsOpened,
+        });
+
+        const containerClass = classNames('button-action__container', {
+            'button-action__container--detailed': isDetailed,
+        });
+
+        return <div className={containerClass}>
+            <button type="button"
+                    className={buttonClass}
+                    onClick={onClick}
+                    disabled={processingRules}
+            >
+                {t(buttonType)}
+            </button>
+            {content && <button className={buttonArrowClass} disabled={processingRules}>
+                <IconTooltip
+                        className='h-100'
+                        tooltipClass='button-action--arrow-option-container'
+                        xlinkHref='chevron-down'
+                        triggerClass='button-action--icon'
+                        content={content} placement="bottom-end" trigger="click"
+                        onVisibilityChange={setOptionsOpened}
+                />
+            </button>}
+        </div>;
     };
 
     return <div className="o-hidden h-100 logs__cell logs__cell--client" role="gridcell">
@@ -81,9 +148,7 @@ const ClientCell = ({
             </div>
             {isDetailed && name && !whoisAvailable
             && <div className="detailed-info d-none d-sm-block logs__text"
-                    title={name}>
-                {name}
-            </div>}
+                    title={name}>{name}</div>}
         </div>
         {renderBlockingButton(isFiltered, domain)}
     </div>;
diff --git a/client/src/components/Logs/Cells/IconTooltip.js b/client/src/components/Logs/Cells/IconTooltip.js
index 5b9cc2cb..8bb3d624 100644
--- a/client/src/components/Logs/Cells/IconTooltip.js
+++ b/client/src/components/Logs/Cells/IconTooltip.js
@@ -6,17 +6,21 @@ import { processContent } from '../../../helpers/helpers';
 import Tooltip from '../../ui/Tooltip';
 import 'react-popper-tooltip/dist/styles.css';
 import './IconTooltip.css';
+import { SHOW_TOOLTIP_DELAY } from '../../../helpers/constants';
 
 const IconTooltip = ({
     className,
     contentItemClass,
     columnClass,
+    triggerClass,
     canShowTooltip = true,
     xlinkHref,
     title,
     placement,
     tooltipClass,
     content,
+    trigger,
+    onVisibilityChange,
     renderContent = content ? React.Children.map(
         processContent(content),
         (item, idx) => <div key={idx} className={contentItemClass}>
@@ -36,6 +40,10 @@ const IconTooltip = ({
         className={tooltipClassName}
         content={tooltipContent}
         placement={placement}
+        triggerClass={triggerClass}
+        trigger={trigger}
+        onVisibilityChange={onVisibilityChange}
+        delayShow={trigger === 'click' ? 0 : SHOW_TOOLTIP_DELAY}
     >
         {xlinkHref && <svg className={className}>
             <use xlinkHref={`#${xlinkHref}`} />
@@ -45,6 +53,8 @@ const IconTooltip = ({
 
 IconTooltip.propTypes = {
     className: PropTypes.string,
+    trigger: PropTypes.string,
+    triggerClass: PropTypes.string,
     contentItemClass: PropTypes.string,
     columnClass: PropTypes.string,
     tooltipClass: PropTypes.string,
@@ -52,11 +62,9 @@ IconTooltip.propTypes = {
     placement: PropTypes.string,
     canShowTooltip: PropTypes.bool,
     xlinkHref: PropTypes.string,
-    content: PropTypes.oneOfType([
-        PropTypes.string,
-        PropTypes.array,
-    ]),
+    content: PropTypes.node,
     renderContent: PropTypes.arrayOf(PropTypes.element),
+    onVisibilityChange: PropTypes.func,
 };
 
 export default IconTooltip;
diff --git a/client/src/components/Logs/Cells/helpers/index.js b/client/src/components/Logs/Cells/helpers/index.js
new file mode 100644
index 00000000..61e7ff5c
--- /dev/null
+++ b/client/src/components/Logs/Cells/helpers/index.js
@@ -0,0 +1,19 @@
+import { getIpMatchListStatus } from '../../../../helpers/helpers';
+import { BLOCK_ACTIONS, IP_MATCH_LIST_STATUS } from '../../../../helpers/constants';
+
+export const BUTTON_PREFIX = 'btn_';
+
+export const getBlockClientInfo = (client, disallowed_clients) => {
+    const ipMatchListStatus = getIpMatchListStatus(client, disallowed_clients);
+
+    const isNotFound = ipMatchListStatus === IP_MATCH_LIST_STATUS.NOT_FOUND;
+    const type = isNotFound ? BLOCK_ACTIONS.BLOCK : BLOCK_ACTIONS.UNBLOCK;
+
+    const confirmMessage = isNotFound ? 'client_confirm_block' : 'client_confirm_unblock';
+    const buttonKey = isNotFound ? 'disallow_this_client' : 'allow_this_client';
+    return {
+        confirmMessage,
+        buttonKey,
+        type,
+    };
+};
diff --git a/client/src/components/Logs/Cells/index.js b/client/src/components/Logs/Cells/index.js
index c2d7968b..8a0fced3 100644
--- a/client/src/components/Logs/Cells/index.js
+++ b/client/src/components/Logs/Cells/index.js
@@ -9,6 +9,7 @@ import {
     formatDateTime,
     formatElapsedMs,
     formatTime,
+    getBlockingClientName,
     getFilterName,
     processContent,
 } from '../../../helpers/helpers';
@@ -22,12 +23,14 @@ import {
     SCHEME_TO_PROTOCOL_MAP,
 } from '../../../helpers/constants';
 import { getSourceData } from '../../../helpers/trackers/trackers';
-import { toggleBlocking } from '../../../actions';
+import { toggleBlocking, toggleBlockingForClient } from '../../../actions';
 import DateCell from './DateCell';
 import DomainCell from './DomainCell';
 import ResponseCell from './ResponseCell';
 import ClientCell from './ClientCell';
 import '../Logs.css';
+import { toggleClientBlock } from '../../../actions/access';
+import { getBlockClientInfo, BUTTON_PREFIX } from './helpers';
 
 const Row = memo(({
     style,
@@ -45,6 +48,13 @@ const Row = memo(({
     const whitelistFilters = useSelector((state) => state.filtering.whitelistFilters, shallowEqual);
     const autoClients = useSelector((state) => state.dashboard.autoClients, shallowEqual);
 
+    const disallowed_clients = useSelector(
+        (state) => state.access.disallowed_clients,
+        shallowEqual,
+    );
+
+    const clients = useSelector((state) => state.dashboard.clients);
+
     const onClick = () => {
         if (!isSmallScreen) { return; }
         const {
@@ -98,6 +108,26 @@ const Row = memo(({
 
         const filter = getFilterName(filters, whitelistFilters, filterId);
 
+        const {
+            confirmMessage,
+            buttonKey: blockingClientKey,
+            type: blockType,
+        } = getBlockClientInfo(client, disallowed_clients);
+
+        const blockingForClientKey = isFiltered ? 'unblock_for_this_client_only' : 'block_for_this_client_only';
+        const clientNameBlockingFor = getBlockingClientName(clients, client);
+
+        const onBlockingForClientClick = () => {
+            dispatch(toggleBlockingForClient(buttonType, domain, clientNameBlockingFor));
+        };
+
+        const onBlockingClientClick = () => {
+            const message = `${blockType === BLOCK_ACTIONS.BLOCK ? t('adg_will_drop_dns_queries') : ''} ${t(confirmMessage, { ip: client })}`;
+            if (window.confirm(message)) {
+                dispatch(toggleClientBlock(blockType, client));
+            }
+        };
+
         const detailedData = {
             time_table_header: formatTime(time, LONG_TIME_FORMAT),
             date: formatDateTime(time, DEFAULT_SHORT_DATE_FORMAT_OPTIONS),
@@ -132,10 +162,12 @@ const Row = memo(({
             source_label: source,
             validated_with_dnssec: dnssec_enabled ? Boolean(answer_dnssec) : false,
             original_response: originalResponse?.join('\n'),
-            [buttonType]: <div onClick={onToggleBlock}
-                                   className={classNames('title--border text-center', {
-                                       'bg--danger': isBlocked,
-                                   })}>{t(buttonType)}</div>,
+            [BUTTON_PREFIX + buttonType]: <div onClick={onToggleBlock}
+                                               className={classNames('title--border text-center', {
+                                                   'bg--danger': isBlocked,
+                                               })}>{t(buttonType)}</div>,
+            [BUTTON_PREFIX + blockingForClientKey]: <div onClick={onBlockingForClientClick} className='text-center font-weight-bold py-2'>{t(blockingForClientKey)}</div>,
+            [BUTTON_PREFIX + blockingClientKey]: <div onClick={onBlockingClientClick} className='text-center font-weight-bold py-2'>{t(blockingClientKey)}</div>,
         };
 
         setDetailedDataCurrent(processContent(detailedData));
diff --git a/client/src/components/Logs/Logs.css b/client/src/components/Logs/Logs.css
index 857fd466..fd088b79 100644
--- a/client/src/components/Logs/Logs.css
+++ b/client/src/components/Logs/Logs.css
@@ -10,9 +10,20 @@
     --size-client: 123;
     --gray-216: rgba(216, 216, 216, 0.23);
     --gray-4d: #4D4D4D;
+    --gray-f3: #F3F3F3;
     --gray-8: #888;
     --danger: #DF3812;
     --white80: rgba(255, 255, 255, 0.8);
+
+    --btn-block: #C23814;
+    --btn-block-disabled: #E3B3A6;
+    --btn-block-active: #A62200;
+
+    --btn-unblock: #888888;
+    --btn-unblock-disabled: #D8D8D8;
+    --btn-unblock-active: #4D4D4D;
+
+    --option-border-radius: 4px;
 }
 
 .logs__text {
@@ -191,6 +202,7 @@
     width: 7.6875rem;
     flex: var(--size-client) 0 auto;
     padding-right: 0;
+    position: relative;
 }
 
 .logs__cell--header__container > .logs__cell--header__item {
@@ -202,12 +214,95 @@
     padding-right: 0;
 }
 
-.logs__cell--block-button {
-    max-height: 1.75rem;
-    position: relative;
-    left: 10%;
-    top: 40%;
-    visibility: hidden;
+.button-action__container {
+    display: flex;
+    position: absolute;
+    right: 0;
+    bottom: 0.5rem;
+    height: 1.6rem;
+}
+
+.button-action__container--detailed {
+    bottom: 1.3rem;
+}
+
+.button-action {
+    outline: 0 !important;
+    background: var(--btn-block);
+    border-radius: var(--option-border-radius);
+    font-size: 0.8rem;
+    color: var(--white);
+    letter-spacing: 0;
+    text-align: center;
+    line-height: 28px;
+    border: 0;
+}
+
+.button-action--unblock {
+    background: var(--btn-unblock);
+}
+
+.button-action--main {
+    padding: 0 1rem;
+    display: flex;
+    align-items: center;
+}
+
+.button-action--with-options {
+    border-top-right-radius: 0;
+    border-bottom-right-radius: 0;
+}
+
+.button-action--arrow {
+    border-top-left-radius: 0;
+    border-bottom-left-radius: 0;
+    border-left: 1px solid var(--white);
+    width: 1.5625rem;
+    padding: 0;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
+
+.button-action:hover {
+    cursor: pointer;
+}
+
+.button-action--arrow .button-action--icon {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+}
+
+.button-action:active {
+    background: var(--btn-block-active);
+}
+
+.button-action--unblock:active {
+    background: var(--btn-unblock-active);
+}
+
+.button-action:disabled {
+    background: var(--btn-block-disabled);
+    cursor: default;
+}
+
+.button-action--unblock:disabled {
+    background: var(--btn-unblock-disabled);
+}
+
+.button-action--arrow-option:hover {
+    cursor: pointer;
+    background: var(--gray-f3);
+    overflow: hidden;
+}
+
+.button-action--arrow-option-container {
+    overflow: visible;
+    transform-origin: left;
+    padding: 1rem 0;
 }
 
 .logs__row {
@@ -222,14 +317,6 @@
     border-bottom: 2px solid var(--gray-216);
 }
 
-.logs__table .logs__row:hover .logs__cell--block-button {
-    visibility: visible;
-}
-
-.logs__table .logs__row .logs__cell--block-button:disabled {
-    background-color: var(--white) !important;
-}
-
 /* QUERY_STATUS_COLORS */
 .logs__row--blue {
     background-color: var(--blue);
diff --git a/client/src/components/Logs/index.js b/client/src/components/Logs/index.js
index 13fa697c..bcc9a94d 100644
--- a/client/src/components/Logs/index.js
+++ b/client/src/components/Logs/index.js
@@ -23,15 +23,16 @@ import {
 } from '../../actions/queryLogs';
 import InfiniteTable from './InfiniteTable';
 import './Logs.css';
+import { BUTTON_PREFIX } from './Cells/helpers';
 
-const processContent = (data, buttonType) => Object.entries(data)
+const processContent = (data) => Object.entries(data)
     .map(([key, value]) => {
         if (!value) {
             return null;
         }
 
         const isTitle = value === 'title';
-        const isButton = key === buttonType;
+        const isButton = key.startsWith(BUTTON_PREFIX);
         const isBoolean = typeof value === 'boolean';
         const isHidden = isBoolean && value === false;
 
diff --git a/client/src/components/ui/Icons.js b/client/src/components/ui/Icons.js
index f29bcc21aaf23ebc611b2329d1785e21dd900b3b..3851ff014c13df989d2c8e8f44bd8a64b4946674 100644
GIT binary patch
delta 169
zcmZoW$@Km-(}u()lMU2Gd6F|y%Zl>zbW`%n^CmA;78S7LQa}PW>60V6j3ysg$f;|f
zV31*CQf6f0VNj){kd~Q~W2>ZWVGe>2u75#da%M@Tt&*OB;p7WlQj;f66`R};$v$~)
z7tiFwwrZ0LdRZrbh-8!3GcYz&Ff`D!G|n*w5(*}I2Ie5b%t9B$pWNQ7F*$LGDgYIv
BH2eSn

delta 13
VcmaEVnyKw1(}u()lTR&G1pqRN2I>F+

diff --git a/client/src/components/ui/Tooltip.js b/client/src/components/ui/Tooltip.js
index 9f34b3fe..87b353de 100644
--- a/client/src/components/ui/Tooltip.js
+++ b/client/src/components/ui/Tooltip.js
@@ -20,6 +20,7 @@ const Tooltip = ({
     trigger = 'hover',
     delayShow = SHOW_TOOLTIP_DELAY,
     delayHide = HIDE_TOOLTIP_DELAY,
+    onVisibilityChange,
 }) => {
     const { t } = useTranslation();
     const touchEventsAvailable = 'ontouchstart' in window;
@@ -73,6 +74,7 @@ const Tooltip = ({
             delayHide={delayHideValue}
             delayShow={delayShowValue}
             tooltip={renderTooltip}
+            onVisibilityChange={onVisibilityChange}
         >
             {renderTrigger}
         </TooltipTrigger>
@@ -90,10 +92,11 @@ Tooltip.propTypes = {
     ).isRequired,
     placement: propTypes.string,
     trigger: propTypes.string,
-    delayHide: propTypes.string,
-    delayShow: propTypes.string,
+    delayHide: propTypes.number,
+    delayShow: propTypes.number,
     className: propTypes.string,
     triggerClass: propTypes.string,
+    onVisibilityChange: propTypes.func,
 };
 
 export default Tooltip;
diff --git a/client/src/helpers/helpers.js b/client/src/helpers/helpers.js
index fa3a5046..bafd230e 100644
--- a/client/src/helpers/helpers.js
+++ b/client/src/helpers/helpers.js
@@ -836,3 +836,21 @@ export const isScrolledIntoView = (el) => {
 
     return elemTop < window.innerHeight && elemBottom >= 0;
 };
+
+/**
+ * If this is a manually created client, return its name.
+ * If this is a "runtime" client, return it's IP address.
+ * @param clients {Array.<object>}
+ * @param ip {string}
+ * @returns {string}
+ */
+export const getBlockingClientName = (clients, ip) => {
+    for (let i = 0; i < clients.length; i += 1) {
+        const client = clients[i];
+
+        if (client.ids.includes(ip)) {
+            return client.name;
+        }
+    }
+    return ip;
+};

From 15a82233f3dc8b5307e3edef2214f98ab408de64 Mon Sep 17 00:00:00 2001
From: ArtemBaskal <a.baskal@adguard.com>
Date: Mon, 7 Sep 2020 10:36:17 +0300
Subject: [PATCH 07/19] - client: Fix whois cell styles

---
 client/src/components/Logs/Logs.css           | 25 +++++++++++++++++++
 .../components/Settings/Clients/whoisCell.js  |  2 +-
 .../src/helpers/renderFormattedClientCell.js  |  2 +-
 3 files changed, 27 insertions(+), 2 deletions(-)

diff --git a/client/src/components/Logs/Logs.css b/client/src/components/Logs/Logs.css
index fd088b79..b230ab8b 100644
--- a/client/src/components/Logs/Logs.css
+++ b/client/src/components/Logs/Logs.css
@@ -388,3 +388,28 @@
 .logs__table .loading:before {
     min-height: 100%;
 }
+
+.logs__whois {
+    display: inline;
+    font-size: 12px;
+    white-space: nowrap;
+}
+
+.logs__whois::after {
+    content: "|";
+    padding: 0 5px;
+    opacity: 0.3;
+}
+
+.logs__whois:last-child::after {
+    content: "";
+}
+
+.logs__whois-icon.icons {
+    position: relative;
+    top: -2px;
+    width: 12px;
+    height: 12px;
+    margin-right: 1px;
+    opacity: 0.5;
+}
diff --git a/client/src/components/Settings/Clients/whoisCell.js b/client/src/components/Settings/Clients/whoisCell.js
index 94afd6cc..1a8b0484 100644
--- a/client/src/components/Settings/Clients/whoisCell.js
+++ b/client/src/components/Settings/Clients/whoisCell.js
@@ -14,7 +14,7 @@ const getFormattedWhois = (value, t) => {
                 <div key={key} title={t(key)}>
                     {icon && (
                         <Fragment>
-                            <svg className="logs__whois-icon text-muted-dark icons">
+                            <svg className="logs__whois-icon text-muted-dark icons icon--24">
                                 <use xlinkHref={`#${icon}`} />
                             </svg>
                             &nbsp;
diff --git a/client/src/helpers/renderFormattedClientCell.js b/client/src/helpers/renderFormattedClientCell.js
index fa705496..d677c4ca 100644
--- a/client/src/helpers/renderFormattedClientCell.js
+++ b/client/src/helpers/renderFormattedClientCell.js
@@ -9,7 +9,7 @@ const getFormattedWhois = (whois) => {
             .map((key) => {
                 const icon = WHOIS_ICONS[key];
                 return (
-                    <span className="logs__whois text-muted " key={key} title={whoisInfo[key]}>
+                    <span className="logs__whois text-muted" key={key} title={whoisInfo[key]}>
                     {icon && (
                         <>
                             <svg className="logs__whois-icon icons icon--18">

From 03506df25d5c6af6daf1ba19300d2697ad241bda Mon Sep 17 00:00:00 2001
From: David Sheets <sheets@alum.mit.edu>
Date: Mon, 7 Sep 2020 09:10:56 +0100
Subject: [PATCH 08/19] cli: factor options struct and parsing into
 home/options.go

---
 home/home.go         | 109 +++---------------
 home/options.go      | 255 +++++++++++++++++++++++++++++++++++++++++++
 home/options_test.go | 171 +++++++++++++++++++++++++++++
 3 files changed, 439 insertions(+), 96 deletions(-)
 create mode 100644 home/options.go
 create mode 100644 home/options_test.go

diff --git a/home/home.go b/home/home.go
index b9d7a760..806c59e2 100644
--- a/home/home.go
+++ b/home/home.go
@@ -526,108 +526,25 @@ func cleanupAlways() {
 	log.Info("Stopped")
 }
 
-// command-line arguments
-type options struct {
-	verbose        bool   // is verbose logging enabled
-	configFilename string // path to the config file
-	workDir        string // path to the working directory where we will store the filters data and the querylog
-	bindHost       string // host address to bind HTTP server on
-	bindPort       int    // port to serve HTTP pages on
-	logFile        string // Path to the log file. If empty, write to stdout. If "syslog", writes to syslog
-	pidFile        string // File name to save PID to
-	checkConfig    bool   // Check configuration and exit
-	disableUpdate  bool   // If set, don't check for updates
-
-	// service control action (see service.ControlAction array + "status" command)
-	serviceControlAction string
-
-	// runningAsService flag is set to true when options are passed from the service runner
-	runningAsService bool
-
-	glinetMode bool // Activate GL-Inet mode
+func exitWithError() {
+	os.Exit(64)
 }
 
 // loadOptions reads command line arguments and initializes configuration
 func loadOptions() options {
-	o := options{}
+	o, f, err := parse(os.Args[0], os.Args[1:])
 
-	var printHelp func()
-	var opts = []struct {
-		longName          string
-		shortName         string
-		description       string
-		callbackWithValue func(value string)
-		callbackNoValue   func()
-	}{
-		{"config", "c", "Path to the config file", func(value string) { o.configFilename = value }, nil},
-		{"work-dir", "w", "Path to the working directory", func(value string) { o.workDir = value }, nil},
-		{"host", "h", "Host address to bind HTTP server on", func(value string) { o.bindHost = value }, nil},
-		{"port", "p", "Port to serve HTTP pages on", func(value string) {
-			v, err := strconv.Atoi(value)
-			if err != nil {
-				panic("Got port that is not a number")
-			}
-			o.bindPort = v
-		}, nil},
-		{"service", "s", "Service control action: status, install, uninstall, start, stop, restart, reload (configuration)", func(value string) {
-			o.serviceControlAction = value
-		}, nil},
-		{"logfile", "l", "Path to log file. If empty: write to stdout; if 'syslog': write to system log", func(value string) {
-			o.logFile = value
-		}, nil},
-		{"pidfile", "", "Path to a file where PID is stored", func(value string) { o.pidFile = value }, nil},
-		{"check-config", "", "Check configuration and exit", nil, func() { o.checkConfig = true }},
-		{"no-check-update", "", "Don't check for updates", nil, func() { o.disableUpdate = true }},
-		{"verbose", "v", "Enable verbose output", nil, func() { o.verbose = true }},
-		{"glinet", "", "Run in GL-Inet compatibility mode", nil, func() { o.glinetMode = true }},
-		{"version", "", "Show the version and exit", nil, func() {
-			fmt.Println(version())
+	if err != nil {
+		log.Error(err.Error())
+		_ = printHelp(os.Args[0])
+		exitWithError()
+	} else if f != nil {
+		err = f()
+		if err != nil {
+			log.Error(err.Error())
+			exitWithError()
+		} else {
 			os.Exit(0)
-		}},
-		{"help", "", "Print this help", nil, func() {
-			printHelp()
-			os.Exit(64)
-		}},
-	}
-	printHelp = func() {
-		fmt.Printf("Usage:\n\n")
-		fmt.Printf("%s [options]\n\n", os.Args[0])
-		fmt.Printf("Options:\n")
-		for _, opt := range opts {
-			val := ""
-			if opt.callbackWithValue != nil {
-				val = " VALUE"
-			}
-			if opt.shortName != "" {
-				fmt.Printf("  -%s, %-30s %s\n", opt.shortName, "--"+opt.longName+val, opt.description)
-			} else {
-				fmt.Printf("  %-34s %s\n", "--"+opt.longName+val, opt.description)
-			}
-		}
-	}
-	for i := 1; i < len(os.Args); i++ {
-		v := os.Args[i]
-		knownParam := false
-		for _, opt := range opts {
-			if v == "--"+opt.longName || (opt.shortName != "" && v == "-"+opt.shortName) {
-				if opt.callbackWithValue != nil {
-					if i+1 >= len(os.Args) {
-						log.Error("Got %s without argument\n", v)
-						os.Exit(64)
-					}
-					i++
-					opt.callbackWithValue(os.Args[i])
-				} else if opt.callbackNoValue != nil {
-					opt.callbackNoValue()
-				}
-				knownParam = true
-				break
-			}
-		}
-		if !knownParam {
-			log.Error("unknown option %v\n", v)
-			printHelp()
-			os.Exit(64)
 		}
 	}
 
diff --git a/home/options.go b/home/options.go
new file mode 100644
index 00000000..ec789d4f
--- /dev/null
+++ b/home/options.go
@@ -0,0 +1,255 @@
+package home
+
+import (
+	"fmt"
+	"os"
+	"strconv"
+)
+
+// options passed from command-line arguments
+type options struct {
+	verbose        bool   // is verbose logging enabled
+	configFilename string // path to the config file
+	workDir        string // path to the working directory where we will store the filters data and the querylog
+	bindHost       string // host address to bind HTTP server on
+	bindPort       int    // port to serve HTTP pages on
+	logFile        string // Path to the log file. If empty, write to stdout. If "syslog", writes to syslog
+	pidFile        string // File name to save PID to
+	checkConfig    bool   // Check configuration and exit
+	disableUpdate  bool   // If set, don't check for updates
+
+	// service control action (see service.ControlAction array + "status" command)
+	serviceControlAction string
+
+	// runningAsService flag is set to true when options are passed from the service runner
+	runningAsService bool
+
+	glinetMode bool // Activate GL-Inet mode
+}
+
+// functions used for their side-effects
+type effect func() error
+
+type arg struct {
+	description string // a short, English description of the argument
+	longName    string // the name of the argument used after '--'
+	shortName   string // the name of the argument used after '-'
+
+	// only one of updateWithValue, updateNoValue, and effect should be present
+
+	updateWithValue func(o options, v string) (options, error)         // the mutator for arguments with parameters
+	updateNoValue   func(o options) (options, error)                   // the mutator for arguments without parameters
+	effect          func(o options, exec string) (f effect, err error) // the side-effect closure generator
+}
+
+var args []arg
+
+var configArg = arg{
+	"Path to the config file",
+	"config", "c",
+	func(o options, v string) (options, error) { o.configFilename = v; return o, nil },
+	nil,
+	nil,
+}
+
+var workDirArg = arg{
+	"Path to the working directory",
+	"work-dir", "w",
+	func(o options, v string) (options, error) { o.workDir = v; return o, nil }, nil, nil,
+}
+
+var hostArg = arg{
+	"Host address to bind HTTP server on",
+	"host", "h",
+	func(o options, v string) (options, error) { o.bindHost = v; return o, nil }, nil, nil,
+}
+
+var portArg = arg{
+	"Port to serve HTTP pages on",
+	"port", "p",
+	func(o options, v string) (options, error) {
+		var err error
+		var p int
+		minPort, maxPort := 0, 1<<16-1
+		if p, err = strconv.Atoi(v); err != nil {
+			err = fmt.Errorf("port '%s' is not a number", v)
+		} else if p < minPort || p > maxPort {
+			err = fmt.Errorf("port %d not in range %d - %d", p, minPort, maxPort)
+		} else {
+			o.bindPort = p
+		}
+		return o, err
+	}, nil, nil,
+}
+
+var serviceArg = arg{
+	"Service control action: status, install, uninstall, start, stop, restart, reload (configuration)",
+	"service", "s",
+	func(o options, v string) (options, error) {
+		o.serviceControlAction = v
+		return o, nil
+	}, nil, nil,
+}
+
+var logfileArg = arg{
+	"Path to log file. If empty: write to stdout; if 'syslog': write to system log",
+	"logfile", "l",
+	func(o options, v string) (options, error) { o.logFile = v; return o, nil }, nil, nil,
+}
+
+var pidfileArg = arg{
+	"Path to a file where PID is stored",
+	"pidfile", "",
+	func(o options, v string) (options, error) { o.pidFile = v; return o, nil }, nil, nil,
+}
+
+var checkConfigArg = arg{
+	"Check configuration and exit",
+	"check-config", "",
+	nil, func(o options) (options, error) { o.checkConfig = true; return o, nil }, nil,
+}
+
+var noCheckUpdateArg = arg{
+	"Don't check for updates",
+	"no-check-update", "",
+	nil, func(o options) (options, error) { o.disableUpdate = true; return o, nil }, nil,
+}
+
+var verboseArg = arg{
+	"Enable verbose output",
+	"verbose", "v",
+	nil, func(o options) (options, error) { o.verbose = true; return o, nil }, nil,
+}
+
+var glinetArg = arg{
+	"Run in GL-Inet compatibility mode",
+	"glinet", "",
+	nil, func(o options) (options, error) { o.glinetMode = true; return o, nil }, nil,
+}
+
+var versionArg = arg{
+	"Show the version and exit",
+	"version", "",
+	nil, nil, func(o options, exec string) (effect, error) {
+		return func() error { fmt.Println(version()); os.Exit(0); return nil }, nil
+	},
+}
+
+var helpArg = arg{
+	"Print this help",
+	"help", "",
+	nil, nil, func(o options, exec string) (effect, error) {
+		return func() error { _ = printHelp(exec); os.Exit(64); return nil }, nil
+	},
+}
+
+func init() {
+	args = []arg{
+		configArg,
+		workDirArg,
+		hostArg,
+		portArg,
+		serviceArg,
+		logfileArg,
+		pidfileArg,
+		checkConfigArg,
+		noCheckUpdateArg,
+		verboseArg,
+		glinetArg,
+		versionArg,
+		helpArg,
+	}
+}
+
+func getUsageLines(exec string, args []arg) []string {
+	usage := []string{
+		"Usage:",
+		"",
+		fmt.Sprintf("%s [options]", exec),
+		"",
+		"Options:",
+	}
+	for _, arg := range args {
+		val := ""
+		if arg.updateWithValue != nil {
+			val = " VALUE"
+		}
+		if arg.shortName != "" {
+			usage = append(usage, fmt.Sprintf("  -%s, %-30s %s",
+				arg.shortName,
+				"--"+arg.longName+val,
+				arg.description))
+		} else {
+			usage = append(usage, fmt.Sprintf("  %-34s %s",
+				"--"+arg.longName+val,
+				arg.description))
+		}
+	}
+	return usage
+}
+
+func printHelp(exec string) error {
+	for _, line := range getUsageLines(exec, args) {
+		_, err := fmt.Println(line)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func argMatches(a arg, v string) bool {
+	return v == "--"+a.longName || (a.shortName != "" && v == "-"+a.shortName)
+}
+
+func parse(exec string, ss []string) (o options, f effect, err error) {
+	for i := 0; i < len(ss); i++ {
+		v := ss[i]
+		knownParam := false
+		for _, arg := range args {
+			if argMatches(arg, v) {
+				if arg.updateWithValue != nil {
+					if i+1 >= len(ss) {
+						return o, f, fmt.Errorf("got %s without argument", v)
+					}
+					i++
+					o, err = arg.updateWithValue(o, ss[i])
+					if err != nil {
+						return
+					}
+				} else if arg.updateNoValue != nil {
+					o, err = arg.updateNoValue(o)
+					if err != nil {
+						return
+					}
+				} else if arg.effect != nil {
+					var eff effect
+					eff, err = arg.effect(o, exec)
+					if err != nil {
+						return
+					}
+					if eff != nil {
+						prevf := f
+						f = func() error {
+							var err error
+							if prevf != nil {
+								err = prevf()
+							}
+							if err == nil {
+								err = eff()
+							}
+							return err
+						}
+					}
+				}
+				knownParam = true
+				break
+			}
+		}
+		if !knownParam {
+			return o, f, fmt.Errorf("unknown option %v", v)
+		}
+	}
+
+	return
+}
diff --git a/home/options_test.go b/home/options_test.go
new file mode 100644
index 00000000..750f3ce2
--- /dev/null
+++ b/home/options_test.go
@@ -0,0 +1,171 @@
+package home
+
+import (
+	"fmt"
+	"testing"
+)
+
+func testParseOk(t *testing.T, ss ...string) options {
+	o, _, err := parse("", ss)
+	if err != nil {
+		t.Fatal(err.Error())
+	}
+	return o
+}
+
+func testParseErr(t *testing.T, descr string, ss ...string) {
+	_, _, err := parse("", ss)
+	if err == nil {
+		t.Fatalf("expected an error because %s but no error returned", descr)
+	}
+}
+
+func testParseParamMissing(t *testing.T, param string) {
+	testParseErr(t, fmt.Sprintf("%s parameter missing", param), param)
+}
+
+func TestParseVerbose(t *testing.T) {
+	if testParseOk(t).verbose {
+		t.Fatal("empty is not verbose")
+	}
+	if !testParseOk(t, "-v").verbose {
+		t.Fatal("-v is verbose")
+	}
+	if !testParseOk(t, "--verbose").verbose {
+		t.Fatal("--verbose is verbose")
+	}
+}
+
+func TestParseConfigFilename(t *testing.T) {
+	if testParseOk(t).configFilename != "" {
+		t.Fatal("empty is no config filename")
+	}
+	if testParseOk(t, "-c", "path").configFilename != "path" {
+		t.Fatal("-c is config filename")
+	}
+	testParseParamMissing(t, "-c")
+	if testParseOk(t, "--config", "path").configFilename != "path" {
+		t.Fatal("--configFilename is config filename")
+	}
+	testParseParamMissing(t, "--config")
+}
+
+func TestParseWorkDir(t *testing.T) {
+	if testParseOk(t).workDir != "" {
+		t.Fatal("empty is no work dir")
+	}
+	if testParseOk(t, "-w", "path").workDir != "path" {
+		t.Fatal("-w is work dir")
+	}
+	testParseParamMissing(t, "-w")
+	if testParseOk(t, "--work-dir", "path").workDir != "path" {
+		t.Fatal("--work-dir is work dir")
+	}
+	testParseParamMissing(t, "--work-dir")
+}
+
+func TestParseBindHost(t *testing.T) {
+	if testParseOk(t).bindHost != "" {
+		t.Fatal("empty is no host")
+	}
+	if testParseOk(t, "-h", "addr").bindHost != "addr" {
+		t.Fatal("-h is host")
+	}
+	testParseParamMissing(t, "-h")
+	if testParseOk(t, "--host", "addr").bindHost != "addr" {
+		t.Fatal("--host is host")
+	}
+	testParseParamMissing(t, "--host")
+}
+
+func TestParseBindPort(t *testing.T) {
+	if testParseOk(t).bindPort != 0 {
+		t.Fatal("empty is port 0")
+	}
+	if testParseOk(t, "-p", "65535").bindPort != 65535 {
+		t.Fatal("-p is port")
+	}
+	testParseParamMissing(t, "-p")
+	if testParseOk(t, "--port", "65535").bindPort != 65535 {
+		t.Fatal("--port is port")
+	}
+	testParseParamMissing(t, "--port")
+}
+
+func TestParseBindPortBad(t *testing.T) {
+	testParseErr(t, "not an int", "-p", "x")
+	testParseErr(t, "hex not supported", "-p", "0x100")
+	testParseErr(t, "port negative", "-p", "-1")
+	testParseErr(t, "port too high", "-p", "65536")
+	testParseErr(t, "port too high", "-p", "4294967297")           // 2^32 + 1
+	testParseErr(t, "port too high", "-p", "18446744073709551617") // 2^64 + 1
+}
+
+func TestParseLogfile(t *testing.T) {
+	if testParseOk(t).logFile != "" {
+		t.Fatal("empty is no log file")
+	}
+	if testParseOk(t, "-l", "path").logFile != "path" {
+		t.Fatal("-l is log file")
+	}
+	if testParseOk(t, "--logfile", "path").logFile != "path" {
+		t.Fatal("--logfile is log file")
+	}
+}
+
+func TestParsePidfile(t *testing.T) {
+	if testParseOk(t).pidFile != "" {
+		t.Fatal("empty is no pid file")
+	}
+	if testParseOk(t, "--pidfile", "path").pidFile != "path" {
+		t.Fatal("--pidfile is pid file")
+	}
+}
+
+func TestParseCheckConfig(t *testing.T) {
+	if testParseOk(t).checkConfig {
+		t.Fatal("empty is not check config")
+	}
+	if !testParseOk(t, "--check-config").checkConfig {
+		t.Fatal("--check-config is check config")
+	}
+}
+
+func TestParseDisableUpdate(t *testing.T) {
+	if testParseOk(t).disableUpdate {
+		t.Fatal("empty is not disable update")
+	}
+	if !testParseOk(t, "--no-check-update").disableUpdate {
+		t.Fatal("--no-check-update is disable update")
+	}
+}
+
+func TestParseService(t *testing.T) {
+	if testParseOk(t).serviceControlAction != "" {
+		t.Fatal("empty is no service command")
+	}
+	if testParseOk(t, "-s", "command").serviceControlAction != "command" {
+		t.Fatal("-s is service command")
+	}
+	if testParseOk(t, "--service", "command").serviceControlAction != "command" {
+		t.Fatal("--service is service command")
+	}
+}
+
+func TestParseGLInet(t *testing.T) {
+	if testParseOk(t).glinetMode {
+		t.Fatal("empty is not GL-Inet mode")
+	}
+	if !testParseOk(t, "--glinet").glinetMode {
+		t.Fatal("--glinet is GL-Inet mode")
+	}
+}
+
+func TestParseUnknown(t *testing.T) {
+	testParseErr(t, "unknown word", "x")
+	testParseErr(t, "unknown short", "-x")
+	testParseErr(t, "unknown long", "--x")
+	testParseErr(t, "unknown triple", "---x")
+	testParseErr(t, "unknown plus", "+x")
+	testParseErr(t, "unknown dash", "-")
+}

From d3428ca46c9d68b4d9d6d1967fbe128b7799dac7 Mon Sep 17 00:00:00 2001
From: David Sheets <sheets@alum.mit.edu>
Date: Mon, 7 Sep 2020 09:53:16 +0100
Subject: [PATCH 09/19] home/options: add options -> args serialization

---
 home/options.go      | 58 ++++++++++++++++++++++++++++++++++++++
 home/options_test.go | 66 ++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 124 insertions(+)

diff --git a/home/options.go b/home/options.go
index ec789d4f..90d356e8 100644
--- a/home/options.go
+++ b/home/options.go
@@ -40,6 +40,33 @@ type arg struct {
 	updateWithValue func(o options, v string) (options, error)         // the mutator for arguments with parameters
 	updateNoValue   func(o options) (options, error)                   // the mutator for arguments without parameters
 	effect          func(o options, exec string) (f effect, err error) // the side-effect closure generator
+
+	serialize func(o options) []string // the re-serialization function back to arguments (return nil for omit)
+}
+
+// {type}SliceOrNil functions check their parameter of type {type}
+// against its zero value and return nil if the parameter value is
+// zero otherwise they return a string slice of the parameter
+
+func stringSliceOrNil(s string) []string {
+	if s == "" {
+		return nil
+	}
+	return []string{s}
+}
+
+func intSliceOrNil(i int) []string {
+	if i == 0 {
+		return nil
+	}
+	return []string{strconv.Itoa(i)}
+}
+
+func boolSliceOrNil(b bool) []string {
+	if b {
+		return []string{}
+	}
+	return nil
 }
 
 var args []arg
@@ -50,18 +77,21 @@ var configArg = arg{
 	func(o options, v string) (options, error) { o.configFilename = v; return o, nil },
 	nil,
 	nil,
+	func(o options) []string { return stringSliceOrNil(o.configFilename) },
 }
 
 var workDirArg = arg{
 	"Path to the working directory",
 	"work-dir", "w",
 	func(o options, v string) (options, error) { o.workDir = v; return o, nil }, nil, nil,
+	func(o options) []string { return stringSliceOrNil(o.workDir) },
 }
 
 var hostArg = arg{
 	"Host address to bind HTTP server on",
 	"host", "h",
 	func(o options, v string) (options, error) { o.bindHost = v; return o, nil }, nil, nil,
+	func(o options) []string { return stringSliceOrNil(o.bindHost) },
 }
 
 var portArg = arg{
@@ -80,6 +110,7 @@ var portArg = arg{
 		}
 		return o, err
 	}, nil, nil,
+	func(o options) []string { return intSliceOrNil(o.bindPort) },
 }
 
 var serviceArg = arg{
@@ -89,42 +120,49 @@ var serviceArg = arg{
 		o.serviceControlAction = v
 		return o, nil
 	}, nil, nil,
+	func(o options) []string { return stringSliceOrNil(o.serviceControlAction) },
 }
 
 var logfileArg = arg{
 	"Path to log file. If empty: write to stdout; if 'syslog': write to system log",
 	"logfile", "l",
 	func(o options, v string) (options, error) { o.logFile = v; return o, nil }, nil, nil,
+	func(o options) []string { return stringSliceOrNil(o.logFile) },
 }
 
 var pidfileArg = arg{
 	"Path to a file where PID is stored",
 	"pidfile", "",
 	func(o options, v string) (options, error) { o.pidFile = v; return o, nil }, nil, nil,
+	func(o options) []string { return stringSliceOrNil(o.pidFile) },
 }
 
 var checkConfigArg = arg{
 	"Check configuration and exit",
 	"check-config", "",
 	nil, func(o options) (options, error) { o.checkConfig = true; return o, nil }, nil,
+	func(o options) []string { return boolSliceOrNil(o.checkConfig) },
 }
 
 var noCheckUpdateArg = arg{
 	"Don't check for updates",
 	"no-check-update", "",
 	nil, func(o options) (options, error) { o.disableUpdate = true; return o, nil }, nil,
+	func(o options) []string { return boolSliceOrNil(o.disableUpdate) },
 }
 
 var verboseArg = arg{
 	"Enable verbose output",
 	"verbose", "v",
 	nil, func(o options) (options, error) { o.verbose = true; return o, nil }, nil,
+	func(o options) []string { return boolSliceOrNil(o.verbose) },
 }
 
 var glinetArg = arg{
 	"Run in GL-Inet compatibility mode",
 	"glinet", "",
 	nil, func(o options) (options, error) { o.glinetMode = true; return o, nil }, nil,
+	func(o options) []string { return boolSliceOrNil(o.glinetMode) },
 }
 
 var versionArg = arg{
@@ -133,6 +171,7 @@ var versionArg = arg{
 	nil, nil, func(o options, exec string) (effect, error) {
 		return func() error { fmt.Println(version()); os.Exit(0); return nil }, nil
 	},
+	func(o options) []string { return nil },
 }
 
 var helpArg = arg{
@@ -141,6 +180,7 @@ var helpArg = arg{
 	nil, nil, func(o options, exec string) (effect, error) {
 		return func() error { _ = printHelp(exec); os.Exit(64); return nil }, nil
 	},
+	func(o options) []string { return nil },
 }
 
 func init() {
@@ -253,3 +293,21 @@ func parse(exec string, ss []string) (o options, f effect, err error) {
 
 	return
 }
+
+func shortestFlag(a arg) string {
+	if a.shortName != "" {
+		return "-" + a.shortName
+	}
+	return "--" + a.longName
+}
+
+func serialize(o options) []string {
+	ss := []string{}
+	for _, arg := range args {
+		s := arg.serialize(o)
+		if s != nil {
+			ss = append(ss, append([]string{shortestFlag(arg)}, s...)...)
+		}
+	}
+	return ss
+}
diff --git a/home/options_test.go b/home/options_test.go
index 750f3ce2..5a2b1155 100644
--- a/home/options_test.go
+++ b/home/options_test.go
@@ -169,3 +169,69 @@ func TestParseUnknown(t *testing.T) {
 	testParseErr(t, "unknown plus", "+x")
 	testParseErr(t, "unknown dash", "-")
 }
+
+func testSerialize(t *testing.T, o options, ss ...string) {
+	result := serialize(o)
+	if len(result) != len(ss) {
+		t.Fatalf("expected %s but got %s", ss, result)
+	}
+	for i, r := range result {
+		if r != ss[i] {
+			t.Fatalf("expected %s but got %s", ss, result)
+		}
+	}
+}
+
+func TestSerializeEmpty(t *testing.T) {
+	testSerialize(t, options{})
+}
+
+func TestSerializeConfigFilename(t *testing.T) {
+	testSerialize(t, options{configFilename: "path"}, "-c", "path")
+}
+
+func TestSerializeWorkDir(t *testing.T) {
+	testSerialize(t, options{workDir: "path"}, "-w", "path")
+}
+
+func TestSerializeBindHost(t *testing.T) {
+	testSerialize(t, options{bindHost: "addr"}, "-h", "addr")
+}
+
+func TestSerializeBindPort(t *testing.T) {
+	testSerialize(t, options{bindPort: 666}, "-p", "666")
+}
+
+func TestSerializeLogfile(t *testing.T) {
+	testSerialize(t, options{logFile: "path"}, "-l", "path")
+}
+
+func TestSerializePidfile(t *testing.T) {
+	testSerialize(t, options{pidFile: "path"}, "--pidfile", "path")
+}
+
+func TestSerializeCheckConfig(t *testing.T) {
+	testSerialize(t, options{checkConfig: true}, "--check-config")
+}
+
+func TestSerializeDisableUpdate(t *testing.T) {
+	testSerialize(t, options{disableUpdate: true}, "--no-check-update")
+}
+
+func TestSerializeService(t *testing.T) {
+	testSerialize(t, options{serviceControlAction: "run"}, "-s", "run")
+}
+
+func TestSerializeGLInet(t *testing.T) {
+	testSerialize(t, options{glinetMode: true}, "--glinet")
+}
+
+func TestSerializeMultiple(t *testing.T) {
+	testSerialize(t, options{
+		serviceControlAction: "run",
+		configFilename:       "config",
+		workDir:              "work",
+		pidFile:              "pid",
+		disableUpdate:        true,
+	}, "-c", "config", "-w", "work", "-s", "run", "--pidfile", "pid", "--no-check-update")
+}

From 9e87f0afed6f26605adb1661cdc32f5c07947c02 Mon Sep 17 00:00:00 2001
From: David Sheets <sheets@alum.mit.edu>
Date: Mon, 7 Sep 2020 10:04:31 +0100
Subject: [PATCH 10/19] home/auth: disable non-crypto RNG gosec lint check for
 session salt

Fixes #2078.
---
 home/auth.go | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/home/auth.go b/home/auth.go
index 36e56d05..afb7f0ed 100644
--- a/home/auth.go
+++ b/home/auth.go
@@ -276,7 +276,11 @@ type loginJSON struct {
 }
 
 func getSession(u *User) []byte {
-	d := []byte(fmt.Sprintf("%d%s%s", rand.Uint32(), u.Name, u.PasswordHash))
+	// the developers don't currently believe that using a
+	// non-cryptographic RNG for the session hash salt is
+	// insecure
+	salt := rand.Uint32() //nolint:gosec
+	d := []byte(fmt.Sprintf("%d%s%s", salt, u.Name, u.PasswordHash))
 	hash := sha256.Sum256(d)
 	return hash[:]
 }

From 90ef204d0473c9df6bba468f33f2669a0bcf519f Mon Sep 17 00:00:00 2001
From: David Sheets <sheets@alum.mit.edu>
Date: Mon, 7 Sep 2020 10:26:40 +0100
Subject: [PATCH 11/19] service: installation and running of AGH as a service
 with CLI args

---
 home/home.go    |  2 +-
 home/service.go | 13 +++++++++----
 2 files changed, 10 insertions(+), 5 deletions(-)

diff --git a/home/home.go b/home/home.go
index 806c59e2..898fb4bd 100644
--- a/home/home.go
+++ b/home/home.go
@@ -126,7 +126,7 @@ func Main(version string, channel string, armVer string) {
 	}()
 
 	if args.serviceControlAction != "" {
-		handleServiceControlAction(args.serviceControlAction)
+		handleServiceControlAction(args)
 		return
 	}
 
diff --git a/home/service.go b/home/service.go
index 21c9cfc6..b48c743b 100644
--- a/home/service.go
+++ b/home/service.go
@@ -24,12 +24,14 @@ const (
 
 // Represents the program that will be launched by a service or daemon
 type program struct {
+	opts options
 }
 
 // Start should quickly start the program
 func (p *program) Start(s service.Service) error {
 	// Start should not block. Do the actual work async.
-	args := options{runningAsService: true}
+	args := p.opts
+	args.runningAsService = true
 	go run(args)
 	return nil
 }
@@ -125,7 +127,8 @@ func sendSigReload() {
 // run - this is a special command that is not supposed to be used directly
 // it is specified when we register a service, and it indicates to the app
 // that it is being run as a service/daemon.
-func handleServiceControlAction(action string) {
+func handleServiceControlAction(opts options) {
+	action := opts.serviceControlAction
 	log.Printf("Service control action: %s", action)
 
 	if action == "reload" {
@@ -137,15 +140,17 @@ func handleServiceControlAction(action string) {
 	if err != nil {
 		log.Fatal("Unable to find the path to the current directory")
 	}
+	runOpts := opts
+	runOpts.serviceControlAction = "run"
 	svcConfig := &service.Config{
 		Name:             serviceName,
 		DisplayName:      serviceDisplayName,
 		Description:      serviceDescription,
 		WorkingDirectory: pwd,
-		Arguments:        []string{"-s", "run"},
+		Arguments:        serialize(runOpts),
 	}
 	configureService(svcConfig)
-	prg := &program{}
+	prg := &program{runOpts}
 	s, err := service.New(prg, svcConfig)
 	if err != nil {
 		log.Fatal(err)

From 14bc5297ac6cb7c6dbc74d55239eeb16c041c048 Mon Sep 17 00:00:00 2001
From: ArtemBaskal <a.baskal@adguard.com>
Date: Mon, 7 Sep 2020 13:50:03 +0300
Subject: [PATCH 12/19] + client: Add experimental DNS-over-QUIC support

---
 client/src/__locales/en.json                  |  8 +++++
 client/src/actions/encryption.js              |  2 ++
 .../Settings/Dns/Upstream/Examples.js         | 19 +++++++++++
 .../components/Settings/Encryption/Form.js    | 33 +++++++++++++++++--
 .../components/Settings/Encryption/index.js   |  2 ++
 client/src/components/Settings/Settings.css   |  2 +-
 client/src/helpers/constants.js               |  1 +
 client/src/helpers/validators.js              |  6 ++++
 8 files changed, 70 insertions(+), 3 deletions(-)

diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json
index 6cce0d91..58e270be 100644
--- a/client/src/__locales/en.json
+++ b/client/src/__locales/en.json
@@ -186,6 +186,7 @@
     "example_upstream_regular": "regular DNS (over UDP)",
     "example_upstream_dot": "encrypted <0>DNS-over-TLS</0>",
     "example_upstream_doh": "encrypted <0>DNS-over-HTTPS</0>",
+    "example_upstream_doq": "encrypted <0>DNS-over-QUIC</0>",
     "example_upstream_sdns": "you can use <0>DNS Stamps</0> for <1>DNSCrypt</1> or <2>DNS-over-HTTPS</2> resolvers",
     "example_upstream_tcp": "regular DNS (over TCP)",
     "all_lists_up_to_date_toast": "All lists are already up-to-date",
@@ -330,6 +331,8 @@
     "encryption_https_desc": "If HTTPS port is configured, AdGuard Home admin interface will be accessible via HTTPS, and it will also provide DNS-over-HTTPS on '/dns-query' location.",
     "encryption_dot": "DNS-over-TLS port",
     "encryption_dot_desc": "If this port is configured, AdGuard Home will run a DNS-over-TLS server on this port.",
+    "encryption_doq": "DNS-over-QUIC port",
+    "encryption_doq_desc": "If this port is configured, AdGuard Home will run a DNS-over-QUIC server on this port. It's experimental and may not be reliable at the moment. The only DNS provider that supports it now is AdGuard DNS",
     "encryption_certificates": "Certificates",
     "encryption_certificates_desc": "In order to use encryption, you need to provide a valid SSL certificates chain for your domain. You can get a free certificate on <0>{{link}}</0> or you can buy it from one of the trusted Certificate Authorities.",
     "encryption_certificates_input": "Copy/paste your PEM-encoded certificates here.",
@@ -574,5 +577,10 @@
     "original_response": "Original response",
     "click_to_view_queries": "Click to view queries",
     "port_53_faq_link": "Port 53 is often occupied by \"DNSStubListener\" or \"systemd-resolved\" services. Please read <0>this instruction</0> on how to resolve this.",
+<<<<<<< Updated upstream
     "adg_will_drop_dns_queries": "AdGuard Home will be dropping all DNS queries from this client."
+=======
+    "adg_will_drop_dns_queries": "AdGuard Home will be dropping all DNS queries from this client.",
+    "experimental": "Experimental"
+>>>>>>> Stashed changes
 }
diff --git a/client/src/actions/encryption.js b/client/src/actions/encryption.js
index 0e743323..36faf2ec 100644
--- a/client/src/actions/encryption.js
+++ b/client/src/actions/encryption.js
@@ -34,6 +34,7 @@ export const setTlsConfig = (config) => async (dispatch, getState) => {
         values.private_key = btoa(values.private_key);
         values.port_https = values.port_https || 0;
         values.port_dns_over_tls = values.port_dns_over_tls || 0;
+        values.port_dns_over_quic = values.port_dns_over_quic || 0;
 
         const response = await apiClient.setTlsConfig(values);
         response.certificate_chain = atob(response.certificate_chain);
@@ -59,6 +60,7 @@ export const validateTlsConfig = (config) => async (dispatch) => {
         values.private_key = btoa(values.private_key);
         values.port_https = values.port_https || 0;
         values.port_dns_over_tls = values.port_dns_over_tls || 0;
+        values.port_dns_over_quic = values.port_dns_over_quic || 0;
 
         const response = await apiClient.validateTlsConfig(values);
         response.certificate_chain = atob(response.certificate_chain);
diff --git a/client/src/components/Settings/Dns/Upstream/Examples.js b/client/src/components/Settings/Dns/Upstream/Examples.js
index de779b18..a18c1c90 100644
--- a/client/src/components/Settings/Dns/Upstream/Examples.js
+++ b/client/src/components/Settings/Dns/Upstream/Examples.js
@@ -63,6 +63,25 @@ const Examples = (props) => (
                     </Trans>
                 </span>
             </li>
+            <li>
+                <code>quic://dns-unfiltered.adguard.com:784</code> –&nbsp;
+                <span>
+                    <Trans
+                        components={[
+                            <a
+                                href="https://wikipedia.org/wiki/QUIC"
+                                target="_blank"
+                                rel="noopener noreferrer"
+                                key="0"
+                            >
+                                DNS-over-QUIC
+                            </a>,
+                        ]}
+                    >
+                        example_upstream_doq
+                    </Trans>
+                </span>
+            </li>
             <li>
                 <code>tcp://9.9.9.9</code> – <Trans>example_upstream_tcp</Trans>
             </li>
diff --git a/client/src/components/Settings/Encryption/Form.js b/client/src/components/Settings/Encryption/Form.js
index 7be23b10..ee56b315 100644
--- a/client/src/components/Settings/Encryption/Form.js
+++ b/client/src/components/Settings/Encryption/Form.js
@@ -11,11 +11,15 @@ import {
     renderRadioField,
     toNumber,
 } from '../../../helpers/form';
-import { validateIsSafePort, validatePort, validatePortTLS } from '../../../helpers/validators';
+import {
+    validateIsSafePort, validatePort, validatePortQuic, validatePortTLS,
+} from '../../../helpers/validators';
 import i18n from '../../../i18n';
 import KeyStatus from './KeyStatus';
 import CertificateStatus from './CertificateStatus';
-import { DNS_OVER_TLS_PORT, FORM_NAME, STANDARD_HTTPS_PORT } from '../../../helpers/constants';
+import {
+    DNS_OVER_QUIC_PORT, DNS_OVER_TLS_PORT, FORM_NAME, STANDARD_HTTPS_PORT,
+} from '../../../helpers/constants';
 
 const validate = (values) => {
     const errors = {};
@@ -38,6 +42,7 @@ const clearFields = (change, setTlsConfig, t) => {
         certificate_path: '',
         port_https: STANDARD_HTTPS_PORT,
         port_dns_over_tls: DNS_OVER_TLS_PORT,
+        port_dns_over_quic: DNS_OVER_QUIC_PORT,
         server_name: '',
         force_https: false,
         enabled: false,
@@ -189,6 +194,30 @@ let Form = (props) => {
                         </div>
                     </div>
                 </div>
+                <div className="col-lg-6">
+                    <div className="form__group form__group--settings">
+                        <label className="form__label" htmlFor="port_dns_over_quic">
+                            <Trans>encryption_doq</Trans>
+                            &nbsp;
+                            (<Trans>experimental</Trans>)
+                        </label>
+                        <Field
+                                id="port_dns_over_quic"
+                                name="port_dns_over_quic"
+                                component={renderInputField}
+                                type="number"
+                                className="form-control"
+                                placeholder={t('encryption_doq')}
+                                validate={[validatePortQuic]}
+                                normalize={toNumber}
+                                onChange={handleChange}
+                                disabled={!isEnabled}
+                        />
+                        <div className="form__desc">
+                            <Trans>encryption_doq_desc</Trans>
+                        </div>
+                    </div>
+                </div>
             </div>
             <div className="row">
                 <div className="col-12">
diff --git a/client/src/components/Settings/Encryption/index.js b/client/src/components/Settings/Encryption/index.js
index 7c2cccc8..f7ca52e0 100644
--- a/client/src/components/Settings/Encryption/index.js
+++ b/client/src/components/Settings/Encryption/index.js
@@ -66,6 +66,7 @@ class Encryption extends Component {
             force_https,
             port_https,
             port_dns_over_tls,
+            port_dns_over_quic,
             certificate_chain,
             private_key,
             certificate_path,
@@ -78,6 +79,7 @@ class Encryption extends Component {
             force_https,
             port_https,
             port_dns_over_tls,
+            port_dns_over_quic,
             certificate_chain,
             private_key,
             certificate_path,
diff --git a/client/src/components/Settings/Settings.css b/client/src/components/Settings/Settings.css
index 3bf1a121..4efb0868 100644
--- a/client/src/components/Settings/Settings.css
+++ b/client/src/components/Settings/Settings.css
@@ -54,7 +54,7 @@
 }
 
 .form__message--error {
-    color: var(--red);
+    color: #cd201f;
 }
 
 .form__message--left-pad {
diff --git a/client/src/helpers/constants.js b/client/src/helpers/constants.js
index e0075a25..1020a50c 100644
--- a/client/src/helpers/constants.js
+++ b/client/src/helpers/constants.js
@@ -69,6 +69,7 @@ export const STANDARD_DNS_PORT = 53;
 export const STANDARD_WEB_PORT = 80;
 export const STANDARD_HTTPS_PORT = 443;
 export const DNS_OVER_TLS_PORT = 853;
+export const DNS_OVER_QUIC_PORT = 784;
 export const MAX_PORT = 65535;
 
 export const EMPTY_DATE = '0001-01-01T00:00:00Z';
diff --git a/client/src/helpers/validators.js b/client/src/helpers/validators.js
index 45055154..64c5fa49 100644
--- a/client/src/helpers/validators.js
+++ b/client/src/helpers/validators.js
@@ -180,6 +180,12 @@ export const validatePortTLS = (value) => {
     return undefined;
 };
 
+/**
+ * @param value {number}
+ * @returns {undefined|string}
+ */
+export const validatePortQuic = validatePortTLS;
+
 /**
  * @param value {number}
  * @returns {undefined|string}

From 4d1666eff1f5643dab485e35051677c103dd20b2 Mon Sep 17 00:00:00 2001
From: ArtemBaskal <a.baskal@adguard.com>
Date: Mon, 7 Sep 2020 13:52:23 +0300
Subject: [PATCH 13/19] Resolve conflict

---
 client/src/__locales/en.json | 4 ----
 1 file changed, 4 deletions(-)

diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json
index 58e270be..eafb667b 100644
--- a/client/src/__locales/en.json
+++ b/client/src/__locales/en.json
@@ -577,10 +577,6 @@
     "original_response": "Original response",
     "click_to_view_queries": "Click to view queries",
     "port_53_faq_link": "Port 53 is often occupied by \"DNSStubListener\" or \"systemd-resolved\" services. Please read <0>this instruction</0> on how to resolve this.",
-<<<<<<< Updated upstream
-    "adg_will_drop_dns_queries": "AdGuard Home will be dropping all DNS queries from this client."
-=======
     "adg_will_drop_dns_queries": "AdGuard Home will be dropping all DNS queries from this client.",
     "experimental": "Experimental"
->>>>>>> Stashed changes
 }

From 9f3c27c03a6fcba71ee84e3826c3e10bd270c348 Mon Sep 17 00:00:00 2001
From: ArtemBaskal <a.baskal@adguard.com>
Date: Tue, 8 Sep 2020 10:17:39 +0300
Subject: [PATCH 14/19] Change link and translation

---
 client/src/__locales/en.json                            | 2 +-
 client/src/components/Settings/Dns/Upstream/Examples.js | 4 +++-
 client/src/components/Settings/Encryption/Form.js       | 2 +-
 3 files changed, 5 insertions(+), 3 deletions(-)

diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json
index eafb667b..6abd1d4c 100644
--- a/client/src/__locales/en.json
+++ b/client/src/__locales/en.json
@@ -332,7 +332,7 @@
     "encryption_dot": "DNS-over-TLS port",
     "encryption_dot_desc": "If this port is configured, AdGuard Home will run a DNS-over-TLS server on this port.",
     "encryption_doq": "DNS-over-QUIC port",
-    "encryption_doq_desc": "If this port is configured, AdGuard Home will run a DNS-over-QUIC server on this port. It's experimental and may not be reliable at the moment. The only DNS provider that supports it now is AdGuard DNS",
+    "encryption_doq_desc": "If this port is configured, AdGuard Home will run a DNS-over-QUIC server on this port. It's experimental and may not be reliable. Also, there are not too many clients that support it at the moment.",
     "encryption_certificates": "Certificates",
     "encryption_certificates_desc": "In order to use encryption, you need to provide a valid SSL certificates chain for your domain. You can get a free certificate on <0>{{link}}</0> or you can buy it from one of the trusted Certificate Authorities.",
     "encryption_certificates_input": "Copy/paste your PEM-encoded certificates here.",
diff --git a/client/src/components/Settings/Dns/Upstream/Examples.js b/client/src/components/Settings/Dns/Upstream/Examples.js
index a18c1c90..70797909 100644
--- a/client/src/components/Settings/Dns/Upstream/Examples.js
+++ b/client/src/components/Settings/Dns/Upstream/Examples.js
@@ -69,7 +69,7 @@ const Examples = (props) => (
                     <Trans
                         components={[
                             <a
-                                href="https://wikipedia.org/wiki/QUIC"
+                                href="https://tools.ietf.org/html/draft-huitema-quic-dnsoquic-07"
                                 target="_blank"
                                 rel="noopener noreferrer"
                                 key="0"
@@ -80,6 +80,8 @@ const Examples = (props) => (
                     >
                         example_upstream_doq
                     </Trans>
+                    &nbsp;
+                    <span className="text-lowercase">(<Trans>experimental</Trans>)</span>
                 </span>
             </li>
             <li>
diff --git a/client/src/components/Settings/Encryption/Form.js b/client/src/components/Settings/Encryption/Form.js
index ee56b315..15f8a3c6 100644
--- a/client/src/components/Settings/Encryption/Form.js
+++ b/client/src/components/Settings/Encryption/Form.js
@@ -199,7 +199,7 @@ let Form = (props) => {
                         <label className="form__label" htmlFor="port_dns_over_quic">
                             <Trans>encryption_doq</Trans>
                             &nbsp;
-                            (<Trans>experimental</Trans>)
+                            <span className="text-lowercase">(<Trans>experimental</Trans>)</span>
                         </label>
                         <Field
                                 id="port_dns_over_quic"

From 06594bde8f70c5bbf04b03a3e1c9ad99539787e2 Mon Sep 17 00:00:00 2001
From: Artem Baskal <a.baskal@adguard.com>
Date: Tue, 8 Sep 2020 13:18:19 +0300
Subject: [PATCH 15/19] - client: Add link to 'update_failed' error toast

Close #2062

Squashed commit of the following:

commit a1a1d4fe74dd414f83477d972bc07062e2c890ab
Merge: 9535e109 84938c56
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Tue Sep 8 10:21:47 2020 +0300

    Merge branch 'master' into fix/2062

commit 9535e10934c57c2592df234a030bad183c0086cd
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Mon Sep 7 13:59:57 2020 +0300

    Fix translation

commit e6f912d1d2793fd008c22b4418681abcc54896d0
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Mon Sep 7 12:03:45 2020 +0300

    - client: Add link to 'update_failed' error toast
---
 client/package-lock.json              |  6 +++---
 client/package.json                   |  2 +-
 client/src/__locales/en.json          |  2 +-
 client/src/actions/index.js           | 12 ++++++++++--
 client/src/components/Toasts/Toast.js | 12 +++++++++---
 client/src/helpers/constants.js       | 13 ++++++++-----
 client/src/reducers/toasts.js         |  2 ++
 7 files changed, 34 insertions(+), 15 deletions(-)

diff --git a/client/package-lock.json b/client/package-lock.json
index 84c8e186..42cac10f 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -12377,9 +12377,9 @@
       }
     },
     "react-i18next": {
-      "version": "11.4.0",
-      "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.4.0.tgz",
-      "integrity": "sha512-lyOZSSQkif4H9HnHN3iEKVkryLI+WkdZSEw3VAZzinZLopfYRMHVY5YxCopdkXPLEHs6S5GjKYPh3+j0j336Fg==",
+      "version": "11.7.2",
+      "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.7.2.tgz",
+      "integrity": "sha512-Djj3K3hh5Tecla2CI9rLO3TZBYGMFrGilm0JY4cLofAQONCi5TK6nVmUPKoB59n1ZffgjfgJt6zlbE9aGF6Q0Q==",
       "requires": {
         "@babel/runtime": "^7.3.1",
         "html-parse-stringify2": "2.0.1"
diff --git a/client/package.json b/client/package.json
index fdc19c9d..4ad8d5eb 100644
--- a/client/package.json
+++ b/client/package.json
@@ -28,7 +28,7 @@
         "react": "^16.13.1",
         "react-click-outside": "^3.0.1",
         "react-dom": "^16.13.1",
-        "react-i18next": "^11.4.0",
+        "react-i18next": "^11.7.2",
         "react-modal": "^3.11.2",
         "react-popper-tooltip": "^2.11.1",
         "react-redux": "^7.2.0",
diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json
index 6cce0d91..56b6988f 100644
--- a/client/src/__locales/en.json
+++ b/client/src/__locales/en.json
@@ -363,7 +363,7 @@
     "fix": "Fix",
     "dns_providers": "Here is a <0>list of known DNS providers</0> to choose from.",
     "update_now": "Update now",
-    "update_failed": "Auto-update failed. Please <a href='https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started#update'>follow the steps</a> to update manually.",
+    "update_failed": "Auto-update failed. Please <a>follow these steps</a> to update manually.",
     "processing_update": "Please wait, AdGuard Home is being updated",
     "clients_title": "Clients",
     "clients_desc": "Configure devices connected to AdGuard Home",
diff --git a/client/src/actions/index.js b/client/src/actions/index.js
index d4018bb0..ff039af1 100644
--- a/client/src/actions/index.js
+++ b/client/src/actions/index.js
@@ -4,9 +4,10 @@ import axios from 'axios';
 
 import endsWith from 'lodash/endsWith';
 import escapeRegExp from 'lodash/escapeRegExp';
+import React from 'react';
 import { splitByNewLine, sortClients } from '../helpers/helpers';
 import {
-    BLOCK_ACTIONS, CHECK_TIMEOUT, STATUS_RESPONSE, SETTINGS_NAMES, FORM_NAME,
+    BLOCK_ACTIONS, CHECK_TIMEOUT, STATUS_RESPONSE, SETTINGS_NAMES, FORM_NAME, GETTING_STARTED_LINK,
 } from '../helpers/constants';
 import { areEqualVersions } from '../helpers/version';
 import { getTlsStatus } from './encryption';
@@ -184,7 +185,14 @@ export const getUpdate = () => async (dispatch, getState) => {
 
     dispatch(getUpdateRequest());
     const handleRequestError = () => {
-        dispatch(addNoticeToast({ error: 'update_failed' }));
+        const options = {
+            components: {
+                a: <a href={GETTING_STARTED_LINK} target="_blank"
+                      rel="noopener noreferrer" />,
+            },
+        };
+
+        dispatch(addNoticeToast({ error: 'update_failed', options }));
         dispatch(getUpdateFailure());
     };
 
diff --git a/client/src/components/Toasts/Toast.js b/client/src/components/Toasts/Toast.js
index a4c58aad..4c46078a 100644
--- a/client/src/components/Toasts/Toast.js
+++ b/client/src/components/Toasts/Toast.js
@@ -1,6 +1,6 @@
 import React, { useEffect, useState } from 'react';
 import PropTypes from 'prop-types';
-import { useTranslation } from 'react-i18next';
+import { Trans } from 'react-i18next';
 import { useDispatch } from 'react-redux';
 import { TOAST_TIMEOUTS } from '../../helpers/constants';
 import { removeToast } from '../../actions';
@@ -9,8 +9,8 @@ const Toast = ({
     id,
     message,
     type,
+    options,
 }) => {
-    const { t } = useTranslation();
     const dispatch = useDispatch();
     const [timerId, setTimerId] = useState(null);
 
@@ -30,7 +30,12 @@ const Toast = ({
     return <div className={`toast toast--${type}`}
                 onMouseOver={clearRemoveToastTimeout}
                 onMouseOut={setRemoveToastTimeout}>
-        <p className="toast__content">{t(message)}</p>
+        <p className="toast__content">
+            <Trans
+                    i18nKey={message}
+                    {...options}
+            />
+        </p>
         <button className="toast__dismiss" onClick={removeCurrentToast}>
             <svg stroke="#fff" fill="none" width="20" height="20" strokeWidth="2"
                  viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
@@ -45,6 +50,7 @@ Toast.propTypes = {
     id: PropTypes.string.isRequired,
     message: PropTypes.string.isRequired,
     type: PropTypes.string.isRequired,
+    options: PropTypes.object,
 };
 
 export default Toast;
diff --git a/client/src/helpers/constants.js b/client/src/helpers/constants.js
index e0075a25..90d06a6f 100644
--- a/client/src/helpers/constants.js
+++ b/client/src/helpers/constants.js
@@ -53,6 +53,8 @@ export const REPOSITORY = {
 export const PRIVACY_POLICY_LINK = 'https://adguard.com/privacy/home.html';
 export const PORT_53_FAQ_LINK = 'https://github.com/AdguardTeam/AdGuardHome/wiki/FAQ#bindinuse';
 
+export const GETTING_STARTED_LINK = 'https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started#update';
+
 export const ADDRESS_IN_USE_TEXT = 'address already in use';
 
 export const INSTALL_FIRST_STEP = 1;
@@ -76,8 +78,6 @@ export const EMPTY_DATE = '0001-01-01T00:00:00Z';
 export const DEBOUNCE_TIMEOUT = 300;
 export const DEBOUNCE_FILTER_TIMEOUT = 500;
 export const CHECK_TIMEOUT = 1000;
-export const SUCCESS_TOAST_TIMEOUT = 5000;
-export const FAILURE_TOAST_TIMEOUT = 30000;
 export const HIDE_TOOLTIP_DELAY = 300;
 export const SHOW_TOOLTIP_DELAY = 200;
 export const MODAL_OPEN_TIMEOUT = 150;
@@ -540,8 +540,11 @@ export const TOAST_TYPES = {
     NOTICE: 'notice',
 };
 
+export const SUCCESS_TOAST_TIMEOUT = 5000;
+export const FAILURE_TOAST_TIMEOUT = 30000;
+
 export const TOAST_TIMEOUTS = {
-    [TOAST_TYPES.SUCCESS]: 5000,
-    [TOAST_TYPES.ERROR]: 30000,
-    [TOAST_TYPES.NOTICE]: 30000,
+    [TOAST_TYPES.SUCCESS]: SUCCESS_TOAST_TIMEOUT,
+    [TOAST_TYPES.ERROR]: FAILURE_TOAST_TIMEOUT,
+    [TOAST_TYPES.NOTICE]: FAILURE_TOAST_TIMEOUT,
 };
diff --git a/client/src/reducers/toasts.js b/client/src/reducers/toasts.js
index be66bd57..7e76b312 100644
--- a/client/src/reducers/toasts.js
+++ b/client/src/reducers/toasts.js
@@ -15,6 +15,7 @@ const toasts = handleActions({
         const errorToast = {
             id: nanoid(),
             message,
+            options: payload.options,
             type: TOAST_TYPES.ERROR,
         };
 
@@ -35,6 +36,7 @@ const toasts = handleActions({
         const noticeToast = {
             id: nanoid(),
             message: payload.error.toString(),
+            options: payload.options,
             type: TOAST_TYPES.NOTICE,
         };
 

From c5ca2e60c6c94f0e00a710e692e8d9a6900371d1 Mon Sep 17 00:00:00 2001
From: Artem Baskal <a.baskal@adguard.com>
Date: Tue, 8 Sep 2020 14:05:26 +0300
Subject: [PATCH 16/19] - client: Count client requests correctly

Close #2037

Squashed commit of the following:

commit f19b5f5919ab0c9c31e03728a0def04f02b8d7db
Merge: b51cf160 06594bde
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Tue Sep 8 13:18:39 2020 +0300

    Merge branch 'master' into fix/2037

commit b51cf16008f516bfeed6d8212d27902514d3251c
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Fri Sep 4 18:06:17 2020 +0300

    - client: Count client requests correctly
---
 client/src/__tests__/helpers.test.js          | 59 ++++++++++++++-
 .../Settings/Clients/ClientsTable.js          |  7 +-
 client/src/helpers/constants.js               |  6 ++
 client/src/helpers/helpers.js                 | 72 +++++++++++++++++++
 4 files changed, 140 insertions(+), 4 deletions(-)

diff --git a/client/src/__tests__/helpers.test.js b/client/src/__tests__/helpers.test.js
index f974cca6..bb371be4 100644
--- a/client/src/__tests__/helpers.test.js
+++ b/client/src/__tests__/helpers.test.js
@@ -1,5 +1,7 @@
-import { getIpMatchListStatus, sortIp } from '../helpers/helpers';
-import { IP_MATCH_LIST_STATUS } from '../helpers/constants';
+import {
+    countClientsStatistics, findAddressType, getIpMatchListStatus, sortIp,
+} from '../helpers/helpers';
+import { ADDRESS_TYPES, IP_MATCH_LIST_STATUS } from '../helpers/constants';
 
 describe('getIpMatchListStatus', () => {
     describe('IPv4', () => {
@@ -482,3 +484,56 @@ describe('sortIp', () => {
         });
     });
 });
+
+describe('findAddressType', () => {
+    describe('ip', () => {
+        expect(findAddressType('127.0.0.1')).toStrictEqual(ADDRESS_TYPES.IP);
+    });
+    describe('cidr', () => {
+        expect(findAddressType('127.0.0.1/8')).toStrictEqual(ADDRESS_TYPES.CIDR);
+    });
+    describe('mac', () => {
+        expect(findAddressType('00:1B:44:11:3A:B7')).toStrictEqual(ADDRESS_TYPES.UNKNOWN);
+    });
+});
+
+describe('countClientsStatistics', () => {
+    test('single ip', () => {
+        expect(countClientsStatistics(['127.0.0.1'], {
+            '127.0.0.1': 1,
+        })).toStrictEqual(1);
+    });
+    test('multiple ip', () => {
+        expect(countClientsStatistics(['127.0.0.1', '127.0.0.2'], {
+            '127.0.0.1': 1,
+            '127.0.0.2': 2,
+        })).toStrictEqual(1 + 2);
+    });
+    test('cidr', () => {
+        expect(countClientsStatistics(['127.0.0.0/8'], {
+            '127.0.0.1': 1,
+            '127.0.0.2': 2,
+        })).toStrictEqual(1 + 2);
+    });
+    test('cidr and multiple ip', () => {
+        expect(countClientsStatistics(['1.1.1.1', '2.2.2.2', '3.3.3.0/24'], {
+            '1.1.1.1': 1,
+            '2.2.2.2': 2,
+            '3.3.3.3': 3,
+        })).toStrictEqual(1 + 2 + 3);
+    });
+    test('mac', () => {
+        expect(countClientsStatistics(['00:1B:44:11:3A:B7', '2.2.2.2', '3.3.3.0/24'], {
+            '1.1.1.1': 1,
+            '2.2.2.2': 2,
+            '3.3.3.3': 3,
+        })).toStrictEqual(2 + 3);
+    });
+    test('not found', () => {
+        expect(countClientsStatistics(['4.4.4.4', '5.5.5.5', '6.6.6.6'], {
+            '1.1.1.1': 1,
+            '2.2.2.2': 2,
+            '3.3.3.3': 3,
+        })).toStrictEqual(0);
+    });
+});
diff --git a/client/src/components/Settings/Clients/ClientsTable.js b/client/src/components/Settings/Clients/ClientsTable.js
index cb7f2914..ac613164 100644
--- a/client/src/components/Settings/Clients/ClientsTable.js
+++ b/client/src/components/Settings/Clients/ClientsTable.js
@@ -4,7 +4,7 @@ import { Trans, withTranslation } from 'react-i18next';
 import ReactTable from 'react-table';
 
 import { MODAL_TYPE } from '../../../helpers/constants';
-import { splitByNewLine } from '../../../helpers/helpers';
+import { splitByNewLine, countClientsStatistics } from '../../../helpers/helpers';
 import Card from '../../ui/Card';
 import Modal from './Modal';
 import CellWrap from '../../ui/CellWrap';
@@ -204,7 +204,10 @@ class ClientsTable extends Component {
         {
             Header: this.props.t('requests_count'),
             id: 'statistics',
-            accessor: (row) => this.props.normalizedTopClients.configured[row.name] || 0,
+            accessor: (row) => countClientsStatistics(
+                row.ids,
+                this.props.normalizedTopClients.auto,
+            ),
             sortMethod: (a, b) => b - a,
             minWidth: 120,
             Cell: (row) => {
diff --git a/client/src/helpers/constants.js b/client/src/helpers/constants.js
index 90d06a6f..deabf72a 100644
--- a/client/src/helpers/constants.js
+++ b/client/src/helpers/constants.js
@@ -548,3 +548,9 @@ export const TOAST_TIMEOUTS = {
     [TOAST_TYPES.ERROR]: FAILURE_TOAST_TIMEOUT,
     [TOAST_TYPES.NOTICE]: FAILURE_TOAST_TIMEOUT,
 };
+
+export const ADDRESS_TYPES = {
+    IP: 'IP',
+    CIDR: 'CIDR',
+    UNKNOWN: 'UNKNOWN',
+};
diff --git a/client/src/helpers/helpers.js b/client/src/helpers/helpers.js
index bafd230e..f509ef44 100644
--- a/client/src/helpers/helpers.js
+++ b/client/src/helpers/helpers.js
@@ -14,6 +14,7 @@ import queryString from 'query-string';
 import { getTrackerData } from './trackers/trackers';
 
 import {
+    ADDRESS_TYPES,
     CHECK_TIMEOUT,
     CUSTOM_FILTERING_RULES_ID,
     DEFAULT_DATE_FORMAT_OPTIONS,
@@ -509,6 +510,18 @@ const isIpMatchCidr = (parsedIp, parsedCidr) => {
     }
 };
 
+export const isIpInCidr = (ip, cidr) => {
+    try {
+        const parsedIp = ipaddr.parse(ip);
+        const parsedCidr = ipaddr.parseCIDR(cidr);
+
+        return isIpMatchCidr(parsedIp, parsedCidr);
+    } catch (e) {
+        console.error(e);
+        return false;
+    }
+};
+
 /**
  * The purpose of this method is to quickly check
  * if this IP can possibly be in the specified CIDR range.
@@ -578,6 +591,29 @@ const isIpQuickMatchCIDR = (ip, listItem) => {
     return false;
 };
 
+/**
+ *
+ * @param ipOrCidr
+ * @returns {'IP' | 'CIDR' | 'UNKNOWN'}
+ *
+ */
+export const findAddressType = (address) => {
+    try {
+        const cidrMaybe = address.includes('/');
+
+        if (!cidrMaybe && ipaddr.isValid(address)) {
+            return ADDRESS_TYPES.IP;
+        }
+        if (cidrMaybe && ipaddr.parseCIDR(address)) {
+            return ADDRESS_TYPES.CIDR;
+        }
+
+        return ADDRESS_TYPES.UNKNOWN;
+    } catch (e) {
+        return ADDRESS_TYPES.UNKNOWN;
+    }
+};
+
 /**
  * @param ip {string}
  * @param list {string}
@@ -622,6 +658,42 @@ export const getIpMatchListStatus = (ip, list) => {
     }
 };
 
+/**
+ * @param ids {string[]}
+ * @returns {Object}
+ */
+export const separateIpsAndCidrs = (ids) => ids.reduce((acc, curr) => {
+    const addressType = findAddressType(curr);
+
+    if (addressType === ADDRESS_TYPES.IP) {
+        acc.ips.push(curr);
+    }
+    if (addressType === ADDRESS_TYPES.CIDR) {
+        acc.cidrs.push(curr);
+    }
+    return acc;
+}, { ips: [], cidrs: [] });
+
+export const countClientsStatistics = (ids, autoClients) => {
+    const { ips, cidrs } = separateIpsAndCidrs(ids);
+
+    const ipsCount = ips.reduce((acc, curr) => {
+        const count = autoClients[curr] || 0;
+        return acc + count;
+    }, 0);
+
+    const cidrsCount = Object.entries(autoClients)
+        .reduce((acc, curr) => {
+            const [id, count] = curr;
+            if (cidrs.some((cidr) => isIpInCidr(id, cidr))) {
+            // eslint-disable-next-line no-param-reassign
+                acc += count;
+            }
+            return acc;
+        }, 0);
+
+    return ipsCount + cidrsCount;
+};
 
 /**
  * @param {string} elapsedMs

From 7be0cc72df9b01922e6bc5b3e6e9c6ab57b3fc26 Mon Sep 17 00:00:00 2001
From: Andrey Meshkov <am@adguard.com>
Date: Tue, 8 Sep 2020 15:53:25 +0300
Subject: [PATCH 17/19] Don't run home_test with race detection

---
 .githooks/pre-commit | 6 +++++-
 home/home_test.go    | 2 ++
 2 files changed, 7 insertions(+), 1 deletion(-)

diff --git a/.githooks/pre-commit b/.githooks/pre-commit
index d933e462..4fd4e4f5 100755
--- a/.githooks/pre-commit
+++ b/.githooks/pre-commit
@@ -1,6 +1,10 @@
 #!/bin/bash
 set -e;
-git diff --cached --name-only | grep -q '.js$' && make lint-js;
+git diff --cached --name-only | grep -q '.js$' && found=1
+if [ $found == 1 ]; then
+    make lint-js || exit 1
+    npm run test --prefix client || exit 1
+fi
 
 found=0
 git diff --cached --name-only | grep -q '.go$' && found=1
diff --git a/home/home_test.go b/home/home_test.go
index 0868cb71..c78aee52 100644
--- a/home/home_test.go
+++ b/home/home_test.go
@@ -1,3 +1,5 @@
+// +build !race
+
 package home
 
 import (

From 314867734ac60e430143a25060ee6e62e027a9cb Mon Sep 17 00:00:00 2001
From: Andrey Meshkov <am@adguard.com>
Date: Tue, 8 Sep 2020 17:20:24 +0300
Subject: [PATCH 18/19] - (dnsforward): fixed running only quic, added a test

QUIC was not initialized if DOT port is not set. Also, there were no
tests on DoQ functionality.
---
 dnsforward/config.go          | 65 ++++++++++++++++++++---------------
 dnsforward/dnsforward_test.go | 36 +++++++++++++++++++
 go.sum                        |  2 ++
 3 files changed, 76 insertions(+), 27 deletions(-)

diff --git a/dnsforward/config.go b/dnsforward/config.go
index 5d06b9dc..4c86a2ff 100644
--- a/dnsforward/config.go
+++ b/dnsforward/config.go
@@ -154,10 +154,6 @@ func (s *Server) createProxyConfig() (proxy.Config, error) {
 		MaxGoroutines:          int(s.conf.MaxGoroutines),
 	}
 
-	if s.conf.QUICListenAddr != nil {
-		proxyConfig.QUICListenAddr = []*net.UDPAddr{s.conf.QUICListenAddr}
-	}
-
 	if s.conf.CacheSize != 0 {
 		proxyConfig.CacheEnabled = true
 		proxyConfig.CacheSizeBytes = int(s.conf.CacheSize)
@@ -240,34 +236,49 @@ func (s *Server) prepareIntlProxy() {
 
 // prepareTLS - prepares TLS configuration for the DNS proxy
 func (s *Server) prepareTLS(proxyConfig *proxy.Config) error {
-	if s.conf.TLSListenAddr != nil && len(s.conf.CertificateChainData) != 0 && len(s.conf.PrivateKeyData) != 0 {
+	if len(s.conf.CertificateChainData) == 0 || len(s.conf.PrivateKeyData) == 0 {
+		return nil
+	}
+
+	if s.conf.TLSListenAddr == nil &&
+		s.conf.QUICListenAddr == nil {
+		return nil
+	}
+
+	if s.conf.TLSListenAddr != nil {
 		proxyConfig.TLSListenAddr = []*net.TCPAddr{s.conf.TLSListenAddr}
-		var err error
-		s.conf.cert, err = tls.X509KeyPair(s.conf.CertificateChainData, s.conf.PrivateKeyData)
+	}
+
+	if s.conf.QUICListenAddr != nil {
+		proxyConfig.QUICListenAddr = []*net.UDPAddr{s.conf.QUICListenAddr}
+	}
+
+	var err error
+	s.conf.cert, err = tls.X509KeyPair(s.conf.CertificateChainData, s.conf.PrivateKeyData)
+	if err != nil {
+		return errorx.Decorate(err, "Failed to parse TLS keypair")
+	}
+
+	if s.conf.StrictSNICheck {
+		x, err := x509.ParseCertificate(s.conf.cert.Certificate[0])
 		if err != nil {
-			return errorx.Decorate(err, "Failed to parse TLS keypair")
+			return errorx.Decorate(err, "x509.ParseCertificate(): %s", err)
 		}
-
-		if s.conf.StrictSNICheck {
-			x, err := x509.ParseCertificate(s.conf.cert.Certificate[0])
-			if err != nil {
-				return errorx.Decorate(err, "x509.ParseCertificate(): %s", err)
-			}
-			if len(x.DNSNames) != 0 {
-				s.conf.dnsNames = x.DNSNames
-				log.Debug("DNS: using DNS names from certificate's SAN: %v", x.DNSNames)
-				sort.Strings(s.conf.dnsNames)
-			} else {
-				s.conf.dnsNames = append(s.conf.dnsNames, x.Subject.CommonName)
-				log.Debug("DNS: using DNS name from certificate's CN: %s", x.Subject.CommonName)
-			}
-		}
-
-		proxyConfig.TLSConfig = &tls.Config{
-			GetCertificate: s.onGetCertificate,
-			MinVersion:     tls.VersionTLS12,
+		if len(x.DNSNames) != 0 {
+			s.conf.dnsNames = x.DNSNames
+			log.Debug("DNS: using DNS names from certificate's SAN: %v", x.DNSNames)
+			sort.Strings(s.conf.dnsNames)
+		} else {
+			s.conf.dnsNames = append(s.conf.dnsNames, x.Subject.CommonName)
+			log.Debug("DNS: using DNS name from certificate's CN: %s", x.Subject.CommonName)
 		}
 	}
+
+	proxyConfig.TLSConfig = &tls.Config{
+		GetCertificate: s.onGetCertificate,
+		MinVersion:     tls.VersionTLS12,
+	}
+
 	upstream.RootCAs = s.conf.TLSv12Roots
 	upstream.CipherSuites = s.conf.TLSCiphers
 	return nil
diff --git a/dnsforward/dnsforward_test.go b/dnsforward/dnsforward_test.go
index a3eb94f1..a183d786 100644
--- a/dnsforward/dnsforward_test.go
+++ b/dnsforward/dnsforward_test.go
@@ -8,6 +8,7 @@ import (
 	"crypto/x509"
 	"crypto/x509/pkix"
 	"encoding/pem"
+	"fmt"
 	"math/big"
 	"net"
 	"sort"
@@ -128,6 +129,41 @@ func TestDotServer(t *testing.T) {
 	}
 }
 
+func TestDoqServer(t *testing.T) {
+	// Prepare the proxy server
+	_, certPem, keyPem := createServerTLSConfig(t)
+	s := createTestServer(t)
+
+	s.conf.TLSConfig = TLSConfig{
+		QUICListenAddr:       &net.UDPAddr{Port: 0},
+		CertificateChainData: certPem,
+		PrivateKeyData:       keyPem,
+	}
+
+	_ = s.Prepare(nil)
+	// Starting the server
+	err := s.Start()
+	assert.Nil(t, err)
+
+	// Create a DNS-over-QUIC upstream
+	addr := s.dnsProxy.Addr(proxy.ProtoQUIC)
+	opts := upstream.Options{InsecureSkipVerify: true}
+	u, err := upstream.AddressToUpstream(fmt.Sprintf("quic://%s", addr), opts)
+	assert.Nil(t, err)
+
+	// Send the test message
+	req := createGoogleATestMessage()
+	res, err := u.Exchange(req)
+	assert.Nil(t, err)
+	assertGoogleAResponse(t, res)
+
+	// Stop the proxy
+	err = s.Stop()
+	if err != nil {
+		t.Fatalf("DNS server failed to stop: %s", err)
+	}
+}
+
 func TestServerRace(t *testing.T) {
 	s := createTestServer(t)
 	err := s.Start()
diff --git a/go.sum b/go.sum
index d1e535fa..60b7eda0 100644
--- a/go.sum
+++ b/go.sum
@@ -290,6 +290,7 @@ golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLL
 golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k=
 golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU=
 golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -382,6 +383,7 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy
 gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
 gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
 gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
 gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

From 7d7609cf7a2c0c3c539649776f0eebd35adb08c7 Mon Sep 17 00:00:00 2001
From: Andrey Meshkov <am@adguard.com>
Date: Tue, 8 Sep 2020 17:28:01 +0300
Subject: [PATCH 19/19] * (dnsforward): moved setting upstream.RootCAs

---
 dnsforward/config.go | 19 ++++++++++++++-----
 1 file changed, 14 insertions(+), 5 deletions(-)

diff --git a/dnsforward/config.go b/dnsforward/config.go
index 4c86a2ff..4cba7f30 100644
--- a/dnsforward/config.go
+++ b/dnsforward/config.go
@@ -9,12 +9,11 @@ import (
 	"net/http"
 	"sort"
 
-	"github.com/AdguardTeam/golibs/log"
-	"github.com/joomcode/errorx"
-
 	"github.com/AdguardTeam/AdGuardHome/dnsfilter"
 	"github.com/AdguardTeam/dnsproxy/proxy"
 	"github.com/AdguardTeam/dnsproxy/upstream"
+	"github.com/AdguardTeam/golibs/log"
+	"github.com/joomcode/errorx"
 )
 
 // FilteringConfig represents the DNS filtering configuration of AdGuard Home
@@ -216,6 +215,18 @@ func (s *Server) initDefaultSettings() {
 
 // prepareUpstreamSettings - prepares upstream DNS server settings
 func (s *Server) prepareUpstreamSettings() error {
+	// We're setting a customized set of RootCAs
+	// The reason is that Go default mechanism of loading TLS roots
+	// does not always work properly on some routers so we're
+	// loading roots manually and pass it here.
+	// See "util.LoadSystemRootCAs"
+	upstream.RootCAs = s.conf.TLSv12Roots
+
+	// See util.InitTLSCiphers -- removed unsafe ciphers
+	if len(s.conf.TLSCiphers) > 0 {
+		upstream.CipherSuites = s.conf.TLSCiphers
+	}
+
 	upstreamConfig, err := proxy.ParseUpstreamsConfig(s.conf.UpstreamDNS, s.conf.BootstrapDNS, DefaultTimeout)
 	if err != nil {
 		return fmt.Errorf("DNS: proxy.ParseUpstreamsConfig: %s", err)
@@ -279,8 +290,6 @@ func (s *Server) prepareTLS(proxyConfig *proxy.Config) error {
 		MinVersion:     tls.VersionTLS12,
 	}
 
-	upstream.RootCAs = s.conf.TLSv12Roots
-	upstream.CipherSuites = s.conf.TLSCiphers
 	return nil
 }