From e2ddc82d70ecff9ed2ddedb68bffaf7937a21370 Mon Sep 17 00:00:00 2001
From: Simon Zolin <s.zolin@adguard.com>
Date: Wed, 22 Apr 2020 19:14:04 +0300
Subject: [PATCH] + DNS: add fastest_addr setting

* API: /dns_info, /dns_config: add "parallel_requests" instead of "all_servers" from /set_upstreams_config
* API: /status: removed fields

#715

Squashed commit of the following:

commit 7dd913bd336ecbaa7419b998d0bf913d89702fe6
Merge: 43706970 8170955a
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Wed Apr 22 19:09:36 2020 +0300

    Merge remote-tracking branch 'origin/master' into 715-fastest-addr

commit 437069702a3e91e0b066e4b22b08cdc02ff19eaf
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Wed Apr 22 19:08:55 2020 +0300

    minor

commit 9e713df80c5bf113c98794c0a20915c756a76938
Merge: e3bf4037 9b7c1181
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Tue Apr 21 16:02:03 2020 +0300

    Merge remote-tracking branch 'origin/master' into 715-fastest-addr

commit e3bf4037f49198e42bde55305d6f9077341b556a
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Tue Apr 21 15:40:49 2020 +0300

    minor

commit d6e6a823c5e51acc061b2850d362772efcb827e1
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Fri Apr 17 17:56:24 2020 +0300

    * API changes

    . removed POST /set_upstreams_config
    . removed fields from GET /status: bootstrap_dns, upstream_dns, all_servers
    . added new fields to /dns_config and /dns_info

commit 237a452d09cc48ff8f00e81c7fd35e7828bea835
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Fri Apr 17 16:43:13 2020 +0300

    * API: /dns_info, /dns_config: add "parallel_requests" instead of "all_servers" from /set_upstreams_config

commit 9976723b9725ed19e0cce152d1d1198b13c4acc1
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Mon Mar 23 10:28:25 2020 +0300

    openapi

commit 6f8ea16c6332606f29095b0094d71e8a91798f82
Merge: 36e4d4e8 c8285c41
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Fri Mar 20 19:18:48 2020 +0300

    Merge remote-tracking branch 'origin/master' into 715-fastest-addr

commit 36e4d4e82cadeaba5a11313f0d69d66a0924c342
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Fri Mar 20 18:13:43 2020 +0300

    + DNS: add fastest_addr setting
---
 AGHTechDoc.md                 | 10 ++++
 dnsforward/dnsforward.go      |  3 ++
 dnsforward/dnsforward_http.go | 94 ++++++++++++++++++-----------------
 home/control.go               |  3 --
 openapi/CHANGELOG.md          | 64 ++++++++++++++++++++++++
 openapi/openapi.yaml          | 57 +++++++++------------
 6 files changed, 148 insertions(+), 83 deletions(-)

diff --git a/AGHTechDoc.md b/AGHTechDoc.md
index 4c8e0fda..5a3b001c 100644
--- a/AGHTechDoc.md
+++ b/AGHTechDoc.md
@@ -882,6 +882,9 @@ Response:
 	200 OK
 
 	{
+		"upstream_dns": ["tls://...", ...],
+		"bootstrap_dns": ["1.2.3.4", ...],
+
 		"protection_enabled": true | false,
 		"ratelimit": 1234,
 		"blocking_mode": "default" | "nxdomain" | "null_ip" | "custom_ip",
@@ -890,6 +893,8 @@ Response:
 		"edns_cs_enabled": true | false,
 		"dnssec_enabled": true | false
 		"disable_ipv6": true | false,
+		"fastest_addr": true | false, // use Fastest Address algorithm
+		"parallel_requests": true | false, // send DNS requests to all upstream servers at once
 	}
 
 
