diff --git a/client/src/components/Settings/Dns/Upstream/index.js b/client/src/components/Settings/Dns/Upstream/index.js
index 0342713e..c30211ca 100644
--- a/client/src/components/Settings/Dns/Upstream/index.js
+++ b/client/src/components/Settings/Dns/Upstream/index.js
@@ -12,6 +12,8 @@ const Upstream = () => {
upstream_dns,
bootstrap_dns,
upstream_mode,
+ resolve_clients,
+ local_ptr_upstreams,
} = useSelector((state) => state.dnsConfig, shallowEqual);
const upstream_dns_file = useSelector((state) => state.dnsConfig.upstream_dns_file);
@@ -21,11 +23,15 @@ const Upstream = () => {
bootstrap_dns,
upstream_dns,
upstream_mode,
+ resolve_clients,
+ local_ptr_upstreams,
} = values;
const dnsConfig = {
bootstrap_dns,
upstream_mode,
+ resolve_clients,
+ local_ptr_upstreams,
...(upstream_dns_file ? null : { upstream_dns }),
};
@@ -45,6 +51,8 @@ const Upstream = () => {
upstream_dns: upstreamDns,
bootstrap_dns,
upstream_mode,
+ resolve_clients,
+ local_ptr_upstreams,
}}
onSubmit={handleSubmit}
/>
diff --git a/client/src/reducers/dnsConfig.js b/client/src/reducers/dnsConfig.js
index bbe4ad2f..fbc3afdb 100644
--- a/client/src/reducers/dnsConfig.js
+++ b/client/src/reducers/dnsConfig.js
@@ -16,6 +16,7 @@ const dnsConfig = handleActions(
blocking_ipv6,
upstream_dns,
bootstrap_dns,
+ local_ptr_upstreams,
...values
} = payload;
@@ -26,6 +27,7 @@ const dnsConfig = handleActions(
blocking_ipv6: blocking_ipv6 || DEFAULT_BLOCKING_IPV6,
upstream_dns: (upstream_dns && upstream_dns.join('\n')) || '',
bootstrap_dns: (bootstrap_dns && bootstrap_dns.join('\n')) || '',
+ local_ptr_upstreams: (local_ptr_upstreams && local_ptr_upstreams.join('\n')) || '',
processingGetConfig: false,
};
},
diff --git a/internal/agherr/agherr.go b/internal/agherr/agherr.go
index fd3cd830..0a9f1b6d 100644
--- a/internal/agherr/agherr.go
+++ b/internal/agherr/agherr.go
@@ -46,7 +46,8 @@ func (e *manyError) Error() (msg string) {
b := &strings.Builder{}
// Ignore errors, since strings.(*Buffer).Write never returns
- // errors.
+ // errors. We don't use aghstrings.WriteToBuilder here since
+ // this package should be importable for any other.
_, _ = fmt.Fprintf(b, "%s: %s (hidden: %s", e.message, e.underlying[0], e.underlying[1])
for _, u := range e.underlying[2:] {
// See comment above.
diff --git a/internal/aghnet/exchanger.go b/internal/aghnet/exchanger.go
index 2ddeb7ad..c148e290 100644
--- a/internal/aghnet/exchanger.go
+++ b/internal/aghnet/exchanger.go
@@ -31,7 +31,11 @@ type multiAddrExchanger struct {
// NewMultiAddrExchanger creates an Exchanger instance from passed addresses.
// It returns an error if any of addrs failed to become an upstream.
-func NewMultiAddrExchanger(addrs []string, timeout time.Duration) (e Exchanger, err error) {
+func NewMultiAddrExchanger(
+ addrs []string,
+ bootstraps []string,
+ timeout time.Duration,
+) (e Exchanger, err error) {
defer agherr.Annotate("exchanger: %w", &err)
if len(addrs) == 0 {
@@ -41,7 +45,10 @@ func NewMultiAddrExchanger(addrs []string, timeout time.Duration) (e Exchanger,
var ups []upstream.Upstream = make([]upstream.Upstream, 0, len(addrs))
for _, addr := range addrs {
var u upstream.Upstream
- u, err = upstream.AddressToUpstream(addr, upstream.Options{Timeout: timeout})
+ u, err = upstream.AddressToUpstream(addr, upstream.Options{
+ Bootstrap: bootstraps,
+ Timeout: timeout,
+ })
if err != nil {
return nil, err
}
diff --git a/internal/aghnet/exchanger_test.go b/internal/aghnet/exchanger_test.go
index 774bec86..ace4b76b 100644
--- a/internal/aghnet/exchanger_test.go
+++ b/internal/aghnet/exchanger_test.go
@@ -15,19 +15,19 @@ func TestNewMultiAddrExchanger(t *testing.T) {
var err error
t.Run("empty", func(t *testing.T) {
- e, err = NewMultiAddrExchanger([]string{}, 0)
+ e, err = NewMultiAddrExchanger([]string{}, nil, 0)
require.NoError(t, err)
assert.NotNil(t, e)
})
t.Run("successful", func(t *testing.T) {
- e, err = NewMultiAddrExchanger([]string{"www.example.com"}, 0)
+ e, err = NewMultiAddrExchanger([]string{"www.example.com"}, nil, 0)
require.NoError(t, err)
assert.NotNil(t, e)
})
t.Run("unsuccessful", func(t *testing.T) {
- e, err = NewMultiAddrExchanger([]string{"invalid-proto://www.example.com"}, 0)
+ e, err = NewMultiAddrExchanger([]string{"invalid-proto://www.example.com"}, nil, 0)
require.Error(t, err)
assert.Nil(t, e)
})
diff --git a/internal/aghnet/net.go b/internal/aghnet/net.go
index fd36fe24..d23a17f7 100644
--- a/internal/aghnet/net.go
+++ b/internal/aghnet/net.go
@@ -15,6 +15,7 @@ import (
"time"
"github.com/AdguardTeam/AdGuardHome/internal/agherr"
+ "github.com/AdguardTeam/AdGuardHome/internal/aghstrings"
"github.com/AdguardTeam/golibs/log"
)
@@ -355,30 +356,30 @@ const (
// (PTR) record lookups. This is the modified version of ReverseAddr from
// github.com/miekg/dns package with no error among returned values.
func ReverseAddr(ip net.IP) (arpa string) {
+ const dot = "."
+
var strLen int
var suffix string
- // Don't handle errors in implementations since strings.WriteString
- // never returns non-nil errors.
var writeByte func(val byte)
b := &strings.Builder{}
if ip4 := ip.To4(); ip4 != nil {
strLen, suffix = arpaV4MaxLen, arpaV4Suffix[1:]
ip = ip4
writeByte = func(val byte) {
- _, _ = b.WriteString(strconv.Itoa(int(val)))
- _, _ = b.WriteRune('.')
+ aghstrings.WriteToBuilder(b, strconv.Itoa(int(val)), dot)
}
} else if ip6 := ip.To16(); ip6 != nil {
strLen, suffix = arpaV6MaxLen, arpaV6Suffix[1:]
ip = ip6
writeByte = func(val byte) {
- lByte, rByte := val&0xF, val>>4
-
- _, _ = b.WriteString(strconv.FormatUint(uint64(lByte), 16))
- _, _ = b.WriteRune('.')
- _, _ = b.WriteString(strconv.FormatUint(uint64(rByte), 16))
- _, _ = b.WriteRune('.')
+ aghstrings.WriteToBuilder(
+ b,
+ strconv.FormatUint(uint64(val&0xF), 16),
+ dot,
+ strconv.FormatUint(uint64(val>>4), 16),
+ dot,
+ )
}
} else {
@@ -389,7 +390,38 @@ func ReverseAddr(ip net.IP) (arpa string) {
for i := len(ip) - 1; i >= 0; i-- {
writeByte(ip[i])
}
- _, _ = b.WriteString(suffix)
+ aghstrings.WriteToBuilder(b, suffix)
return b.String()
}
+
+// CollectAllIfacesAddrs returns the slice of all network interfaces IP
+// addresses without port number.
+func CollectAllIfacesAddrs() (addrs []string, err error) {
+ var ifaces []net.Interface
+ ifaces, err = net.Interfaces()
+ if err != nil {
+ return nil, fmt.Errorf("getting network interfaces: %w", err)
+ }
+
+ for _, iface := range ifaces {
+ var ifaceAddrs []net.Addr
+ ifaceAddrs, err = iface.Addrs()
+ if err != nil {
+ return nil, fmt.Errorf("getting addresses for %q: %w", iface.Name, err)
+ }
+
+ for _, addr := range ifaceAddrs {
+ cidr := addr.String()
+ var ip net.IP
+ ip, _, err = net.ParseCIDR(cidr)
+ if err != nil {
+ return nil, fmt.Errorf("parsing cidr: %w", err)
+ }
+
+ addrs = append(addrs, ip.String())
+ }
+ }
+
+ return addrs, nil
+}
diff --git a/internal/aghstrings/strings.go b/internal/aghstrings/strings.go
new file mode 100644
index 00000000..f42dded6
--- /dev/null
+++ b/internal/aghstrings/strings.go
@@ -0,0 +1,71 @@
+// Package aghstrings contains utilities dealing with strings.
+package aghstrings
+
+import (
+ "strings"
+)
+
+// CloneSliceOrEmpty returns the copy of a or empty strings slice if a is nil.
+func CloneSliceOrEmpty(a []string) (b []string) {
+ return append([]string{}, a...)
+}
+
+// CloneSlice returns the exact copy of a.
+func CloneSlice(a []string) (b []string) {
+ if a == nil {
+ return nil
+ }
+
+ return CloneSliceOrEmpty(a)
+}
+
+// InSlice checks if string is in the slice of strings.
+func InSlice(strs []string, str string) (ok bool) {
+ for _, s := range strs {
+ if s == str {
+ return true
+ }
+ }
+
+ return false
+}
+
+// SplitNext splits string by a byte and returns the first chunk skipping empty
+// ones. Whitespaces are trimmed.
+func SplitNext(s *string, sep rune) (chunk string) {
+ if s == nil {
+ return chunk
+ }
+
+ i := strings.IndexByte(*s, byte(sep))
+ if i == -1 {
+ chunk = *s
+ *s = ""
+
+ return strings.TrimSpace(chunk)
+ }
+
+ chunk = (*s)[:i]
+ *s = (*s)[i+1:]
+ var j int
+ var r rune
+ for j, r = range *s {
+ if r != sep {
+ break
+ }
+ }
+
+ *s = (*s)[j:]
+
+ return strings.TrimSpace(chunk)
+}
+
+// WriteToBuilder is a convenient wrapper for strings.(*Builder).WriteString
+// that deals with multiple strings and ignores errors that are guaranteed to be
+// nil.
+func WriteToBuilder(b *strings.Builder, strs ...string) {
+ // TODO(e.burkov): Recover from panic?
+ for _, s := range strs {
+ _, _ = b.WriteString(s)
+ }
+}
diff --git a/internal/aghstrings/strings_test.go b/internal/aghstrings/strings_test.go
new file mode 100644
index 00000000..304e0164
--- /dev/null
+++ b/internal/aghstrings/strings_test.go
@@ -0,0 +1,114 @@
+package aghstrings
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestCloneSlice_family(t *testing.T) {
+ a := []string{"1", "2", "3"}
+
+ t.Run("cloneslice_simple", func(t *testing.T) {
+ assert.Equal(t, a, CloneSlice(a))
+ })
+
+ t.Run("cloneslice_nil", func(t *testing.T) {
+ assert.Nil(t, CloneSlice(nil))
+ })
+
+ t.Run("cloneslice_empty", func(t *testing.T) {
+ assert.Equal(t, []string{}, CloneSlice([]string{}))
+ })
+
+ t.Run("clonesliceorempty_nil", func(t *testing.T) {
+ assert.Equal(t, []string{}, CloneSliceOrEmpty(nil))
+ })
+
+ t.Run("clonesliceorempty_empty", func(t *testing.T) {
+ assert.Equal(t, []string{}, CloneSliceOrEmpty([]string{}))
+ })
+
+ t.Run("clonesliceorempty_sameness", func(t *testing.T) {
+ assert.Equal(t, CloneSlice(a), CloneSliceOrEmpty(a))
+ })
+}
+
+func TestInSlice(t *testing.T) {
+ simpleStrs := []string{"1", "2", "3"}
+
+ testCases := []struct {
+ name string
+ str string
+ strs []string
+ want bool
+ }{{
+ name: "yes",
+ str: "2",
+ strs: simpleStrs,
+ want: true,
+ }, {
+ name: "no",
+ str: "4",
+ strs: simpleStrs,
+ want: false,
+ }, {
+ name: "nil",
+ str: "any",
+ strs: nil,
+ want: false,
+ }}
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ assert.Equal(t, tc.want, InSlice(tc.strs, tc.str))
+ })
+ }
+}
+
+func TestSplitNext(t *testing.T) {
+ t.Run("ordinary", func(t *testing.T) {
+ s := " a,b , c "
+ require.Equal(t, "a", SplitNext(&s, ','))
+ require.Equal(t, "b", SplitNext(&s, ','))
+ require.Equal(t, "c", SplitNext(&s, ','))
+
+ assert.Empty(t, s)
+ })
+
+ t.Run("nil_source", func(t *testing.T) {
+ assert.Equal(t, "", SplitNext(nil, 's'))
+ })
+}
+
+func TestWriteToBuilder(t *testing.T) {
+ b := &strings.Builder{}
+
+ t.Run("single", func(t *testing.T) {
+ assert.NotPanics(t, func() { WriteToBuilder(b, t.Name()) })
+ assert.Equal(t, t.Name(), b.String())
+ })
+
+ b.Reset()
+ t.Run("several", func(t *testing.T) {
+ const (
+ _1 = "one"
+ _2 = "two"
+ _123 = _1 + _2
+ )
+ assert.NotPanics(t, func() { WriteToBuilder(b, _1, _2) })
+ assert.Equal(t, _123, b.String())
+ })
+
+ b.Reset()
+ t.Run("nothing", func(t *testing.T) {
+ assert.NotPanics(t, func() { WriteToBuilder(b) })
+ assert.Equal(t, "", b.String())
+ })
+
+ t.Run("nil_builder", func(t *testing.T) {
+ assert.Panics(t, func() { WriteToBuilder(nil, "a") })
+ })
+}
diff --git a/internal/aghtest/exchanger.go b/internal/aghtest/exchanger.go
index d68a3566..2c617814 100644
--- a/internal/aghtest/exchanger.go
+++ b/internal/aghtest/exchanger.go
@@ -11,10 +11,10 @@ type Exchanger struct {
}
// Exchange implements aghnet.Exchanger interface for *Exchanger.
-func (lr *Exchanger) Exchange(req *dns.Msg) (resp *dns.Msg, err error) {
- if lr.Ups == nil {
- lr.Ups = &TestErrUpstream{}
+func (e *Exchanger) Exchange(req *dns.Msg) (resp *dns.Msg, err error) {
+ if e.Ups == nil {
+ e.Ups = &TestErrUpstream{}
}
- return lr.Ups.Exchange(req)
+ return e.Ups.Exchange(req)
}
diff --git a/internal/dnsfilter/dnsfilter.go b/internal/dnsfilter/dnsfilter.go
index 0660f7ae..b306c16f 100644
--- a/internal/dnsfilter/dnsfilter.go
+++ b/internal/dnsfilter/dnsfilter.go
@@ -75,7 +75,7 @@ type Config struct {
HTTPRegister func(string, string, func(http.ResponseWriter, *http.Request)) `yaml:"-"`
// CustomResolver is the resolver used by DNSFilter.
- CustomResolver Resolver
+ CustomResolver Resolver `yaml:"-"`
}
// LookupStats store stats collected during safebrowsing or parental checks
diff --git a/internal/dnsfilter/safebrowsing.go b/internal/dnsfilter/safebrowsing.go
index 2ad2db42..d8de2419 100644
--- a/internal/dnsfilter/safebrowsing.go
+++ b/internal/dnsfilter/safebrowsing.go
@@ -13,6 +13,7 @@ import (
"strings"
"time"
+ "github.com/AdguardTeam/AdGuardHome/internal/aghstrings"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/cache"
"github.com/AdguardTeam/golibs/log"
@@ -181,26 +182,21 @@ func hostnameToHashes(host string) map[[32]byte]string {
// convert hash array to string
func (c *sbCtx) getQuestion() string {
b := &strings.Builder{}
- encoder := hex.NewEncoder(b)
for hash := range c.hashToHost {
- // Ignore errors, since strings.(*Buffer).Write never returns
- // errors.
- //
// TODO(e.burkov, a.garipov): Find out and document why exactly
// this slice.
- _, _ = encoder.Write(hash[0:2])
- _, _ = b.WriteRune('.')
+ aghstrings.WriteToBuilder(b, hex.EncodeToString(hash[0:2]), ".")
}
if c.svc == "SafeBrowsing" {
- // See comment above.
- _, _ = b.WriteString(sbTXTSuffix)
+ aghstrings.WriteToBuilder(b, sbTXTSuffix)
+
return b.String()
}
- // See comment above.
- _, _ = b.WriteString(pcTXTSuffix)
+ aghstrings.WriteToBuilder(b, pcTXTSuffix)
+
return b.String()
}
diff --git a/internal/dnsforward/access.go b/internal/dnsforward/access.go
index 8afae955..c3e5aa7c 100644
--- a/internal/dnsforward/access.go
+++ b/internal/dnsforward/access.go
@@ -8,6 +8,7 @@ import (
"strings"
"sync"
+ "github.com/AdguardTeam/AdGuardHome/internal/aghstrings"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/urlfilter"
"github.com/AdguardTeam/urlfilter/filterlist"
@@ -36,16 +37,15 @@ func (a *accessCtx) Init(allowedClients, disallowedClients, blockedHosts []strin
return err
}
- buf := strings.Builder{}
+ b := &strings.Builder{}
for _, s := range blockedHosts {
- buf.WriteString(s)
- buf.WriteString("\n")
+ aghstrings.WriteToBuilder(b, s, "\n")
}
listArray := []filterlist.RuleList{}
list := &filterlist.StringRuleList{
ID: int(0),
- RulesText: buf.String(),
+ RulesText: b.String(),
IgnoreCosmetic: true,
}
listArray = append(listArray, list)
diff --git a/internal/dnsforward/config.go b/internal/dnsforward/config.go
index 613a0102..edc6f4f1 100644
--- a/internal/dnsforward/config.go
+++ b/internal/dnsforward/config.go
@@ -10,8 +10,8 @@ import (
"net/http"
"sort"
+ "github.com/AdguardTeam/AdGuardHome/internal/aghstrings"
"github.com/AdguardTeam/AdGuardHome/internal/dnsfilter"
- "github.com/AdguardTeam/AdGuardHome/internal/util"
"github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/log"
@@ -149,6 +149,13 @@ type ServerConfig struct {
// Register an HTTP handler
HTTPRegister func(string, string, func(http.ResponseWriter, *http.Request))
+
+ // ResolveClients signals if the RDNS should resolve clients' addresses.
+ ResolveClients bool
+
+ // LocalPTRResolvers is a slice of addresses to be used as upstreams for
+ // resolving PTR queries for local addresses.
+ LocalPTRResolvers []string
}
// if any of ServerConfig values are zero, then default values from below are used
@@ -274,7 +281,7 @@ func (s *Server) prepareUpstreamSettings() error {
}
d := string(data)
for len(d) != 0 {
- s := util.SplitNext(&d, '\n')
+ s := aghstrings.SplitNext(&d, '\n')
upstreams = append(upstreams, s)
}
log.Debug("dns: using %d upstream servers from file %s", len(upstreams), s.conf.UpstreamDNSFileName)
diff --git a/internal/dnsforward/dns.go b/internal/dnsforward/dns.go
index 4fffcc21..a93ab0bd 100644
--- a/internal/dnsforward/dns.go
+++ b/internal/dnsforward/dns.go
@@ -293,6 +293,14 @@ func (s *Server) processRestrictLocal(ctx *dnsContext) (rc resultCode) {
// Do not perform unreversing ever again.
ctx.unreversedReqIP = ip
+ // Disable redundant filtering.
+ filterSetts := s.getClientRequestFilteringSettings(ctx)
+ filterSetts.ParentalEnabled = false
+ filterSetts.SafeBrowsingEnabled = false
+ filterSetts.SafeSearchEnabled = false
+ filterSetts.ServicesRules = nil
+ ctx.setts = filterSetts
+
// Nothing to restrict.
return resultCodeSuccess
}
@@ -405,15 +413,19 @@ func processFilteringBeforeRequest(ctx *dnsContext) (rc resultCode) {
var err error
ctx.protectionEnabled = s.conf.ProtectionEnabled && s.dnsFilter != nil
if ctx.protectionEnabled {
- ctx.setts = s.getClientRequestFilteringSettings(ctx)
+ if ctx.setts == nil {
+ ctx.setts = s.getClientRequestFilteringSettings(ctx)
+ }
ctx.result, err = s.filterDNSRequest(ctx)
}
s.RUnlock()
if err != nil {
ctx.err = err
+
return resultCodeError
}
+
return resultCodeSuccess
}
diff --git a/internal/dnsforward/dnsforward.go b/internal/dnsforward/dnsforward.go
index 3dfd1e37..ab65a935 100644
--- a/internal/dnsforward/dnsforward.go
+++ b/internal/dnsforward/dnsforward.go
@@ -8,10 +8,13 @@ import (
"net/http"
"os"
"runtime"
+ "strings"
"sync"
"time"
+ "github.com/AdguardTeam/AdGuardHome/internal/agherr"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
+ "github.com/AdguardTeam/AdGuardHome/internal/aghstrings"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
"github.com/AdguardTeam/AdGuardHome/internal/dnsfilter"
"github.com/AdguardTeam/AdGuardHome/internal/querylog"
@@ -92,7 +95,6 @@ type DNSCreateParams struct {
QueryLog querylog.QueryLog
DHCPServer dhcpd.ServerInterface
SubnetDetector *aghnet.SubnetDetector
- LocalResolvers aghnet.Exchanger
AutohostTLD string
}
@@ -127,7 +129,6 @@ func NewServer(p DNSCreateParams) (s *Server, err error) {
stats: p.Stats,
queryLog: p.QueryLog,
subnetDetector: p.SubnetDetector,
- localResolvers: p.LocalResolvers,
autohostSuffix: autohostSuffix,
}
@@ -176,15 +177,23 @@ func (s *Server) WriteDiskConfig(c *FilteringConfig) {
s.RLock()
sc := s.conf.FilteringConfig
*c = sc
- c.RatelimitWhitelist = stringArrayDup(sc.RatelimitWhitelist)
- c.BootstrapDNS = stringArrayDup(sc.BootstrapDNS)
- c.AllowedClients = stringArrayDup(sc.AllowedClients)
- c.DisallowedClients = stringArrayDup(sc.DisallowedClients)
- c.BlockedHosts = stringArrayDup(sc.BlockedHosts)
- c.UpstreamDNS = stringArrayDup(sc.UpstreamDNS)
+ c.RatelimitWhitelist = aghstrings.CloneSlice(sc.RatelimitWhitelist)
+ c.BootstrapDNS = aghstrings.CloneSlice(sc.BootstrapDNS)
+ c.AllowedClients = aghstrings.CloneSlice(sc.AllowedClients)
+ c.DisallowedClients = aghstrings.CloneSlice(sc.DisallowedClients)
+ c.BlockedHosts = aghstrings.CloneSlice(sc.BlockedHosts)
+ c.UpstreamDNS = aghstrings.CloneSlice(sc.UpstreamDNS)
s.RUnlock()
}
+// RDNSSettings returns the copy of actual RDNS configuration.
+func (s *Server) RDNSSettings() (localPTRResolvers []string, resolveClients bool) {
+ s.RLock()
+ defer s.RUnlock()
+
+ return aghstrings.CloneSlice(s.conf.LocalPTRResolvers), s.conf.ResolveClients
+}
+
// Resolve - get IP addresses by host name from an upstream server.
// No request/response filtering is performed.
// Query log and Stats are not updated.
@@ -195,24 +204,73 @@ func (s *Server) Resolve(host string) ([]net.IPAddr, error) {
return s.internalProxy.LookupIPAddr(host)
}
-// Exchange - send DNS request to an upstream server and receive response
-// No request/response filtering is performed.
-// Query log and Stats are not updated.
-// This method may be called before Start().
-func (s *Server) Exchange(req *dns.Msg) (*dns.Msg, error) {
+// RDNSExchanger is a resolver for clients' addresses.
+type RDNSExchanger interface {
+ // Exchange tries to resolve the ip in a suitable way, e.g. either as
+ // local or as external.
+ Exchange(ip net.IP) (host string, err error)
+}
+
+const (
+ // rDNSEmptyAnswerErr is returned by Exchange method when the answer
+ // section of respond is empty.
+ rDNSEmptyAnswerErr agherr.Error = "the answer section is empty"
+
+ // rDNSNotPTRErr is returned by Exchange method when the response is not
+ // of PTR type.
+ rDNSNotPTRErr agherr.Error = "the response is not a ptr"
+)
+
+// Exchange implements the RDNSExchanger interface for *Server.
+func (s *Server) Exchange(ip net.IP) (host string, err error) {
s.RLock()
defer s.RUnlock()
- ctx := &proxy.DNSContext{
- Proto: "udp",
- Req: req,
- StartTime: time.Now(),
+ if !s.conf.ResolveClients {
+ return "", nil
+ }
+
+ arpa := dns.Fqdn(aghnet.ReverseAddr(ip))
+ req := &dns.Msg{
+ MsgHdr: dns.MsgHdr{
+ Id: dns.Id(),
+ RecursionDesired: true,
+ },
+ Compress: true,
+ Question: []dns.Question{{
+ Name: arpa,
+ Qtype: dns.TypePTR,
+ Qclass: dns.ClassINET,
+ }},
+ }
+
+ var resp *dns.Msg
+ if s.subnetDetector.IsLocallyServedNetwork(ip) {
+ resp, err = s.localResolvers.Exchange(req)
+ } else {
+ ctx := &proxy.DNSContext{
+ Proto: "udp",
+ Req: req,
+ StartTime: time.Now(),
+ }
+ err = s.internalProxy.Resolve(ctx)
+
+ resp = ctx.Res
}
- err := s.internalProxy.Resolve(ctx)
if err != nil {
- return nil, err
+ return "", err
}
- return ctx.Res, nil
+
+ if len(resp.Answer) == 0 {
+ return "", fmt.Errorf("lookup for %q: %w", arpa, rDNSEmptyAnswerErr)
+ }
+
+ ptr, ok := resp.Answer[0].(*dns.PTR)
+ if !ok {
+ return "", fmt.Errorf("type checking: %w", rDNSNotPTRErr)
+ }
+
+ return strings.TrimSuffix(ptr.Ptr, "."), nil
}
// Start starts the DNS server.
@@ -231,6 +289,110 @@ func (s *Server) startLocked() error {
return err
}
+// defaultLocalTimeout is the default timeout for resolving addresses from
+// locally-served networks. It is assumed that local resolvers should work much
+// faster than ordinary upstreams.
+const defaultLocalTimeout = 1 * time.Second
+
+// collectDNSIPAddrs returns the slice of IP addresses without port number which
+// we are listening on. For internal use only.
+func (s *Server) collectDNSIPAddrs() (addrs []string, err error) {
+ addrs = make([]string, len(s.conf.TCPListenAddrs)+len(s.conf.UDPListenAddrs))
+ var i int
+ var ip net.IP
+ for _, addr := range s.conf.TCPListenAddrs {
+ if addr == nil {
+ continue
+ }
+
+ if ip = addr.IP; ip.IsUnspecified() {
+ return aghnet.CollectAllIfacesAddrs()
+ }
+
+ addrs[i] = ip.String()
+ i++
+ }
+ for _, addr := range s.conf.UDPListenAddrs {
+ if addr == nil {
+ continue
+ }
+
+ if ip = addr.IP; ip.IsUnspecified() {
+ return aghnet.CollectAllIfacesAddrs()
+ }
+
+ addrs[i] = ip.String()
+ i++
+ }
+
+ return addrs[:i], nil
+}
+
+// stringSetSubtract subtracts b from a interpreted as sets.
+func stringSetSubtract(a, b []string) (c []string) {
+ // unit is an object to be used as value in set.
+ type unit = struct{}
+
+ cSet := make(map[string]unit)
+ for _, k := range a {
+ cSet[k] = unit{}
+ }
+
+ for _, k := range b {
+ delete(cSet, k)
+ }
+
+ c = make([]string, len(cSet))
+ i := 0
+ for k := range cSet {
+ c[i] = k
+ i++
+ }
+
+ return c
+}
+
+// setupResolvers initializes the resolvers for local addresses. For internal
+// use only.
+func (s *Server) setupResolvers(localAddrs []string) (err error) {
+ bootstraps := s.conf.BootstrapDNS
+ if len(localAddrs) == 0 {
+ var sysRes aghnet.SystemResolvers
+ // TODO(e.burkov): Enable the refresher after the actual
+ // implementation passes the public testing.
+ sysRes, err = aghnet.NewSystemResolvers(0, nil)
+ if err != nil {
+ return err
+ }
+
+ localAddrs = sysRes.Get()
+ bootstraps = nil
+ }
+ log.Debug("upstreams to resolve PTR for local addresses: %v", localAddrs)
+
+ var ourAddrs []string
+ ourAddrs, err = s.collectDNSIPAddrs()
+ if err != nil {
+ return err
+ }
+
+ // TODO(e.burkov): The approach of subtracting sets of strings
+ // is not really applicable here since in case of listening on
+ // all network interfaces we should check the whole interface's
+ // network to cut off all the loopback addresses as well.
+ localAddrs = stringSetSubtract(localAddrs, ourAddrs)
+
+ if s.localResolvers, err = aghnet.NewMultiAddrExchanger(
+ localAddrs,
+ bootstraps,
+ defaultLocalTimeout,
+ ); err != nil {
+ return err
+ }
+
+ return nil
+}
+
// Prepare the object
func (s *Server) Prepare(config *ServerConfig) error {
// Initialize the server configuration
@@ -305,6 +467,12 @@ func (s *Server) Prepare(config *ServerConfig) error {
// Create the main DNS proxy instance
// --
s.dnsProxy = &proxy.Proxy{Config: proxyConfig}
+
+ err = s.setupResolvers(s.conf.LocalPTRResolvers)
+ if err != nil {
+ return fmt.Errorf("setting up resolvers: %w", err)
+ }
+
return nil
}
diff --git a/internal/dnsforward/dnsforward_test.go b/internal/dnsforward/dnsforward_test.go
index 91ecc158..6d16ac12 100644
--- a/internal/dnsforward/dnsforward_test.go
+++ b/internal/dnsforward/dnsforward_test.go
@@ -18,6 +18,7 @@ import (
"testing"
"time"
+ "github.com/AdguardTeam/AdGuardHome/internal/agherr"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
@@ -73,7 +74,6 @@ func createTestServer(t *testing.T, filterConf *dnsfilter.Config, forwardConf Se
s, err = NewServer(DNSCreateParams{
DNSFilter: f,
SubnetDetector: snd,
- LocalResolvers: &aghtest.Exchanger{},
})
require.NoError(t, err)
@@ -82,6 +82,11 @@ func createTestServer(t *testing.T, filterConf *dnsfilter.Config, forwardConf Se
err = s.Prepare(nil)
require.NoError(t, err)
+ s.Lock()
+ defer s.Unlock()
+
+ s.localResolvers = &aghtest.Exchanger{}
+
return s
}
@@ -728,7 +733,6 @@ func TestBlockedCustomIP(t *testing.T) {
s, err = NewServer(DNSCreateParams{
DNSFilter: dnsfilter.New(&dnsfilter.Config{}, filters),
SubnetDetector: snd,
- LocalResolvers: &aghtest.Exchanger{},
})
require.NoError(t, err)
@@ -866,7 +870,6 @@ func TestRewrite(t *testing.T) {
s, err = NewServer(DNSCreateParams{
DNSFilter: f,
SubnetDetector: snd,
- LocalResolvers: &aghtest.Exchanger{},
})
require.NoError(t, err)
@@ -1029,7 +1032,6 @@ func TestPTRResponseFromDHCPLeases(t *testing.T) {
DNSFilter: dnsfilter.New(&dnsfilter.Config{}, nil),
DHCPServer: &testDHCP{},
SubnetDetector: snd,
- LocalResolvers: &aghtest.Exchanger{},
})
require.NoError(t, err)
@@ -1094,7 +1096,6 @@ func TestPTRResponseFromHosts(t *testing.T) {
s, err = NewServer(DNSCreateParams{
DNSFilter: dnsfilter.New(&c, nil),
SubnetDetector: snd,
- LocalResolvers: &aghtest.Exchanger{},
})
require.NoError(t, err)
@@ -1164,3 +1165,100 @@ func TestNewServer(t *testing.T) {
})
}
}
+
+func TestServer_Exchange(t *testing.T) {
+ extUpstream := &aghtest.TestUpstream{
+ Reverse: map[string][]string{
+ "1.1.1.1.in-addr.arpa.": {"one.one.one.one"},
+ },
+ }
+ locUpstream := &aghtest.TestUpstream{
+ Reverse: map[string][]string{
+ "1.1.168.192.in-addr.arpa.": {"local.domain"},
+ "2.1.168.192.in-addr.arpa.": {},
+ },
+ }
+ upstreamErr := agherr.Error("upstream error")
+ errUpstream := &aghtest.TestErrUpstream{
+ Err: upstreamErr,
+ }
+ nonPtrUpstream := &aghtest.TestBlockUpstream{
+ Hostname: "some-host",
+ Block: true,
+ }
+
+ dns := NewCustomServer(&proxy.Proxy{
+ Config: proxy.Config{
+ UpstreamConfig: &proxy.UpstreamConfig{
+ Upstreams: []upstream.Upstream{extUpstream},
+ },
+ },
+ })
+ dns.conf.ResolveClients = true
+
+ var err error
+ dns.subnetDetector, err = aghnet.NewSubnetDetector()
+ require.NoError(t, err)
+
+ localIP := net.IP{192, 168, 1, 1}
+ testCases := []struct {
+ name string
+ want string
+ wantErr error
+ locUpstream upstream.Upstream
+ req net.IP
+ }{{
+ name: "external_good",
+ want: "one.one.one.one",
+ wantErr: nil,
+ locUpstream: nil,
+ req: net.IP{1, 1, 1, 1},
+ }, {
+ name: "local_good",
+ want: "local.domain",
+ wantErr: nil,
+ locUpstream: locUpstream,
+ req: localIP,
+ }, {
+ name: "upstream_error",
+ want: "",
+ wantErr: upstreamErr,
+ locUpstream: errUpstream,
+ req: localIP,
+ }, {
+ name: "empty_answer_error",
+ want: "",
+ wantErr: rDNSEmptyAnswerErr,
+ locUpstream: locUpstream,
+ req: net.IP{192, 168, 1, 2},
+ }, {
+ name: "not_ptr_error",
+ want: "",
+ wantErr: rDNSNotPTRErr,
+ locUpstream: nonPtrUpstream,
+ req: localIP,
+ }}
+
+ for _, tc := range testCases {
+ dns.localResolvers = &aghtest.Exchanger{
+ Ups: tc.locUpstream,
+ }
+
+ t.Run(tc.name, func(t *testing.T) {
+ host, eerr := dns.Exchange(tc.req)
+
+ require.ErrorIs(t, eerr, tc.wantErr)
+ assert.Equal(t, tc.want, host)
+ })
+ }
+
+ t.Run("resolving_disabled", func(t *testing.T) {
+ dns.conf.ResolveClients = false
+ for _, tc := range testCases {
+ host, eerr := dns.Exchange(tc.req)
+
+ require.NoError(t, eerr)
+ assert.Empty(t, host)
+ }
+ })
+}
diff --git a/internal/dnsforward/filter.go b/internal/dnsforward/filter.go
index 8b1c3283..b0e3ff89 100644
--- a/internal/dnsforward/filter.go
+++ b/internal/dnsforward/filter.go
@@ -42,15 +42,15 @@ func (s *Server) getClientRequestFilteringSettings(ctx *dnsContext) *dnsfilter.F
return &setts
}
-// filterDNSRequest applies the dnsFilter and sets d.Res if the request
-// was filtered.
+// filterDNSRequest applies the dnsFilter and sets d.Res if the request was
+// filtered.
func (s *Server) filterDNSRequest(ctx *dnsContext) (*dnsfilter.Result, error) {
d := ctx.proxyCtx
// TODO(e.burkov): Consistently use req instead of d.Req since it is
// declared.
req := d.Req
host := strings.TrimSuffix(req.Question[0].Name, ".")
- res, err := s.dnsFilter.CheckHost(host, d.Req.Question[0].Qtype, ctx.setts)
+ res, err := s.dnsFilter.CheckHost(host, req.Question[0].Qtype, ctx.setts)
if err != nil {
// Return immediately if there's an error
return nil, fmt.Errorf("dnsfilter failed to check host %q: %w", host, err)
@@ -63,8 +63,8 @@ func (s *Server) filterDNSRequest(ctx *dnsContext) (*dnsfilter.Result, error) {
// Resolve the new canonical name, not the original host
// name. The original question is readded in
// processFilteringAfterResponse.
- ctx.origQuestion = d.Req.Question[0]
- d.Req.Question[0].Name = dns.Fqdn(res.CanonName)
+ ctx.origQuestion = req.Question[0]
+ req.Question[0].Name = dns.Fqdn(res.CanonName)
} else if res.Reason == dnsfilter.RewrittenAutoHosts && len(res.ReverseHosts) != 0 {
resp := s.makeResponse(req)
for _, h := range res.ReverseHosts {
@@ -84,7 +84,7 @@ func (s *Server) filterDNSRequest(ctx *dnsContext) (*dnsfilter.Result, error) {
}
d.Res = resp
- } else if res.Reason == dnsfilter.Rewritten || res.Reason == dnsfilter.RewrittenAutoHosts {
+ } else if res.Reason.In(dnsfilter.Rewritten, dnsfilter.RewrittenAutoHosts) {
resp := s.makeResponse(req)
name := host
diff --git a/internal/dnsforward/http.go b/internal/dnsforward/http.go
index effd3b0a..12dd170b 100644
--- a/internal/dnsforward/http.go
+++ b/internal/dnsforward/http.go
@@ -10,6 +10,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/agherr"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
+ "github.com/AdguardTeam/AdGuardHome/internal/aghstrings"
"github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/log"
@@ -27,59 +28,67 @@ type dnsConfig struct {
UpstreamsFile *string `json:"upstream_dns_file"`
Bootstraps *[]string `json:"bootstrap_dns"`
- ProtectionEnabled *bool `json:"protection_enabled"`
- RateLimit *uint32 `json:"ratelimit"`
- BlockingMode *string `json:"blocking_mode"`
- BlockingIPv4 net.IP `json:"blocking_ipv4"`
- BlockingIPv6 net.IP `json:"blocking_ipv6"`
- EDNSCSEnabled *bool `json:"edns_cs_enabled"`
- DNSSECEnabled *bool `json:"dnssec_enabled"`
- DisableIPv6 *bool `json:"disable_ipv6"`
- UpstreamMode *string `json:"upstream_mode"`
- CacheSize *uint32 `json:"cache_size"`
- CacheMinTTL *uint32 `json:"cache_ttl_min"`
- CacheMaxTTL *uint32 `json:"cache_ttl_max"`
+ ProtectionEnabled *bool `json:"protection_enabled"`
+ RateLimit *uint32 `json:"ratelimit"`
+ BlockingMode *string `json:"blocking_mode"`
+ BlockingIPv4 net.IP `json:"blocking_ipv4"`
+ BlockingIPv6 net.IP `json:"blocking_ipv6"`
+ EDNSCSEnabled *bool `json:"edns_cs_enabled"`
+ DNSSECEnabled *bool `json:"dnssec_enabled"`
+ DisableIPv6 *bool `json:"disable_ipv6"`
+ UpstreamMode *string `json:"upstream_mode"`
+ CacheSize *uint32 `json:"cache_size"`
+ CacheMinTTL *uint32 `json:"cache_ttl_min"`
+ CacheMaxTTL *uint32 `json:"cache_ttl_max"`
+ ResolveClients *bool `json:"resolve_clients"`
+ LocalPTRUpstreams *[]string `json:"local_ptr_upstreams"`
}
func (s *Server) getDNSConfig() dnsConfig {
s.RLock()
- upstreams := stringArrayDup(s.conf.UpstreamDNS)
+ defer s.RUnlock()
+
+ upstreams := aghstrings.CloneSliceOrEmpty(s.conf.UpstreamDNS)
upstreamFile := s.conf.UpstreamDNSFileName
- bootstraps := stringArrayDup(s.conf.BootstrapDNS)
+ bootstraps := aghstrings.CloneSliceOrEmpty(s.conf.BootstrapDNS)
protectionEnabled := s.conf.ProtectionEnabled
blockingMode := s.conf.BlockingMode
- BlockingIPv4 := s.conf.BlockingIPv4
- BlockingIPv6 := s.conf.BlockingIPv6
- Ratelimit := s.conf.Ratelimit
- EnableEDNSClientSubnet := s.conf.EnableEDNSClientSubnet
- EnableDNSSEC := s.conf.EnableDNSSEC
- AAAADisabled := s.conf.AAAADisabled
- CacheSize := s.conf.CacheSize
- CacheMinTTL := s.conf.CacheMinTTL
- CacheMaxTTL := s.conf.CacheMaxTTL
+ blockingIPv4 := s.conf.BlockingIPv4
+ blockingIPv6 := s.conf.BlockingIPv6
+ ratelimit := s.conf.Ratelimit
+ enableEDNSClientSubnet := s.conf.EnableEDNSClientSubnet
+ enableDNSSEC := s.conf.EnableDNSSEC
+ aaaaDisabled := s.conf.AAAADisabled
+ cacheSize := s.conf.CacheSize
+ cacheMinTTL := s.conf.CacheMinTTL
+ cacheMaxTTL := s.conf.CacheMaxTTL
+ resolveClients := s.conf.ResolveClients
+ localPTRUpstreams := aghstrings.CloneSliceOrEmpty(s.conf.LocalPTRResolvers)
var upstreamMode string
if s.conf.FastestAddr {
upstreamMode = "fastest_addr"
} else if s.conf.AllServers {
upstreamMode = "parallel"
}
- s.RUnlock()
+
return dnsConfig{
Upstreams: &upstreams,
UpstreamsFile: &upstreamFile,
Bootstraps: &bootstraps,
ProtectionEnabled: &protectionEnabled,
BlockingMode: &blockingMode,
- BlockingIPv4: BlockingIPv4,
- BlockingIPv6: BlockingIPv6,
- RateLimit: &Ratelimit,
- EDNSCSEnabled: &EnableEDNSClientSubnet,
- DNSSECEnabled: &EnableDNSSEC,
- DisableIPv6: &AAAADisabled,
- CacheSize: &CacheSize,
- CacheMinTTL: &CacheMinTTL,
- CacheMaxTTL: &CacheMaxTTL,
+ BlockingIPv4: blockingIPv4,
+ BlockingIPv6: blockingIPv6,
+ RateLimit: &ratelimit,
+ EDNSCSEnabled: &enableEDNSClientSubnet,
+ DNSSECEnabled: &enableDNSSEC,
+ DisableIPv6: &aaaaDisabled,
+ CacheSize: &cacheSize,
+ CacheMinTTL: &cacheMinTTL,
+ CacheMaxTTL: &cacheMaxTTL,
UpstreamMode: &upstreamMode,
+ ResolveClients: &resolveClients,
+ LocalPTRUpstreams: &localPTRUpstreams,
}
}
@@ -227,6 +236,11 @@ func (s *Server) setConfigRestartable(dc dnsConfig) (restart bool) {
restart = true
}
+ if dc.LocalPTRUpstreams != nil {
+ s.conf.LocalPTRResolvers = *dc.LocalPTRUpstreams
+ restart = true
+ }
+
if dc.UpstreamsFile != nil {
s.conf.UpstreamDNSFileName = *dc.UpstreamsFile
restart = true
@@ -294,15 +308,24 @@ func (s *Server) setConfig(dc dnsConfig) (restart bool) {
s.conf.FastestAddr = *dc.UpstreamMode == "fastest_addr"
}
+ if dc.ResolveClients != nil {
+ s.conf.ResolveClients = *dc.ResolveClients
+ }
+
return s.setConfigRestartable(dc)
}
+// upstreamJSON is a request body for handleTestUpstreamDNS endpoint.
type upstreamJSON struct {
- Upstreams []string `json:"upstream_dns"` // Upstreams
- BootstrapDNS []string `json:"bootstrap_dns"` // Bootstrap DNS
+ Upstreams []string `json:"upstream_dns"`
+ BootstrapDNS []string `json:"bootstrap_dns"`
+ PrivateUpstreams []string `json:"private_upstream"`
}
-// ValidateUpstreams validates each upstream and returns an error if any upstream is invalid or if there are no default upstreams specified
+// ValidateUpstreams validates each upstream and returns an error if any
+// upstream is invalid or if there are no default upstreams specified.
+//
+// TODO(e.burkov): Move into aghnet or even into dnsproxy.
func ValidateUpstreams(upstreams []string) (err error) {
// No need to validate comments
upstreams = filterOutComments(upstreams)
@@ -428,52 +451,76 @@ func checkPlainDNS(upstream string) error {
return nil
}
-func (s *Server) handleTestUpstreamDNS(w http.ResponseWriter, r *http.Request) {
- req := upstreamJSON{}
- err := json.NewDecoder(r.Body).Decode(&req)
- if err != nil {
- httpError(r, w, http.StatusBadRequest, "Failed to read request body: %s", err)
- return
+// excFunc is a signature of function to check if upstream exchanges correctly.
+type excFunc func(u upstream.Upstream) (err error)
+
+// checkDNSUpstreamExc checks if the DNS upstream exchanges correctly.
+func checkDNSUpstreamExc(u upstream.Upstream) (err error) {
+ req := &dns.Msg{
+ MsgHdr: dns.MsgHdr{
+ Id: dns.Id(),
+ RecursionDesired: true,
+ },
+ Question: []dns.Question{{
+ Name: "google-public-dns-a.google.com.",
+ Qtype: dns.TypeA,
+ Qclass: dns.ClassINET,
+ }},
}
- result := map[string]string{}
+ var reply *dns.Msg
+ reply, err = u.Exchange(req)
+ if err != nil {
+ return fmt.Errorf("couldn't communicate with upstream: %w", err)
+ }
- for _, host := range req.Upstreams {
- err = checkDNS(host, req.BootstrapDNS)
- if err != nil {
- log.Info("%v", err)
- result[host] = err.Error()
- } else {
- result[host] = "OK"
+ if len(reply.Answer) != 1 {
+ return fmt.Errorf("wrong response")
+ }
+
+ if t, ok := reply.Answer[0].(*dns.A); ok {
+ if !net.IPv4(8, 8, 8, 8).Equal(t.A) {
+ return fmt.Errorf("wrong response")
}
}
- jsonVal, err := json.Marshal(result)
- if err != nil {
- httpError(r, w, http.StatusInternalServerError, "Unable to marshal status json: %s", err)
- return
- }
-
- w.Header().Set("Content-Type", "application/json")
- _, err = w.Write(jsonVal)
- if err != nil {
- httpError(r, w, http.StatusInternalServerError, "Couldn't write body: %s", err)
- return
- }
+ return nil
}
-func checkDNS(input string, bootstrap []string) error {
+// checkPrivateUpstreamExc checks if the upstream for resolving private
+// addresses exchanges correctly.
+func checkPrivateUpstreamExc(u upstream.Upstream) (err error) {
+ req := &dns.Msg{
+ MsgHdr: dns.MsgHdr{
+ Id: dns.Id(),
+ RecursionDesired: true,
+ },
+ Question: []dns.Question{{
+ Name: "1.0.0.127.in-addr.arpa.",
+ Qtype: dns.TypePTR,
+ Qclass: dns.ClassINET,
+ }},
+ }
+
+ if _, err = u.Exchange(req); err != nil {
+ return fmt.Errorf("couldn't communicate with upstream: %w", err)
+ }
+
+ return nil
+}
+
+func checkDNS(input string, bootstrap []string, ef excFunc) (err error) {
if !isUpstream(input) {
return nil
}
- // separate upstream from domains list
- input, useDefault, err := separateUpstream(input)
- if err != nil {
+ // Separate upstream from domains list.
+ var useDefault bool
+ if input, useDefault, err = separateUpstream(input); err != nil {
return fmt.Errorf("wrong upstream format: %w", err)
}
- // No need to check this DNS server
+ // No need to check this DNS server.
if !useDefault {
return nil
}
@@ -486,35 +533,80 @@ func checkDNS(input string, bootstrap []string) error {
bootstrap = defaultBootstrap
}
- log.Debug("checking if dns %s works...", input)
- u, err := upstream.AddressToUpstream(input, upstream.Options{Bootstrap: bootstrap, Timeout: DefaultTimeout})
+ log.Debug("checking if dns server %q works...", input)
+ var u upstream.Upstream
+ u, err = upstream.AddressToUpstream(input, upstream.Options{
+ Bootstrap: bootstrap,
+ Timeout: DefaultTimeout,
+ })
if err != nil {
- return fmt.Errorf("failed to choose upstream for %s: %w", input, err)
+ return fmt.Errorf("failed to choose upstream for %q: %w", input, err)
}
- req := dns.Msg{}
- req.Id = dns.Id()
- req.RecursionDesired = true
- req.Question = []dns.Question{
- {Name: "google-public-dns-a.google.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET},
- }
- reply, err := u.Exchange(&req)
- if err != nil {
- return fmt.Errorf("couldn't communicate with dns server %s: %w", input, err)
- }
- if len(reply.Answer) != 1 {
- return fmt.Errorf("dns server %s returned wrong answer", input)
- }
- if t, ok := reply.Answer[0].(*dns.A); ok {
- if !net.IPv4(8, 8, 8, 8).Equal(t.A) {
- return fmt.Errorf("dns server %s returned wrong answer: %v", input, t.A)
- }
+ if err = ef(u); err != nil {
+ return fmt.Errorf("upstream %q fails to exchange: %w", input, err)
}
log.Debug("dns %s works OK", input)
+
return nil
}
+func (s *Server) handleTestUpstreamDNS(w http.ResponseWriter, r *http.Request) {
+ req := &upstreamJSON{}
+ err := json.NewDecoder(r.Body).Decode(req)
+ if err != nil {
+ httpError(r, w, http.StatusBadRequest, "Failed to read request body: %s", err)
+
+ return
+ }
+
+ result := map[string]string{}
+ bootstraps := req.BootstrapDNS
+
+ for _, host := range req.Upstreams {
+ err = checkDNS(host, bootstraps, checkDNSUpstreamExc)
+ if err != nil {
+ log.Info("%v", err)
+ result[host] = err.Error()
+
+ continue
+ }
+
+ result[host] = "OK"
+ }
+
+ for _, host := range req.PrivateUpstreams {
+ err = checkDNS(host, bootstraps, checkPrivateUpstreamExc)
+ if err != nil {
+ log.Info("%v", err)
+ // TODO(e.burkov): If passed upstream have already
+ // written an error above, we rewriting the error for
+ // it. These cases should be handled properly instead.
+ result[host] = err.Error()
+
+ continue
+ }
+
+ result[host] = "OK"
+ }
+
+ jsonVal, err := json.Marshal(result)
+ if err != nil {
+ httpError(r, w, http.StatusInternalServerError, "Unable to marshal status json: %s", err)
+
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ _, err = w.Write(jsonVal)
+ if err != nil {
+ httpError(r, w, http.StatusInternalServerError, "Couldn't write body: %s", err)
+
+ return
+ }
+}
+
// Control flow:
// web
// -> dnsforward.handleDOH -> dnsforward.ServeHTTP
diff --git a/internal/dnsforward/http_test.go b/internal/dnsforward/http_test.go
index 6c3acc10..273d9235 100644
--- a/internal/dnsforward/http_test.go
+++ b/internal/dnsforward/http_test.go
@@ -1,11 +1,14 @@
package dnsforward
import (
+ "bytes"
+ "encoding/json"
"io/ioutil"
"net"
"net/http"
"net/http/httptest"
- "strings"
+ "os"
+ "path/filepath"
"testing"
"github.com/AdguardTeam/AdGuardHome/internal/dnsfilter"
@@ -13,6 +16,22 @@ import (
"github.com/stretchr/testify/require"
)
+func loadTestData(t *testing.T, casesFileName string, cases interface{}) {
+ t.Helper()
+
+ var f *os.File
+ f, err := os.Open(filepath.Join("testdata", casesFileName))
+ require.NoError(t, err)
+ t.Cleanup(func() {
+ require.NoError(t, f.Close())
+ })
+
+ err = json.NewDecoder(f).Decode(cases)
+ require.NoError(t, err)
+}
+
+const jsonExt = ".json"
+
func TestDNSForwardHTTTP_handleGetConfig(t *testing.T) {
filterConf := &dnsfilter.Config{
SafeBrowsingEnabled: true,
@@ -42,36 +61,38 @@ func TestDNSForwardHTTTP_handleGetConfig(t *testing.T) {
w := httptest.NewRecorder()
testCases := []struct {
- name string
conf func() ServerConfig
- want string
+ name string
}{{
- name: "all_right",
conf: func() ServerConfig {
return defaultConf
},
- want: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}\n",
+ name: "all_right",
}, {
- name: "fastest_addr",
conf: func() ServerConfig {
conf := defaultConf
conf.FastestAddr = true
return conf
},
- want: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"fastest_addr\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}\n",
+ name: "fastest_addr",
}, {
- name: "parallel",
conf: func() ServerConfig {
conf := defaultConf
conf.AllServers = true
return conf
},
- want: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"parallel\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}\n",
+ name: "parallel",
}}
+ var data map[string]json.RawMessage
+ loadTestData(t, t.Name()+jsonExt, &data)
+
for _, tc := range testCases {
+ caseWant, ok := data[tc.name]
+ require.True(t, ok)
+
t.Run(tc.name, func(t *testing.T) {
t.Cleanup(w.Body.Reset)
@@ -79,7 +100,7 @@ func TestDNSForwardHTTTP_handleGetConfig(t *testing.T) {
s.handleGetConfig(w, nil)
assert.Equal(t, "application/json", w.Header().Get("Content-Type"))
- assert.Equal(t, tc.want, w.Body.String())
+ assert.JSONEq(t, string(caseWant), w.Body.String())
})
}
}
@@ -108,97 +129,81 @@ func TestDNSForwardHTTTP_handleSetConfig(t *testing.T) {
err := s.Start()
assert.Nil(t, err)
- defer func() {
+ t.Cleanup(func() {
assert.Nil(t, s.Stop())
- }()
+ })
w := httptest.NewRecorder()
- const defaultConfJSON = "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}\n"
testCases := []struct {
name string
- req string
wantSet string
- wantGet string
}{{
name: "upstream_dns",
- req: "{\"upstream_dns\":[\"8.8.8.8:77\",\"8.8.4.4:77\"]}",
wantSet: "",
- wantGet: "{\"upstream_dns\":[\"8.8.8.8:77\",\"8.8.4.4:77\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}\n",
}, {
name: "bootstraps",
- req: "{\"bootstrap_dns\":[\"9.9.9.10\"]}",
wantSet: "",
- wantGet: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}\n",
}, {
name: "blocking_mode_good",
- req: "{\"blocking_mode\":\"refused\"}",
wantSet: "",
- wantGet: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"refused\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}\n",
}, {
name: "blocking_mode_bad",
- req: "{\"blocking_mode\":\"custom_ip\"}",
wantSet: "blocking_mode: incorrect value\n",
- wantGet: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}\n",
}, {
name: "ratelimit",
- req: "{\"ratelimit\":6}",
wantSet: "",
- wantGet: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":6,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}\n",
}, {
name: "edns_cs_enabled",
- req: "{\"edns_cs_enabled\":true}",
wantSet: "",
- wantGet: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":true,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}\n",
}, {
name: "dnssec_enabled",
- req: "{\"dnssec_enabled\":true}",
wantSet: "",
- wantGet: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":true,\"disable_ipv6\":false,\"upstream_mode\":\"\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}\n",
}, {
name: "cache_size",
- req: "{\"cache_size\":1024}",
wantSet: "",
- wantGet: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"\",\"cache_size\":1024,\"cache_ttl_min\":0,\"cache_ttl_max\":0}\n",
}, {
name: "upstream_mode_parallel",
- req: "{\"upstream_mode\":\"parallel\"}",
wantSet: "",
- wantGet: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"parallel\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}\n",
}, {
name: "upstream_mode_fastest_addr",
- req: "{\"upstream_mode\":\"fastest_addr\"}",
wantSet: "",
- wantGet: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"fastest_addr\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}\n",
}, {
name: "upstream_dns_bad",
- req: "{\"upstream_dns\":[\"\"]}",
wantSet: "wrong upstreams specification: missing port in address\n",
- wantGet: defaultConfJSON,
}, {
name: "bootstraps_bad",
- req: "{\"bootstrap_dns\":[\"a\"]}",
wantSet: "a can not be used as bootstrap dns cause: invalid bootstrap server address: Resolver a is not eligible to be a bootstrap DNS server\n",
- wantGet: defaultConfJSON,
}, {
name: "cache_bad_ttl",
- req: "{\"cache_ttl_min\":1024,\"cache_ttl_max\":512}",
wantSet: "cache_ttl_min must be less or equal than cache_ttl_max\n",
- wantGet: defaultConfJSON,
}, {
name: "upstream_mode_bad",
- req: "{\"upstream_mode\":\"somethingelse\"}",
wantSet: "upstream_mode: incorrect value\n",
- wantGet: defaultConfJSON,
+ }, {
+ name: "local_ptr_upstreams_good",
+ wantSet: "",
+ }, {
+ name: "local_ptr_upstreams_null",
+ wantSet: "",
}}
+ var data map[string]struct {
+ Req json.RawMessage `json:"req"`
+ Want json.RawMessage `json:"want"`
+ }
+ loadTestData(t, t.Name()+jsonExt, &data)
+
for _, tc := range testCases {
+ caseData, ok := data[tc.name]
+ require.True(t, ok)
+
t.Run(tc.name, func(t *testing.T) {
t.Cleanup(func() {
s.conf = defaultConf
})
- rBody := ioutil.NopCloser(strings.NewReader(tc.req))
+ rBody := ioutil.NopCloser(bytes.NewReader(caseData.Req))
var r *http.Request
r, err = http.NewRequest(http.MethodPost, "http://example.com", rBody)
require.Nil(t, err)
@@ -208,7 +213,7 @@ func TestDNSForwardHTTTP_handleSetConfig(t *testing.T) {
w.Body.Reset()
s.handleGetConfig(w, nil)
- assert.Equal(t, tc.wantGet, w.Body.String())
+ assert.JSONEq(t, string(caseData.Want), w.Body.String())
w.Body.Reset()
})
}
diff --git a/internal/dnsforward/testdata/TestDNSForwardHTTTP_handleGetConfig.json b/internal/dnsforward/testdata/TestDNSForwardHTTTP_handleGetConfig.json
new file mode 100644
index 00000000..562f0fcc
--- /dev/null
+++ b/internal/dnsforward/testdata/TestDNSForwardHTTTP_handleGetConfig.json
@@ -0,0 +1,83 @@
+{
+ "all_right": {
+ "upstream_dns": [
+ "8.8.8.8:53",
+ "8.8.4.4:53"
+ ],
+ "upstream_dns_file": "",
+ "bootstrap_dns": [
+ "9.9.9.10",
+ "149.112.112.10",
+ "2620:fe::10",
+ "2620:fe::fe:10"
+ ],
+ "protection_enabled": true,
+ "ratelimit": 0,
+ "blocking_mode": "",
+ "blocking_ipv4": "",
+ "blocking_ipv6": "",
+ "edns_cs_enabled": false,
+ "dnssec_enabled": false,
+ "disable_ipv6": false,
+ "upstream_mode": "",
+ "cache_size": 0,
+ "cache_ttl_min": 0,
+ "cache_ttl_max": 0,
+ "resolve_clients": false,
+ "local_ptr_upstreams": []
+ },
+ "fastest_addr": {
+ "upstream_dns": [
+ "8.8.8.8:53",
+ "8.8.4.4:53"
+ ],
+ "upstream_dns_file": "",
+ "bootstrap_dns": [
+ "9.9.9.10",
+ "149.112.112.10",
+ "2620:fe::10",
+ "2620:fe::fe:10"
+ ],
+ "protection_enabled": true,
+ "ratelimit": 0,
+ "blocking_mode": "",
+ "blocking_ipv4": "",
+ "blocking_ipv6": "",
+ "edns_cs_enabled": false,
+ "dnssec_enabled": false,
+ "disable_ipv6": false,
+ "upstream_mode": "fastest_addr",
+ "cache_size": 0,
+ "cache_ttl_min": 0,
+ "cache_ttl_max": 0,
+ "resolve_clients": false,
+ "local_ptr_upstreams": []
+ },
+ "parallel": {
+ "upstream_dns": [
+ "8.8.8.8:53",
+ "8.8.4.4:53"
+ ],
+ "upstream_dns_file": "",
+ "bootstrap_dns": [
+ "9.9.9.10",
+ "149.112.112.10",
+ "2620:fe::10",
+ "2620:fe::fe:10"
+ ],
+ "protection_enabled": true,
+ "ratelimit": 0,
+ "blocking_mode": "",
+ "blocking_ipv4": "",
+ "blocking_ipv6": "",
+ "edns_cs_enabled": false,
+ "dnssec_enabled": false,
+ "disable_ipv6": false,
+ "upstream_mode": "parallel",
+ "cache_size": 0,
+ "cache_ttl_min": 0,
+ "cache_ttl_max": 0,
+ "resolve_clients": false,
+ "local_ptr_upstreams": []
+ }
+}
\ No newline at end of file
diff --git a/internal/dnsforward/testdata/TestDNSForwardHTTTP_handleSetConfig.json b/internal/dnsforward/testdata/TestDNSForwardHTTTP_handleSetConfig.json
new file mode 100644
index 00000000..f1771a19
--- /dev/null
+++ b/internal/dnsforward/testdata/TestDNSForwardHTTTP_handleSetConfig.json
@@ -0,0 +1,525 @@
+{
+ "upstream_dns": {
+ "req": {
+ "upstream_dns": [
+ "8.8.8.8:77",
+ "8.8.4.4:77"
+ ]
+ },
+ "want": {
+ "upstream_dns": [
+ "8.8.8.8:77",
+ "8.8.4.4:77"
+ ],
+ "upstream_dns_file": "",
+ "bootstrap_dns": [
+ "9.9.9.10",
+ "149.112.112.10",
+ "2620:fe::10",
+ "2620:fe::fe:10"
+ ],
+ "protection_enabled": true,
+ "ratelimit": 0,
+ "blocking_mode": "",
+ "blocking_ipv4": "",
+ "blocking_ipv6": "",
+ "edns_cs_enabled": false,
+ "dnssec_enabled": false,
+ "disable_ipv6": false,
+ "upstream_mode": "",
+ "cache_size": 0,
+ "cache_ttl_min": 0,
+ "cache_ttl_max": 0,
+ "resolve_clients": false,
+ "local_ptr_upstreams": []
+ }
+ },
+ "bootstraps": {
+ "req": {
+ "bootstrap_dns": [
+ "9.9.9.10"
+ ]
+ },
+ "want": {
+ "upstream_dns": [
+ "8.8.8.8:53",
+ "8.8.4.4:53"
+ ],
+ "upstream_dns_file": "",
+ "bootstrap_dns": [
+ "9.9.9.10"
+ ],
+ "protection_enabled": true,
+ "ratelimit": 0,
+ "blocking_mode": "",
+ "blocking_ipv4": "",
+ "blocking_ipv6": "",
+ "edns_cs_enabled": false,
+ "dnssec_enabled": false,
+ "disable_ipv6": false,
+ "upstream_mode": "",
+ "cache_size": 0,
+ "cache_ttl_min": 0,
+ "cache_ttl_max": 0,
+ "resolve_clients": false,
+ "local_ptr_upstreams": []
+ }
+ },
+ "blocking_mode_good": {
+ "req": {
+ "blocking_mode": "refused"
+ },
+ "want": {
+ "upstream_dns": [
+ "8.8.8.8:53",
+ "8.8.4.4:53"
+ ],
+ "upstream_dns_file": "",
+ "bootstrap_dns": [
+ "9.9.9.10",
+ "149.112.112.10",
+ "2620:fe::10",
+ "2620:fe::fe:10"
+ ],
+ "protection_enabled": true,
+ "ratelimit": 0,
+ "blocking_mode": "refused",
+ "blocking_ipv4": "",
+ "blocking_ipv6": "",
+ "edns_cs_enabled": false,
+ "dnssec_enabled": false,
+ "disable_ipv6": false,
+ "upstream_mode": "",
+ "cache_size": 0,
+ "cache_ttl_min": 0,
+ "cache_ttl_max": 0,
+ "resolve_clients": false,
+ "local_ptr_upstreams": []
+ }
+ },
+ "blocking_mode_bad": {
+ "req": {
+ "blocking_mode": "custom_ip"
+ },
+ "want": {
+ "upstream_dns": [
+ "8.8.8.8:53",
+ "8.8.4.4:53"
+ ],
+ "upstream_dns_file": "",
+ "bootstrap_dns": [
+ "9.9.9.10",
+ "149.112.112.10",
+ "2620:fe::10",
+ "2620:fe::fe:10"
+ ],
+ "protection_enabled": true,
+ "ratelimit": 0,
+ "blocking_mode": "",
+ "blocking_ipv4": "",
+ "blocking_ipv6": "",
+ "edns_cs_enabled": false,
+ "dnssec_enabled": false,
+ "disable_ipv6": false,
+ "upstream_mode": "",
+ "cache_size": 0,
+ "cache_ttl_min": 0,
+ "cache_ttl_max": 0,
+ "resolve_clients": false,
+ "local_ptr_upstreams": []
+ }
+ },
+ "ratelimit": {
+ "req": {
+ "ratelimit": 6
+ },
+ "want": {
+ "upstream_dns": [
+ "8.8.8.8:53",
+ "8.8.4.4:53"
+ ],
+ "upstream_dns_file": "",
+ "bootstrap_dns": [
+ "9.9.9.10",
+ "149.112.112.10",
+ "2620:fe::10",
+ "2620:fe::fe:10"
+ ],
+ "protection_enabled": true,
+ "ratelimit": 6,
+ "blocking_mode": "",
+ "blocking_ipv4": "",
+ "blocking_ipv6": "",
+ "edns_cs_enabled": false,
+ "dnssec_enabled": false,
+ "disable_ipv6": false,
+ "upstream_mode": "",
+ "cache_size": 0,
+ "cache_ttl_min": 0,
+ "cache_ttl_max": 0,
+ "resolve_clients": false,
+ "local_ptr_upstreams": []
+ }
+ },
+ "edns_cs_enabled": {
+ "req": {
+ "edns_cs_enabled": true
+ },
+ "want": {
+ "upstream_dns": [
+ "8.8.8.8:53",
+ "8.8.4.4:53"
+ ],
+ "upstream_dns_file": "",
+ "bootstrap_dns": [
+ "9.9.9.10",
+ "149.112.112.10",
+ "2620:fe::10",
+ "2620:fe::fe:10"
+ ],
+ "protection_enabled": true,
+ "ratelimit": 0,
+ "blocking_mode": "",
+ "blocking_ipv4": "",
+ "blocking_ipv6": "",
+ "edns_cs_enabled": true,
+ "dnssec_enabled": false,
+ "disable_ipv6": false,
+ "upstream_mode": "",
+ "cache_size": 0,
+ "cache_ttl_min": 0,
+ "cache_ttl_max": 0,
+ "resolve_clients": false,
+ "local_ptr_upstreams": []
+ }
+ },
+ "dnssec_enabled": {
+ "req": {
+ "dnssec_enabled": true
+ },
+ "want": {
+ "upstream_dns": [
+ "8.8.8.8:53",
+ "8.8.4.4:53"
+ ],
+ "upstream_dns_file": "",
+ "bootstrap_dns": [
+ "9.9.9.10",
+ "149.112.112.10",
+ "2620:fe::10",
+ "2620:fe::fe:10"
+ ],
+ "protection_enabled": true,
+ "ratelimit": 0,
+ "blocking_mode": "",
+ "blocking_ipv4": "",
+ "blocking_ipv6": "",
+ "edns_cs_enabled": false,
+ "dnssec_enabled": true,
+ "disable_ipv6": false,
+ "upstream_mode": "",
+ "cache_size": 0,
+ "cache_ttl_min": 0,
+ "cache_ttl_max": 0,
+ "resolve_clients": false,
+ "local_ptr_upstreams": []
+ }
+ },
+ "cache_size": {
+ "req": {
+ "cache_size": 1024
+ },
+ "want": {
+ "upstream_dns": [
+ "8.8.8.8:53",
+ "8.8.4.4:53"
+ ],
+ "upstream_dns_file": "",
+ "bootstrap_dns": [
+ "9.9.9.10",
+ "149.112.112.10",
+ "2620:fe::10",
+ "2620:fe::fe:10"
+ ],
+ "protection_enabled": true,
+ "ratelimit": 0,
+ "blocking_mode": "",
+ "blocking_ipv4": "",
+ "blocking_ipv6": "",
+ "edns_cs_enabled": false,
+ "dnssec_enabled": false,
+ "disable_ipv6": false,
+ "upstream_mode": "",
+ "cache_size": 1024,
+ "cache_ttl_min": 0,
+ "cache_ttl_max": 0,
+ "resolve_clients": false,
+ "local_ptr_upstreams": []
+ }
+ },
+ "upstream_mode_parallel": {
+ "req": {
+ "upstream_mode": "parallel"
+ },
+ "want": {
+ "upstream_dns": [
+ "8.8.8.8:53",
+ "8.8.4.4:53"
+ ],
+ "upstream_dns_file": "",
+ "bootstrap_dns": [
+ "9.9.9.10",
+ "149.112.112.10",
+ "2620:fe::10",
+ "2620:fe::fe:10"
+ ],
+ "protection_enabled": true,
+ "ratelimit": 0,
+ "blocking_mode": "",
+ "blocking_ipv4": "",
+ "blocking_ipv6": "",
+ "edns_cs_enabled": false,
+ "dnssec_enabled": false,
+ "disable_ipv6": false,
+ "upstream_mode": "parallel",
+ "cache_size": 0,
+ "cache_ttl_min": 0,
+ "cache_ttl_max": 0,
+ "resolve_clients": false,
+ "local_ptr_upstreams": []
+ }
+ },
+ "upstream_mode_fastest_addr": {
+ "req": {
+ "upstream_mode": "fastest_addr"
+ },
+ "want": {
+ "upstream_dns": [
+ "8.8.8.8:53",
+ "8.8.4.4:53"
+ ],
+ "upstream_dns_file": "",
+ "bootstrap_dns": [
+ "9.9.9.10",
+ "149.112.112.10",
+ "2620:fe::10",
+ "2620:fe::fe:10"
+ ],
+ "protection_enabled": true,
+ "ratelimit": 0,
+ "blocking_mode": "",
+ "blocking_ipv4": "",
+ "blocking_ipv6": "",
+ "edns_cs_enabled": false,
+ "dnssec_enabled": false,
+ "disable_ipv6": false,
+ "upstream_mode": "fastest_addr",
+ "cache_size": 0,
+ "cache_ttl_min": 0,
+ "cache_ttl_max": 0,
+ "resolve_clients": false,
+ "local_ptr_upstreams": []
+ }
+ },
+ "upstream_dns_bad": {
+ "req": {
+ "upstream_dns": [
+ ""
+ ]
+ },
+ "want": {
+ "upstream_dns": [
+ "8.8.8.8:53",
+ "8.8.4.4:53"
+ ],
+ "upstream_dns_file": "",
+ "bootstrap_dns": [
+ "9.9.9.10",
+ "149.112.112.10",
+ "2620:fe::10",
+ "2620:fe::fe:10"
+ ],
+ "protection_enabled": true,
+ "ratelimit": 0,
+ "blocking_mode": "",
+ "blocking_ipv4": "",
+ "blocking_ipv6": "",
+ "edns_cs_enabled": false,
+ "dnssec_enabled": false,
+ "disable_ipv6": false,
+ "upstream_mode": "",
+ "cache_size": 0,
+ "cache_ttl_min": 0,
+ "cache_ttl_max": 0,
+ "resolve_clients": false,
+ "local_ptr_upstreams": []
+ }
+ },
+ "bootstraps_bad": {
+ "req": {
+ "bootstrap_dns": [
+ "a"
+ ]
+ },
+ "want": {
+ "upstream_dns": [
+ "8.8.8.8:53",
+ "8.8.4.4:53"
+ ],
+ "upstream_dns_file": "",
+ "bootstrap_dns": [
+ "9.9.9.10",
+ "149.112.112.10",
+ "2620:fe::10",
+ "2620:fe::fe:10"
+ ],
+ "protection_enabled": true,
+ "ratelimit": 0,
+ "blocking_mode": "",
+ "blocking_ipv4": "",
+ "blocking_ipv6": "",
+ "edns_cs_enabled": false,
+ "dnssec_enabled": false,
+ "disable_ipv6": false,
+ "upstream_mode": "",
+ "cache_size": 0,
+ "cache_ttl_min": 0,
+ "cache_ttl_max": 0,
+ "resolve_clients": false,
+ "local_ptr_upstreams": []
+ }
+ },
+ "cache_bad_ttl": {
+ "req": {
+ "cache_ttl_min": 1024,
+ "cache_ttl_max": 512
+ },
+ "want": {
+ "upstream_dns": [
+ "8.8.8.8:53",
+ "8.8.4.4:53"
+ ],
+ "upstream_dns_file": "",
+ "bootstrap_dns": [
+ "9.9.9.10",
+ "149.112.112.10",
+ "2620:fe::10",
+ "2620:fe::fe:10"
+ ],
+ "protection_enabled": true,
+ "ratelimit": 0,
+ "blocking_mode": "",
+ "blocking_ipv4": "",
+ "blocking_ipv6": "",
+ "edns_cs_enabled": false,
+ "dnssec_enabled": false,
+ "disable_ipv6": false,
+ "upstream_mode": "",
+ "cache_size": 0,
+ "cache_ttl_min": 0,
+ "cache_ttl_max": 0,
+ "resolve_clients": false,
+ "local_ptr_upstreams": []
+ }
+ },
+ "upstream_mode_bad": {
+ "req": {
+ "upstream_mode": "somethingelse"
+ },
+ "want": {
+ "upstream_dns": [
+ "8.8.8.8:53",
+ "8.8.4.4:53"
+ ],
+ "upstream_dns_file": "",
+ "bootstrap_dns": [
+ "9.9.9.10",
+ "149.112.112.10",
+ "2620:fe::10",
+ "2620:fe::fe:10"
+ ],
+ "protection_enabled": true,
+ "ratelimit": 0,
+ "blocking_mode": "",
+ "blocking_ipv4": "",
+ "blocking_ipv6": "",
+ "edns_cs_enabled": false,
+ "dnssec_enabled": false,
+ "disable_ipv6": false,
+ "upstream_mode": "",
+ "cache_size": 0,
+ "cache_ttl_min": 0,
+ "cache_ttl_max": 0,
+ "resolve_clients": false,
+ "local_ptr_upstreams": []
+ }
+ },
+ "local_ptr_upstreams_good": {
+ "req": {
+ "local_ptr_upstreams": [
+ "123.123.123.123"
+ ]
+ },
+ "want": {
+ "upstream_dns": [
+ "8.8.8.8:53",
+ "8.8.4.4:53"
+ ],
+ "upstream_dns_file": "",
+ "bootstrap_dns": [
+ "9.9.9.10",
+ "149.112.112.10",
+ "2620:fe::10",
+ "2620:fe::fe:10"
+ ],
+ "protection_enabled": true,
+ "ratelimit": 0,
+ "blocking_mode": "",
+ "blocking_ipv4": "",
+ "blocking_ipv6": "",
+ "edns_cs_enabled": false,
+ "dnssec_enabled": false,
+ "disable_ipv6": false,
+ "upstream_mode": "",
+ "cache_size": 0,
+ "cache_ttl_min": 0,
+ "cache_ttl_max": 0,
+ "resolve_clients": false,
+ "local_ptr_upstreams": [
+ "123.123.123.123"
+ ]
+ }
+ },
+ "local_ptr_upstreams_null": {
+ "req": {
+ "local_ptr_upstreams": null
+ },
+ "want": {
+ "upstream_dns": [
+ "8.8.8.8:53",
+ "8.8.4.4:53"
+ ],
+ "upstream_dns_file": "",
+ "bootstrap_dns": [
+ "9.9.9.10",
+ "149.112.112.10",
+ "2620:fe::10",
+ "2620:fe::fe:10"
+ ],
+ "protection_enabled": true,
+ "ratelimit": 0,
+ "blocking_mode": "",
+ "blocking_ipv4": "",
+ "blocking_ipv6": "",
+ "edns_cs_enabled": false,
+ "dnssec_enabled": false,
+ "disable_ipv6": false,
+ "upstream_mode": "",
+ "cache_size": 0,
+ "cache_ttl_min": 0,
+ "cache_ttl_max": 0,
+ "resolve_clients": false,
+ "local_ptr_upstreams": []
+ }
+ }
+}
\ No newline at end of file
diff --git a/internal/dnsforward/util.go b/internal/dnsforward/util.go
index e1d0c4a8..871447eb 100644
--- a/internal/dnsforward/util.go
+++ b/internal/dnsforward/util.go
@@ -30,12 +30,6 @@ func IPStringFromAddr(addr net.Addr) (ipStr string) {
return ""
}
-func stringArrayDup(a []string) []string {
- a2 := make([]string, len(a))
- copy(a2, a)
- return a2
-}
-
// Find value in a sorted array
func findSorted(ar []string, val string) int {
i := sort.SearchStrings(ar, val)
diff --git a/internal/home/clients.go b/internal/home/clients.go
index f3418cf8..32043f8c 100644
--- a/internal/home/clients.go
+++ b/internal/home/clients.go
@@ -13,6 +13,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/agherr"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
+ "github.com/AdguardTeam/AdGuardHome/internal/aghstrings"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
"github.com/AdguardTeam/AdGuardHome/internal/dnsfilter"
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
@@ -216,10 +217,10 @@ func (clients *clientsContainer) WriteDiskConfig(objects *[]clientObject) {
UseGlobalBlockedServices: !cli.UseOwnBlockedServices,
}
- cy.Tags = copyStrings(cli.Tags)
- cy.IDs = copyStrings(cli.IDs)
- cy.BlockedServices = copyStrings(cli.BlockedServices)
- cy.Upstreams = copyStrings(cli.Upstreams)
+ cy.Tags = aghstrings.CloneSlice(cli.Tags)
+ cy.IDs = aghstrings.CloneSlice(cli.IDs)
+ cy.BlockedServices = aghstrings.CloneSlice(cli.BlockedServices)
+ cy.Upstreams = aghstrings.CloneSlice(cli.Upstreams)
*objects = append(*objects, cy)
}
@@ -266,10 +267,6 @@ func (clients *clientsContainer) Exists(id string, source clientSource) (ok bool
return source <= rc.Source
}
-func copyStrings(a []string) (b []string) {
- return append(b, a...)
-}
-
func toQueryLogWhois(wi *RuntimeClientWhoisInfo) (cw *querylog.ClientWhois) {
if wi == nil {
return &querylog.ClientWhois{}
@@ -326,10 +323,10 @@ func (clients *clientsContainer) Find(id string) (c *Client, ok bool) {
return nil, false
}
- c.IDs = copyStrings(c.IDs)
- c.Tags = copyStrings(c.Tags)
- c.BlockedServices = copyStrings(c.BlockedServices)
- c.Upstreams = copyStrings(c.Upstreams)
+ c.IDs = aghstrings.CloneSlice(c.IDs)
+ c.Tags = aghstrings.CloneSlice(c.Tags)
+ c.BlockedServices = aghstrings.CloneSlice(c.BlockedServices)
+ c.Upstreams = aghstrings.CloneSlice(c.Upstreams)
return c, true
}
diff --git a/internal/home/config.go b/internal/home/config.go
index 554d1872..e9a77452 100644
--- a/internal/home/config.go
+++ b/internal/home/config.go
@@ -98,6 +98,13 @@ type dnsConfig struct {
// For example, a machine called "myhost" can be addressed as
// "myhost.lan" when AutohostTLD is "lan".
AutohostTLD string `yaml:"autohost_tld"`
+
+ // ResolveClients enables and disables resolving clients with RDNS.
+ ResolveClients bool `yaml:"resolve_clients"`
+
+ // LocalPTRResolvers is the slice of addresses to be used as upstreams
+ // for PTR queries for locally-served networks.
+ LocalPTRResolvers []string `yaml:"local_ptr_upstreams"`
}
type tlsConfigSettings struct {
@@ -150,6 +157,7 @@ var config = configuration{
FilteringEnabled: true, // whether or not use filter lists
FiltersUpdateIntervalHours: 24,
AutohostTLD: "lan",
+ ResolveClients: true,
},
TLS: tlsConfigSettings{
PortHTTPS: 443,
@@ -296,10 +304,12 @@ func (c *configuration) write() error {
config.DNS.DnsfilterConf = c
}
- if Context.dnsServer != nil {
+ if s := Context.dnsServer; s != nil {
c := dnsforward.FilteringConfig{}
- Context.dnsServer.WriteDiskConfig(&c)
+ s.WriteDiskConfig(&c)
config.DNS.FilteringConfig = c
+
+ config.DNS.LocalPTRResolvers, config.DNS.ResolveClients = s.RDNSSettings()
}
if Context.dhcpServer != nil {
diff --git a/internal/home/dns.go b/internal/home/dns.go
index d6a4aecc..e1cbc1e3 100644
--- a/internal/home/dns.go
+++ b/internal/home/dns.go
@@ -67,7 +67,6 @@ func initDNSServer() error {
Stats: Context.stats,
QueryLog: Context.queryLog,
SubnetDetector: Context.subnetDetector,
- LocalResolvers: Context.localResolvers,
AutohostTLD: config.DNS.AutohostTLD,
}
if Context.dhcpServer != nil {
@@ -95,7 +94,7 @@ func initDNSServer() error {
return fmt.Errorf("dnsServer.Prepare: %w", err)
}
- Context.rdns = NewRDNS(Context.dnsServer, &Context.clients, Context.subnetDetector, Context.localResolvers)
+ Context.rdns = NewRDNS(Context.dnsServer, &Context.clients)
Context.whois = initWhois(&Context.clients)
Context.filters.Init()
@@ -113,7 +112,7 @@ func onDNSRequest(d *proxy.DNSContext) {
return
}
- if !ip.IsLoopback() {
+ if config.DNS.ResolveClients && !ip.IsLoopback() {
Context.rdns.Begin(ip)
}
if !Context.subnetDetector.IsSpecialNetwork(ip) {
@@ -200,6 +199,9 @@ func generateServerConfig() (newConf dnsforward.ServerConfig, err error) {
newConf.FilterHandler = applyAdditionalFiltering
newConf.GetCustomUpstreamByClient = Context.clients.FindUpstreams
+ newConf.ResolveClients = dnsConf.ResolveClients
+ newConf.LocalPTRResolvers = dnsConf.LocalPTRResolvers
+
return newConf, nil
}
@@ -337,7 +339,7 @@ func startDNSServer() error {
const topClientsNumber = 100 // the number of clients to get
for _, ip := range Context.stats.GetTopClientsIP(topClientsNumber) {
- if !Context.subnetDetector.IsLocallyServedNetwork(ip) {
+ if config.DNS.ResolveClients && !ip.IsLoopback() {
Context.rdns.Begin(ip)
}
if !Context.subnetDetector.IsSpecialNetwork(ip) {
diff --git a/internal/home/home.go b/internal/home/home.go
index efbdf0ba..be70fc21 100644
--- a/internal/home/home.go
+++ b/internal/home/home.go
@@ -61,9 +61,7 @@ type homeContext struct {
autoHosts util.AutoHosts // IP-hostname pairs taken from system configuration (e.g. /etc/hosts) files
updater *updater.Updater
- subnetDetector *aghnet.SubnetDetector
- systemResolvers aghnet.SystemResolvers
- localResolvers aghnet.Exchanger
+ subnetDetector *aghnet.SubnetDetector
// mux is our custom http.ServeMux.
mux *http.ServeMux
@@ -222,110 +220,6 @@ func setupConfig(args options) {
}
}
-const defaultLocalTimeout = 5 * time.Second
-
-// stringsSetSubtract subtracts b from a interpreted as sets.
-//
-// TODO(e.burkov): Move into our internal package for working with strings.
-func stringsSetSubtract(a, b []string) (c []string) {
- // unit is an object to be used as value in set.
- type unit = struct{}
-
- cSet := make(map[string]unit)
- for _, k := range a {
- cSet[k] = unit{}
- }
-
- for _, k := range b {
- delete(cSet, k)
- }
-
- c = make([]string, len(cSet))
- i := 0
- for k := range cSet {
- c[i] = k
- i++
- }
-
- return c
-}
-
-// collectAllIfacesAddrs returns the slice of all network interfaces IP
-// addresses without port number.
-func collectAllIfacesAddrs() (addrs []string, err error) {
- var ifaces []net.Interface
- ifaces, err = net.Interfaces()
- if err != nil {
- return nil, fmt.Errorf("getting network interfaces: %w", err)
- }
-
- for _, iface := range ifaces {
- var ifaceAddrs []net.Addr
- ifaceAddrs, err = iface.Addrs()
- if err != nil {
- return nil, fmt.Errorf("getting addresses for %q: %w", iface.Name, err)
- }
-
- for _, addr := range ifaceAddrs {
- cidr := addr.String()
- var ip net.IP
- ip, _, err = net.ParseCIDR(cidr)
- if err != nil {
- return nil, fmt.Errorf("parsing %q as cidr: %w", cidr, err)
- }
-
- addrs = append(addrs, ip.String())
- }
- }
-
- return addrs, nil
-}
-
-// collectDNSIPAddrs returns the slice of IP addresses without port number which
-// we are listening on.
-func collectDNSIPaddrs() (addrs []string, err error) {
- addrs = make([]string, len(config.DNS.BindHosts))
-
- for i, bh := range config.DNS.BindHosts {
- if bh.IsUnspecified() {
- return collectAllIfacesAddrs()
- }
-
- addrs[i] = bh.String()
- }
-
- return addrs, nil
-}
-
-func setupResolvers() {
- // TODO(e.burkov): Enhance when the config will contain local resolvers
- // addresses.
-
- sysRes, err := aghnet.NewSystemResolvers(0, nil)
- if err != nil {
- log.Fatal(err)
- }
-
- Context.systemResolvers = sysRes
-
- var ourAddrs []string
- ourAddrs, err = collectDNSIPaddrs()
- if err != nil {
- log.Fatal(err)
- }
-
- // TODO(e.burkov): The approach of subtracting sets of strings is not
- // really applicable here since in case of listening on all network
- // interfaces we should check the whole interface's network to cut off
- // all the loopback addresses as well.
- addrs := stringsSetSubtract(sysRes.Get(), ourAddrs)
-
- Context.localResolvers, err = aghnet.NewMultiAddrExchanger(addrs, defaultLocalTimeout)
- if err != nil {
- log.Fatal(err)
- }
-}
-
// run performs configurating and starts AdGuard Home.
func run(args options) {
// configure config filename
@@ -416,8 +310,6 @@ func run(args options) {
log.Fatal(err)
}
- setupResolvers()
-
if !Context.firstRun {
err = initDNSServer()
if err != nil {
diff --git a/internal/home/rdns.go b/internal/home/rdns.go
index 55df779c..a36b0f63 100644
--- a/internal/home/rdns.go
+++ b/internal/home/rdns.go
@@ -2,25 +2,19 @@ package home
import (
"encoding/binary"
- "fmt"
"net"
- "strings"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/agherr"
- "github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
"github.com/AdguardTeam/golibs/cache"
"github.com/AdguardTeam/golibs/log"
- "github.com/miekg/dns"
)
// RDNS resolves clients' addresses to enrich their metadata.
type RDNS struct {
- dnsServer *dnsforward.Server
- clients *clientsContainer
- subnetDetector *aghnet.SubnetDetector
- localResolvers aghnet.Exchanger
+ exchanger dnsforward.RDNSExchanger
+ clients *clientsContainer
// ipCh used to pass client's IP to rDNS workerLoop.
ipCh chan net.IP
@@ -42,16 +36,12 @@ const (
// NewRDNS creates and returns initialized RDNS.
func NewRDNS(
- dnsServer *dnsforward.Server,
+ exchanger dnsforward.RDNSExchanger,
clients *clientsContainer,
- snd *aghnet.SubnetDetector,
- lr aghnet.Exchanger,
) (rDNS *RDNS) {
rDNS = &RDNS{
- dnsServer: dnsServer,
- clients: clients,
- subnetDetector: snd,
- localResolvers: lr,
+ exchanger: exchanger,
+ clients: clients,
ipCache: cache.New(cache.Config{
EnableLRU: true,
MaxCount: defaultRDNSCacheSize,
@@ -92,73 +82,23 @@ func (r *RDNS) Begin(ip net.IP) {
}
}
-const (
- // rDNSEmptyAnswerErr is returned by RDNS resolve method when the answer
- // section of respond is empty.
- rDNSEmptyAnswerErr agherr.Error = "the answer section is empty"
-
- // rDNSNotPTRErr is returned by RDNS resolve method when the response is
- // not of PTR type.
- rDNSNotPTRErr agherr.Error = "the response is not a ptr"
-)
-
-// resolve tries to resolve the ip in a suitable way.
-func (r *RDNS) resolve(ip net.IP) (host string, err error) {
- log.Tracef("rdns: resolving host for %q", ip)
-
- arpa := dns.Fqdn(aghnet.ReverseAddr(ip))
- msg := &dns.Msg{
- MsgHdr: dns.MsgHdr{
- Id: dns.Id(),
- RecursionDesired: true,
- },
- Compress: true,
- Question: []dns.Question{{
- Name: arpa,
- Qtype: dns.TypePTR,
- Qclass: dns.ClassINET,
- }},
- }
-
- var resp *dns.Msg
- if r.subnetDetector.IsLocallyServedNetwork(ip) {
- resp, err = r.localResolvers.Exchange(msg)
- } else {
- resp, err = r.dnsServer.Exchange(msg)
- }
- if err != nil {
- return "", fmt.Errorf("performing lookup for %q: %w", arpa, err)
- }
-
- if len(resp.Answer) == 0 {
- return "", fmt.Errorf("lookup for %q: %w", arpa, rDNSEmptyAnswerErr)
- }
-
- ptr, ok := resp.Answer[0].(*dns.PTR)
- if !ok {
- return "", fmt.Errorf("type checking: %w", rDNSNotPTRErr)
- }
-
- log.Tracef("rdns: ptr response for %q: %s", ip, ptr.String())
-
- return strings.TrimSuffix(ptr.Ptr, "."), nil
-}
-
// workerLoop handles incoming IP addresses from ipChan and adds it into
// clients.
func (r *RDNS) workerLoop() {
defer agherr.LogPanic("rdns")
for ip := range r.ipCh {
- host, err := r.resolve(ip)
+ host, err := r.exchanger.Exchange(ip)
if err != nil {
log.Error("rdns: resolving %q: %s", ip, err)
continue
}
- // Don't handle any errors since AddHost doesn't return non-nil
- // errors for now.
- _, _ = r.clients.AddHost(ip.String(), host, ClientSourceRDNS)
+ if host != "" {
+ // Don't handle any errors since AddHost doesn't return non-nil
+ // errors for now.
+ _, _ = r.clients.AddHost(ip.String(), host, ClientSourceRDNS)
+ }
}
}
diff --git a/internal/home/rdns_test.go b/internal/home/rdns_test.go
index d89c5b11..2779f173 100644
--- a/internal/home/rdns_test.go
+++ b/internal/home/rdns_test.go
@@ -9,15 +9,12 @@ import (
"testing"
"time"
- "github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
- "github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
- "github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/cache"
"github.com/AdguardTeam/golibs/log"
+ "github.com/miekg/dns"
"github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
)
func TestRDNS_Begin(t *testing.T) {
@@ -105,90 +102,30 @@ func TestRDNS_Begin(t *testing.T) {
}
}
-func TestRDNS_Resolve(t *testing.T) {
- extUpstream := &aghtest.TestUpstream{
- Reverse: map[string][]string{
- "1.1.1.1.in-addr.arpa.": {"one.one.one.one"},
- },
- }
- locUpstream := &aghtest.TestUpstream{
- Reverse: map[string][]string{
- "1.1.168.192.in-addr.arpa.": {"local.domain"},
- "2.1.168.192.in-addr.arpa.": {},
- },
- }
- upstreamErr := errors.New("upstream error")
- errUpstream := &aghtest.TestErrUpstream{
- Err: upstreamErr,
- }
- nonPtrUpstream := &aghtest.TestBlockUpstream{
- Hostname: "some-host",
- Block: true,
+// rDNSExchanger is a mock dnsforward.RDNSExchanger implementation for tests.
+type rDNSExchanger struct {
+ aghtest.Exchanger
+}
+
+// Exchange implements dnsforward.RDNSExchanger interface for *RDNSExchanger.
+func (e *rDNSExchanger) Exchange(ip net.IP) (host string, err error) {
+ req := &dns.Msg{
+ Question: []dns.Question{{
+ Name: ip.String(),
+ Qtype: dns.TypePTR,
+ }},
}
- dns := dnsforward.NewCustomServer(&proxy.Proxy{
- Config: proxy.Config{
- UpstreamConfig: &proxy.UpstreamConfig{
- Upstreams: []upstream.Upstream{extUpstream},
- },
- },
- })
-
- cc := &clientsContainer{}
-
- snd, err := aghnet.NewSubnetDetector()
- require.NoError(t, err)
-
- localIP := net.IP{192, 168, 1, 1}
- testCases := []struct {
- name string
- want string
- wantErr error
- locUpstream upstream.Upstream
- req net.IP
- }{{
- name: "external_good",
- want: "one.one.one.one",
- wantErr: nil,
- locUpstream: nil,
- req: net.IP{1, 1, 1, 1},
- }, {
- name: "local_good",
- want: "local.domain",
- wantErr: nil,
- locUpstream: locUpstream,
- req: localIP,
- }, {
- name: "upstream_error",
- want: "",
- wantErr: upstreamErr,
- locUpstream: errUpstream,
- req: localIP,
- }, {
- name: "empty_answer_error",
- want: "",
- wantErr: rDNSEmptyAnswerErr,
- locUpstream: locUpstream,
- req: net.IP{192, 168, 1, 2},
- }, {
- name: "not_ptr_error",
- want: "",
- wantErr: rDNSNotPTRErr,
- locUpstream: nonPtrUpstream,
- req: localIP,
- }}
-
- for _, tc := range testCases {
- rdns := NewRDNS(dns, cc, snd, &aghtest.Exchanger{
- Ups: tc.locUpstream,
- })
-
- t.Run(tc.name, func(t *testing.T) {
- r, rerr := rdns.resolve(tc.req)
- require.ErrorIs(t, rerr, tc.wantErr)
- assert.Equal(t, tc.want, r)
- })
+ resp, err := e.Exchanger.Exchange(req)
+ if err != nil {
+ return "", err
}
+
+ if len(resp.Answer) == 0 {
+ return "", nil
+ }
+
+ return resp.Answer[0].Header().Name, nil
}
func TestRDNS_WorkerLoop(t *testing.T) {
@@ -198,34 +135,33 @@ func TestRDNS_WorkerLoop(t *testing.T) {
locUpstream := &aghtest.TestUpstream{
Reverse: map[string][]string{
- "1.1.168.192.in-addr.arpa.": {"local.domain"},
+ "192.168.1.1": {"local.domain"},
},
}
-
- snd, err := aghnet.NewSubnetDetector()
- require.NoError(t, err)
+ errUpstream := &aghtest.TestErrUpstream{
+ Err: errors.New("1234"),
+ }
testCases := []struct {
+ ups upstream.Upstream
wantLog string
name string
cliIP net.IP
}{{
+ ups: locUpstream,
wantLog: "",
name: "all_good",
cliIP: net.IP{192, 168, 1, 1},
}, {
- wantLog: `rdns: resolving "192.168.1.2": lookup for "2.1.168.192.in-addr.arpa.": ` +
- string(rDNSEmptyAnswerErr),
- name: "resolve_error",
- cliIP: net.IP{192, 168, 1, 2},
+ ups: errUpstream,
+ wantLog: `rdns: resolving "192.168.1.2": errupstream: 1234`,
+ name: "resolve_error",
+ cliIP: net.IP{192, 168, 1, 2},
}}
for _, tc := range testCases {
w.Reset()
- lr := &aghtest.Exchanger{
- Ups: locUpstream,
- }
cc := &clientsContainer{
list: map[string]*Client{},
idIndex: map[string]*Client{},
@@ -234,11 +170,13 @@ func TestRDNS_WorkerLoop(t *testing.T) {
}
ch := make(chan net.IP)
rdns := &RDNS{
- dnsServer: nil,
- clients: cc,
- subnetDetector: snd,
- localResolvers: lr,
- ipCh: ch,
+ exchanger: &rDNSExchanger{
+ Exchanger: aghtest.Exchanger{
+ Ups: tc.ups,
+ },
+ },
+ clients: cc,
+ ipCh: ch,
}
t.Run(tc.name, func(t *testing.T) {
diff --git a/internal/home/whois.go b/internal/home/whois.go
index f2923815..469f2e5d 100644
--- a/internal/home/whois.go
+++ b/internal/home/whois.go
@@ -10,7 +10,7 @@ import (
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghio"
- "github.com/AdguardTeam/AdGuardHome/internal/util"
+ "github.com/AdguardTeam/AdGuardHome/internal/aghstrings"
"github.com/AdguardTeam/golibs/cache"
"github.com/AdguardTeam/golibs/log"
)
@@ -67,7 +67,7 @@ func whoisParse(data string) map[string]string {
descr := ""
netname := ""
for len(data) != 0 {
- ln := util.SplitNext(&data, '\n')
+ ln := aghstrings.SplitNext(&data, '\n')
if len(ln) == 0 || ln[0] == '#' || ln[0] == '%' {
continue
}
diff --git a/internal/querylog/http.go b/internal/querylog/http.go
index d3fcd63e..9fdf3a3d 100644
--- a/internal/querylog/http.go
+++ b/internal/querylog/http.go
@@ -8,6 +8,7 @@ import (
"strconv"
"time"
+ "github.com/AdguardTeam/AdGuardHome/internal/aghstrings"
"github.com/AdguardTeam/golibs/jsonutil"
"github.com/AdguardTeam/golibs/log"
)
@@ -125,17 +126,6 @@ func getDoubleQuotesEnclosedValue(s *string) bool {
return false
}
-// inStr checks if string is in the slice of strings.
-func inStr(strs []string, str string) (ok bool) {
- for _, s := range strs {
- if s == str {
- return true
- }
- }
-
- return false
-}
-
// parseSearchCriteria - parses "searchCriteria" from the specified query parameter
func (l *queryLog) parseSearchCriteria(q url.Values, name string, ct criteriaType) (bool, searchCriteria, error) {
val := q.Get(name)
@@ -151,7 +141,7 @@ func (l *queryLog) parseSearchCriteria(q url.Values, name string, ct criteriaTyp
c.strict = true
}
- if ct == ctFilteringStatus && !inStr(filteringStatusValues, c.value) {
+ if ct == ctFilteringStatus && !aghstrings.InSlice(filteringStatusValues, c.value) {
return false, c, fmt.Errorf("invalid value %s", c.value)
}
diff --git a/internal/util/helpers.go b/internal/util/helpers.go
index 7add9617..f97635b7 100644
--- a/internal/util/helpers.go
+++ b/internal/util/helpers.go
@@ -12,30 +12,6 @@ import (
"strings"
)
-// SplitNext - split string by a byte and return the first chunk
-// Skip empty chunks
-// Whitespace is trimmed
-func SplitNext(str *string, splitBy byte) string {
- i := strings.IndexByte(*str, splitBy)
- s := ""
- if i != -1 {
- s = (*str)[0:i]
- *str = (*str)[i+1:]
- k := 0
- ch := rune(0)
- for k, ch = range *str {
- if byte(ch) != splitBy {
- break
- }
- }
- *str = (*str)[k:]
- } else {
- s = *str
- *str = ""
- }
- return strings.TrimSpace(s)
-}
-
// IsOpenWrt returns true if host OS is OpenWrt.
func IsOpenWrt() bool {
if runtime.GOOS != "linux" {
diff --git a/internal/util/helpers_test.go b/internal/util/helpers_test.go
deleted file mode 100644
index a09d97e6..00000000
--- a/internal/util/helpers_test.go
+++ /dev/null
@@ -1,17 +0,0 @@
-package util
-
-import (
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestSplitNext(t *testing.T) {
- s := " a,b , c "
-
- assert.Equal(t, "a", SplitNext(&s, ','))
- assert.Equal(t, "b", SplitNext(&s, ','))
- assert.Equal(t, "c", SplitNext(&s, ','))
- require.Empty(t, s)
-}
diff --git a/internal/version/version.go b/internal/version/version.go
index 7d0a28e9..328ab7db 100644
--- a/internal/version/version.go
+++ b/internal/version/version.go
@@ -7,6 +7,8 @@ import (
"runtime/debug"
"strconv"
"strings"
+
+ "github.com/AdguardTeam/AdGuardHome/internal/aghstrings"
)
// Channel constants.
@@ -68,14 +70,6 @@ const (
nltb = nl + tb
)
-// writeStrings is a convenient wrapper for strings.(*Builder).WriteString that
-// deals with multiple strings and ignores errors that are guaranteed to be nil.
-func writeStrings(b *strings.Builder, strs ...string) {
- for _, s := range strs {
- _, _ = b.WriteString(s)
- }
-}
-
// Constants defining the format of module information string.
const (
modInfoAtSep = "@"
@@ -99,16 +93,16 @@ func fmtModule(m *debug.Module) (formatted string) {
b := &strings.Builder{}
- writeStrings(b, m.Path)
+ aghstrings.WriteToBuilder(b, m.Path)
if ver := m.Version; ver != "" {
sep := modInfoAtSep
if ver == "(devel)" {
sep = modInfoDevSep
}
- writeStrings(b, sep, ver)
+ aghstrings.WriteToBuilder(b, sep, ver)
}
if sum := m.Sum; sum != "" {
- writeStrings(b, modInfoSumLeft, sum, modInfoSumRight)
+ aghstrings.WriteToBuilder(b, modInfoSumLeft, sum, modInfoSumRight)
}
return b.String()
@@ -149,7 +143,7 @@ const (
func Verbose() (v string) {
b := &strings.Builder{}
- writeStrings(
+ aghstrings.WriteToBuilder(
b,
vFmtAGHHdr,
nl,
@@ -163,31 +157,31 @@ func Verbose() (v string) {
runtime.Version(),
)
if buildtime != "" {
- writeStrings(b, nl, vFmtTimeHdr, buildtime)
+ aghstrings.WriteToBuilder(b, nl, vFmtTimeHdr, buildtime)
}
- writeStrings(b, nl, vFmtGOOSHdr, nl, vFmtGOARCHHdr)
+ aghstrings.WriteToBuilder(b, nl, vFmtGOOSHdr, nl, vFmtGOARCHHdr)
if goarm != "" {
- writeStrings(b, nl, vFmtGOARMHdr, "v", goarm)
+ aghstrings.WriteToBuilder(b, nl, vFmtGOARMHdr, "v", goarm)
} else if gomips != "" {
- writeStrings(b, nl, vFmtGOMIPSHdr, gomips)
+ aghstrings.WriteToBuilder(b, nl, vFmtGOMIPSHdr, gomips)
}
- writeStrings(b, nl, vFmtRaceHdr, strconv.FormatBool(isRace))
+ aghstrings.WriteToBuilder(b, nl, vFmtRaceHdr, strconv.FormatBool(isRace))
info, ok := debug.ReadBuildInfo()
if !ok {
return b.String()
}
- writeStrings(b, nl, vFmtMainHdr, nltb, fmtModule(&info.Main))
+ aghstrings.WriteToBuilder(b, nl, vFmtMainHdr, nltb, fmtModule(&info.Main))
if len(info.Deps) == 0 {
return b.String()
}
- writeStrings(b, nl, vFmtDepsHdr)
+ aghstrings.WriteToBuilder(b, nl, vFmtDepsHdr)
for _, dep := range info.Deps {
if depStr := fmtModule(dep); depStr != "" {
- writeStrings(b, nltb, depStr)
+ aghstrings.WriteToBuilder(b, nltb, depStr)
}
}
diff --git a/openapi/CHANGELOG.md b/openapi/CHANGELOG.md
index cd42f521..071c4177 100644
--- a/openapi/CHANGELOG.md
+++ b/openapi/CHANGELOG.md
@@ -4,6 +4,21 @@
## v0.106: API changes
+## New `"private_upstream"` field in `POST /test_upstream_dns`
+
+* The new optional field `"private_upstream"` of `UpstreamConfig` contains the
+ upstream servers for resolving locally-served ip addresses to be checked.
+
+### New fields `"resolve_clients"` and `"local_ptr_upstreams"` in DNS configuration
+
+* The new optional field `"resolve_clients"` of `DNSConfig` is used to turn
+ resolving clients' addresses on and off.
+
+* The new optional field `"local_ptr_upstreams"` of `"DNSConfig"` contains the
+ upstream servers for resolving addresses from locally-served networks. The
+ empty `"local_ptr_resolvers"` states that AGH should use resolvers provided by
+ the operating system.
+
### New `"client_info"` field in `GET /querylog` response
* The new optional field `"client_info"` of `QueryLogItem` objects contains
diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml
index 2717f171..7c9aeaea 100644
--- a/openapi/openapi.yaml
+++ b/openapi/openapi.yaml
@@ -1294,6 +1294,18 @@
- ''
- 'parallel'
- 'fastest_addr'
+ 'resolve_clients':
+ 'type': 'boolean'
+ 'local_ptr_upstreams':
+ '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'
'UpstreamsConfig':
'type': 'object'
'description': 'Upstreams configuration'
@@ -1321,6 +1333,16 @@
'example':
- 'tls://1.1.1.1'
- 'tls://1.0.0.1'
+ 'private_upstream':
+ 'type': 'array'
+ 'description': >
+ Local PTR resolvers, 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'
'UpstreamsConfigResponse':
'type': 'object'
'description': 'Upstreams configuration response'