diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d1f9f32..38ec0381 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,9 @@ and this project adheres to ### Added +- The ability to set up custom upstreams to resolve PTR queries for local + addresses and to disable the automatic resolving of clients' addresses + ([#2704]). - Logging of the client's IP address after failed login attempts ([#2824]). - Search by clients' names in the query log ([#1273]). - Verbose version output with `-v --version` ([#2416]). diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json index 60d7a607..78f73675 100644 --- a/client/src/__locales/en.json +++ b/client/src/__locales/en.json @@ -8,6 +8,11 @@ "load_balancing_desc": "Query one upstream server at a time. AdGuard Home will use the weighted random algorithm to pick the server so that the fastest server is used more often.", "bootstrap_dns": "Bootstrap DNS servers", "bootstrap_dns_desc": "Bootstrap DNS servers are used to resolve IP addresses of the DoH/DoT resolvers you specify as upstreams.", + "local_ptr_title": "Private DNS servers", + "local_ptr_desc": "The DNS server or servers that AdGuard Home will use for queries for locally served resources. For instance, this server will be used for resolving clients' hostnames for the clients with private IP addresses. If not set, AdGuard Home will automatically use your default DNS resolver.", + "local_ptr_placeholder": "Enter one server address per line", + "resolve_clients_title": "Enable clients' hostnames resolution", + "resolve_clients_desc": "If enabled, AdGuard Home will attempt to automatically resolve clients' hostnames from their IP addresses by sending a PTR query to a corresponding resolver (private DNS server for local clients, upstream server for clients with public IP).", "check_dhcp_servers": "Check for DHCP servers", "save_config": "Save configuration", "enabled_dhcp": "DHCP server enabled", diff --git a/client/src/actions/dnsConfig.js b/client/src/actions/dnsConfig.js index c599ca8d..a0b93428 100644 --- a/client/src/actions/dnsConfig.js +++ b/client/src/actions/dnsConfig.js @@ -33,6 +33,10 @@ export const setDnsConfig = (config) => async (dispatch) => { data.bootstrap_dns = splitByNewLine(config.bootstrap_dns); hasDnsSettings = true; } + if (Object.prototype.hasOwnProperty.call(data, 'local_ptr_upstreams')) { + data.local_ptr_upstreams = splitByNewLine(config.local_ptr_upstreams); + hasDnsSettings = true; + } if (Object.prototype.hasOwnProperty.call(data, 'upstream_dns')) { data.upstream_dns = splitByNewLine(config.upstream_dns); hasDnsSettings = true; diff --git a/client/src/actions/index.js b/client/src/actions/index.js index 58e2d018..31179de7 100644 --- a/client/src/actions/index.js +++ b/client/src/actions/index.js @@ -296,7 +296,7 @@ export const testUpstreamFailure = createAction('TEST_UPSTREAM_FAILURE'); export const testUpstreamSuccess = createAction('TEST_UPSTREAM_SUCCESS'); export const testUpstream = ( - { bootstrap_dns, upstream_dns }, upstream_dns_file, + { bootstrap_dns, upstream_dns, local_ptr_upstreams }, upstream_dns_file, ) => async (dispatch) => { dispatch(testUpstreamRequest()); try { @@ -304,6 +304,7 @@ export const testUpstream = ( const config = { bootstrap_dns: splitByNewLine(bootstrap_dns), + private_upstream: splitByNewLine(local_ptr_upstreams), ...(upstream_dns_file ? null : { upstream_dns: removeComments(upstream_dns), }), @@ -332,8 +333,17 @@ export const testUpstream = ( export const testUpstreamWithFormValues = () => async (dispatch, getState) => { const { upstream_dns_file } = getState().dnsConfig; - const { bootstrap_dns, upstream_dns } = getState().form[FORM_NAME.UPSTREAM].values; - return dispatch(testUpstream({ bootstrap_dns, upstream_dns }, upstream_dns_file)); + const { + bootstrap_dns, + upstream_dns, + local_ptr_upstreams, + } = getState().form[FORM_NAME.UPSTREAM].values; + + return dispatch(testUpstream({ + bootstrap_dns, + upstream_dns, + local_ptr_upstreams, + }, upstream_dns_file)); }; export const changeLanguageRequest = createAction('CHANGE_LANGUAGE_REQUEST'); diff --git a/client/src/components/Settings/Dns/Upstream/Form.js b/client/src/components/Settings/Dns/Upstream/Form.js index d44a8018..33b746e3 100644 --- a/client/src/components/Settings/Dns/Upstream/Form.js +++ b/client/src/components/Settings/Dns/Upstream/Form.js @@ -5,7 +5,7 @@ import { Field, reduxForm } from 'redux-form'; import { Trans, useTranslation } from 'react-i18next'; import classnames from 'classnames'; import Examples from './Examples'; -import { renderRadioField, renderTextareaField } from '../../../../helpers/form'; +import { renderRadioField, renderTextareaField, CheckboxField } from '../../../../helpers/form'; import { DNS_REQUEST_OPTIONS, FORM_NAME, @@ -28,11 +28,12 @@ const renderField = ({ const processingTestUpstream = useSelector((state) => state.settings.processingTestUpstream); const processingSetConfig = useSelector((state) => state.dnsConfig.processingSetConfig); - return
- + -
; + /> + + ); }; renderField.propTypes = { @@ -160,10 +162,10 @@ const Form = ({ {' '} DNS providers , @@ -197,6 +199,40 @@ const Form = ({ normalizeOnBlur={removeEmptyLines} /> +
+
+
+
+ +
+ local_ptr_desc +
+ +
+
+ +
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'