@@ -900,6 +905,9 @@ Request:
 	POST /control/dns_config
 
 	{
+		"upstream_dns": ["tls://...", ...],
+		"bootstrap_dns": ["1.2.3.4", ...],
+
 		"protection_enabled": true | false,
 		"ratelimit": 1234,
 		"blocking_mode": "default" | "nxdomain" | "null_ip" | "custom_ip",
@@ -908,6 +916,8 @@ Request:
 		"edns_cs_enabled": true | false,
 		"dnssec_enabled": true | false
 		"disable_ipv6": true | false,
+		"fastest_addr": true | false, // use Fastest Address algorithm
+		"parallel_requests": true | false, // send DNS requests to all upstream servers at once
 	}
 
 Response:
diff --git a/dnsforward/dnsforward.go b/dnsforward/dnsforward.go
index b191a966..4f630afb 100644
--- a/dnsforward/dnsforward.go
+++ b/dnsforward/dnsforward.go
@@ -141,6 +141,8 @@ type FilteringConfig struct {
 	// Respond with an empty answer to all AAAA requests
 	AAAADisabled bool `yaml:"aaaa_disabled"`
 
+	FastestAddrAlgo bool `yaml:"fastest_addr"` // use Fastest Address algorithm
+
 	AllowedClients    []string `yaml:"allowed_clients"`    // IP addresses of whitelist clients
 	DisallowedClients []string `yaml:"disallowed_clients"` // IP addresses of clients that should be blocked
 	BlockedHosts      []string `yaml:"blocked_hosts"`      // hosts that should be blocked
@@ -305,6 +307,7 @@ func (s *Server) Prepare(config *ServerConfig) error {
 		RequestHandler:           s.handleDNSRequest,
 		AllServers:               s.conf.AllServers,
 		EnableEDNSClientSubnet:   s.conf.EnableEDNSClientSubnet,
+		FindFastestAddr:          s.conf.FastestAddrAlgo,
 	}
 
 	intlProxyConfig := proxy.Config{
diff --git a/dnsforward/dnsforward_http.go b/dnsforward/dnsforward_http.go
index 414b728b..4658cbbf 100644
--- a/dnsforward/dnsforward_http.go
+++ b/dnsforward/dnsforward_http.go
@@ -22,6 +22,9 @@ func httpError(r *http.Request, w http.ResponseWriter, code int, format string,
 }
 
 type dnsConfigJSON struct {
+	Upstreams  []string `json:"upstream_dns"`
+	Bootstraps []string `json:"bootstrap_dns"`
+
 	ProtectionEnabled bool   `json:"protection_enabled"`
 	RateLimit         uint32 `json:"ratelimit"`
 	BlockingMode      string `json:"blocking_mode"`
@@ -30,11 +33,16 @@ type dnsConfigJSON struct {
 	EDNSCSEnabled     bool   `json:"edns_cs_enabled"`
 	DNSSECEnabled     bool   `json:"dnssec_enabled"`
 	DisableIPv6       bool   `json:"disable_ipv6"`
+	FastestAddr       bool   `json:"fastest_addr"`
+	ParallelRequests  bool   `json:"parallel_requests"`
 }
 
 func (s *Server) handleGetConfig(w http.ResponseWriter, r *http.Request) {
 	resp := dnsConfigJSON{}
 	s.RLock()
+	resp.Upstreams = stringArrayDup(s.conf.UpstreamDNS)
+	resp.Bootstraps = stringArrayDup(s.conf.BootstrapDNS)
+
 	resp.ProtectionEnabled = s.conf.ProtectionEnabled
 	resp.BlockingMode = s.conf.BlockingMode
 	resp.BlockingIPv4 = s.conf.BlockingIPv4
@@ -43,6 +51,8 @@ func (s *Server) handleGetConfig(w http.ResponseWriter, r *http.Request) {
 	resp.EDNSCSEnabled = s.conf.EnableEDNSClientSubnet
 	resp.DNSSECEnabled = s.conf.EnableDNSSEC
 	resp.DisableIPv6 = s.conf.AAAADisabled
+	resp.FastestAddr = s.conf.FastestAddrAlgo
+	resp.ParallelRequests = s.conf.AllServers
 	s.RUnlock()
 
 	js, err := json.Marshal(resp)
@@ -75,6 +85,7 @@ func checkBlockingMode(req dnsConfigJSON) bool {
 	return true
 }
 
+// nolint(gocyclo) - we need to check each JSON field separately
 func (s *Server) handleSetConfig(w http.ResponseWriter, r *http.Request) {
 	req := dnsConfigJSON{}
 	js, err := jsonutil.DecodeObject(&req, r.Body)
@@ -83,6 +94,25 @@ func (s *Server) handleSetConfig(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	if js.Exists("upstream_dns") {
+		if len(req.Upstreams) != 0 {
+			err = ValidateUpstreams(req.Upstreams)
+			if err != nil {
+				httpError(r, w, http.StatusBadRequest, "wrong upstreams specification: %s", err)
+				return
+			}
+		}
+	}
+
+	if js.Exists("bootstrap_dns") {
+		for _, host := range req.Bootstraps {
+			if err := checkPlainDNS(host); err != nil {
+				httpError(r, w, http.StatusBadRequest, "%s can not be used as bootstrap dns cause: %s", host, err)
+				return
+			}
+		}
+	}
+
 	if js.Exists("blocking_mode") && !checkBlockingMode(req) {
 		httpError(r, w, http.StatusBadRequest, "blocking_mode: incorrect value")
 		return
@@ -91,6 +121,16 @@ func (s *Server) handleSetConfig(w http.ResponseWriter, r *http.Request) {
 	restart := false
 	s.Lock()
 
+	if js.Exists("upstream_dns") {
+		s.conf.UpstreamDNS = req.Upstreams
+		restart = true
+	}
+
+	if js.Exists("bootstrap_dns") {
+		s.conf.BootstrapDNS = req.Bootstraps
+		restart = true
+	}
+
 	if js.Exists("protection_enabled") {
 		s.conf.ProtectionEnabled = req.ProtectionEnabled
 	}
@@ -129,6 +169,14 @@ func (s *Server) handleSetConfig(w http.ResponseWriter, r *http.Request) {
 		s.conf.AAAADisabled = req.DisableIPv6
 	}
 
+	if js.Exists("fastest_addr") {
+		s.conf.FastestAddrAlgo = req.FastestAddr
+	}
+
+	if js.Exists("parallel_requests") {
+		s.conf.AllServers = req.ParallelRequests
+	}
+
 	s.Unlock()
 	s.conf.ConfigModified()
 
@@ -144,51 +192,6 @@ func (s *Server) handleSetConfig(w http.ResponseWriter, r *http.Request) {
 type upstreamJSON struct {
 	Upstreams    []string `json:"upstream_dns"`  // Upstreams
 	BootstrapDNS []string `json:"bootstrap_dns"` // Bootstrap DNS
-	AllServers   bool     `json:"all_servers"`   // --all-servers param for dnsproxy
-}
-
-func (s *Server) handleSetUpstreamConfig(w http.ResponseWriter, r *http.Request) {
-	req := upstreamJSON{}
-	err := json.NewDecoder(r.Body).Decode(&req)
-	if err != nil {
-		httpError(r, w, http.StatusBadRequest, "Failed to parse new upstreams config json: %s", err)
-		return
-	}
-
-	if len(req.Upstreams) != 0 {
-		err = ValidateUpstreams(req.Upstreams)
-		if err != nil {
-			httpError(r, w, http.StatusBadRequest, "wrong upstreams specification: %s", err)
-			return
-		}
-	}
-
-	newconf := FilteringConfig{}
-	newconf.UpstreamDNS = req.Upstreams
-
-	// bootstrap servers are plain DNS only
-	for _, host := range req.BootstrapDNS {
-		if err := checkPlainDNS(host); err != nil {
-			httpError(r, w, http.StatusBadRequest, "%s can not be used as bootstrap dns cause: %s", host, err)
-			return
-		}
-	}
-	newconf.BootstrapDNS = req.BootstrapDNS
-
-	newconf.AllServers = req.AllServers
-
-	s.Lock()
-	s.conf.UpstreamDNS = newconf.UpstreamDNS
-	s.conf.BootstrapDNS = newconf.BootstrapDNS
-	s.conf.AllServers = newconf.AllServers
-	s.Unlock()
-	s.conf.ConfigModified()
-
-	err = s.Reconfigure(nil)
-	if err != nil {
-		httpError(r, w, http.StatusInternalServerError, "%s", err)
-		return
-	}
 }
 
 // ValidateUpstreams validates each upstream and returns an error if any upstream is invalid or if there are no default upstreams specified
@@ -399,7 +402,6 @@ func (s *Server) handleDOH(w http.ResponseWriter, r *http.Request) {
 func (s *Server) registerHandlers() {
 	s.conf.HTTPRegister("GET", "/control/dns_info", s.handleGetConfig)
 	s.conf.HTTPRegister("POST", "/control/dns_config", s.handleSetConfig)
-	s.conf.HTTPRegister("POST", "/control/set_upstreams_config", s.handleSetUpstreamConfig)
 	s.conf.HTTPRegister("POST", "/control/test_upstream_dns", s.handleTestUpstreamDNS)
 
 	s.conf.HTTPRegister("GET", "/control/access/list", s.handleAccessList)
diff --git a/home/control.go b/home/control.go
index 4d1dbe0c..310b9f20 100644
--- a/home/control.go
+++ b/home/control.go
@@ -55,9 +55,6 @@ func handleStatus(w http.ResponseWriter, r *http.Request) {
 		"language":      config.Language,
 
 		"protection_enabled": c.ProtectionEnabled,
-		"bootstrap_dns":      c.BootstrapDNS,
-		"upstream_dns":       c.UpstreamDNS,
-		"all_servers":        c.AllServers,
 	}
 
 	jsonVal, err := json.Marshal(data)
diff --git a/openapi/CHANGELOG.md b/openapi/CHANGELOG.md
index 877c1f7c..193b69fc 100644
--- a/openapi/CHANGELOG.md
+++ b/openapi/CHANGELOG.md
@@ -1,6 +1,70 @@
 # AdGuard Home API Change Log
 
 
+## v0.102: API changes
+
+### API: Get general status: GET /control/status
+
+* Removed "upstream_dns", "bootstrap_dns", "all_servers" parameters
+
+### API: Get DNS general settings: GET /control/dns_info
+
+* Added "parallel_requests", "upstream_dns", "bootstrap_dns" parameters
+
+Request:
+
+	GET /control/dns_info
+
+Response:
+
+	200 OK
+
+	{
+		"upstream_dns": ["tls://...", ...],
+		"bootstrap_dns": ["1.2.3.4", ...],
+
+		"protection_enabled": true | false,
+		"ratelimit": 1234,
+		"blocking_mode": "default" | "nxdomain" | "null_ip" | "custom_ip",
+		"blocking_ipv4": "1.2.3.4",
+		"blocking_ipv6": "1:2:3::4",
+		"edns_cs_enabled": true | false,
+		"dnssec_enabled": true | false
+		"disable_ipv6": true | false,
+		"fastest_addr": true | false, // use Fastest Address algorithm
+		"parallel_requests": true | false, // send DNS requests to all upstream servers at once
+	}
+
+### API: Set DNS general settings: POST /control/dns_config
+
+* Added "parallel_requests", "upstream_dns", "bootstrap_dns" parameters
+* removed /control/set_upstreams_config method
+
+Request:
+
+	POST /control/dns_config
+
+	{
+		"upstream_dns": ["tls://...", ...],
+		"bootstrap_dns": ["1.2.3.4", ...],
+
+		"protection_enabled": true | false,
+		"ratelimit": 1234,
+		"blocking_mode": "default" | "nxdomain" | "null_ip" | "custom_ip",
+		"blocking_ipv4": "1.2.3.4",
+		"blocking_ipv6": "1:2:3::4",
+		"edns_cs_enabled": true | false,
+		"dnssec_enabled": true | false
+		"disable_ipv6": true | false,
+		"fastest_addr": true | false, // use Fastest Address algorithm
+		"parallel_requests": true | false, // send DNS requests to all upstream servers at once
+	}
+
+Response:
+
+	200 OK
+
+
 ## v0.101: API changes
 
 ### API: Refresh filters: POST /control/filtering/refresh
diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml
index 9653af5a..a9791c4d 100644
--- a/openapi/openapi.yaml
+++ b/openapi/openapi.yaml
@@ -2,7 +2,7 @@ swagger: '2.0'
 info:
     title: 'AdGuard Home'
     description: 'AdGuard Home REST API. Admin web interface is built on top of this REST API.'
-    version: '0.101'
+    version: '0.102'
 schemes:
     - http
 basePath: /control
@@ -99,25 +99,6 @@ paths:
                 200:
                     description: OK
 
-    /set_upstreams_config:
-        post:
-            tags:
-                - global
-            operationId: setUpstreamsConfig
-            summary: "Updates the current upstreams configuration"
-            consumes:
-                - application/json
-            parameters:
-                - in: "body"
-                  name: "body"
-                  description: "Upstreams configuration JSON"
-                  required: true
-                  schema:
-                      $ref: "#/definitions/UpstreamsConfig"
-            responses:
-                200:
-                    description: OK
-
     /test_upstream_dns:
         post:
             tags:
@@ -1072,16 +1053,6 @@ definitions:
                 type: "boolean"
             running:
                 type: "boolean"
-            bootstrap_dns:
-                type: "string"
-                example: "8.8.8.8:53"
-            upstream_dns:
-                type: "array"
-                items:
-                    type: "string"
-                example:
-                  - "tls://1.1.1.1"
-                  - "tls://1.0.0.1"
             version:
                 type: "string"
                 example: "0.1"
@@ -1093,6 +1064,22 @@ definitions:
         type: "object"
         description: "Query log configuration"
         properties:
+            bootstrap_dns:
+                type: "array"
+                description: 'Bootstrap servers, port is optional after colon. Empty value will reset it to default values'
+                items:
+                    type: "string"
+                example:
+                    - "8.8.8.8:53"
+                    - "1.1.1.1:53"
+            upstream_dns:
+                type: "array"
+                description: 'Upstream servers, port is optional after colon. Empty value will reset it to default values'
+                items:
+                    type: "string"
+                example:
+                    - "tls://1.1.1.1"
+                    - "tls://1.0.0.1"
             protection_enabled:
                 type: "boolean"
             ratelimit:
@@ -1112,6 +1099,11 @@ definitions:
                 type: "boolean"
             dnssec_enabled:
                 type: "boolean"
+            fastest_addr:
+                type: "boolean"
+            parallel_requests:
+                type: "boolean"
+                description: "If true, parallel queries to all configured upstream servers are enabled"
 
     UpstreamsConfig:
         type: "object"
@@ -1119,7 +1111,6 @@ definitions:
         required:
             - "bootstrap_dns"
             - "upstream_dns"
-            - "all_servers"
         properties:
             bootstrap_dns:
                 type: "array"
@@ -1137,9 +1128,7 @@ definitions:
                 example:
                     - "tls://1.1.1.1"
                     - "tls://1.0.0.1"
-            all_servers:
-                type: "boolean"
-                description: "If true, parallel queries to all configured upstream servers are enabled"
+
     Filter:
         type: "object"
         description: "Filter subscription info"