mirror of
https://github.com/AdguardTeam/AdGuardHome.git
synced 2024-12-24 23:58:17 +03:00
ff7c715c5f
Closes #6854.Updates #6875. Squashed commit of the following: commit b98adbc0cc6eeaffb262d57775c487e03b1d5ba5 Author: Eugene Burkov <E.Burkov@AdGuard.COM> Date: Wed Apr 10 19:21:44 2024 +0300 dnsforward: upd proxy, imp code, docs commit 4de1eb2bca1047426e02ba680c212f46782e5616 Author: Eugene Burkov <E.Burkov@AdGuard.COM> Date: Wed Apr 10 16:09:58 2024 +0300 WIP commit afa9d61e8dc129f907dc681cd2f831cb5c3b054a Author: Eugene Burkov <E.Burkov@AdGuard.COM> Date: Tue Apr 9 19:24:09 2024 +0300 all: log changes commit c8340676a448687a39acd26bc8ce5f94473e441f Author: Eugene Burkov <E.Burkov@AdGuard.COM> Date: Tue Apr 9 19:06:10 2024 +0300 dnsforward: move code commit 08bb7d43d2a3f689ef2ef2409935dc3946752e94 Author: Eugene Burkov <E.Burkov@AdGuard.COM> Date: Tue Apr 9 18:09:46 2024 +0300 dnsforward: imp code commit b27547ec806dd9bce502d3c6a7c28f33693ed575 Merge: b7efca7886f36ebc06
Author: Eugene Burkov <E.Burkov@AdGuard.COM> Date: Tue Apr 9 17:33:19 2024 +0300 Merge branch 'master' into AGDNS-1982-upd-proxy commit b7efca788b66aa672598b088040d4534ce2e55b0 Author: Eugene Burkov <E.Burkov@AdGuard.COM> Date: Tue Apr 9 17:27:14 2024 +0300 all: upd proxy finally commit 3e16fa87befe4c0ef3a3e7a638d7add28627f9b6 Author: Eugene Burkov <E.Burkov@AdGuard.COM> Date: Fri Apr 5 18:20:13 2024 +0300 dnsforward: upd proxy commit f3cdfc86334a182effcd0de22fac5e678fa53ea7 Author: Eugene Burkov <E.Burkov@AdGuard.COM> Date: Thu Apr 4 20:37:32 2024 +0300 all: upd proxy, golibs commit a79298d6d0504521893ee11fdc3a23c098aea911 Merge: 9feeba5c7fd25dcacb
Author: Eugene Burkov <E.Burkov@AdGuard.COM> Date: Thu Apr 4 20:34:01 2024 +0300 Merge branch 'master' into AGDNS-1982-upd-proxy commit 9feeba5c7f24ff1d308a216608d985cb2a7b7588 Author: Eugene Burkov <E.Burkov@AdGuard.COM> Date: Thu Apr 4 20:25:57 2024 +0300 all: imp code, docs commit 6c68d463db64293eb9c5e29ff91879fd68920a77 Merge: d8108e651ee619b2db
Author: Eugene Burkov <E.Burkov@AdGuard.COM> Date: Thu Apr 4 18:46:11 2024 +0300 Merge branch 'master' into AGDNS-1982-upd-proxy commit d8108e65164df8d67aa4e95154a8768a06255b78 Author: Eugene Burkov <E.Burkov@AdGuard.COM> Date: Wed Apr 3 19:25:27 2024 +0300 all: imp code commit 20461565801c9fcd06a652c6066b524b06c80433 Author: Eugene Burkov <E.Burkov@AdGuard.COM> Date: Wed Apr 3 17:10:33 2024 +0300 all: remove private rdns logic
481 lines
12 KiB
Go
481 lines
12 KiB
Go
package dnsforward
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/netip"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"testing/fstest"
|
|
"time"
|
|
|
|
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
|
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
|
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
|
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
|
"github.com/AdguardTeam/dnsproxy/upstream"
|
|
"github.com/AdguardTeam/golibs/httphdr"
|
|
"github.com/AdguardTeam/golibs/netutil"
|
|
"github.com/AdguardTeam/golibs/testutil"
|
|
"github.com/miekg/dns"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TODO(e.burkov): Use the better approach to testdata with a separate
|
|
// directory for each test, and a separate file for each subtest. See the
|
|
// [configmigrate] package.
|
|
|
|
// emptySysResolvers is an empty [SystemResolvers] implementation that always
|
|
// returns nil.
|
|
type emptySysResolvers struct{}
|
|
|
|
// Addrs implements the aghnet.SystemResolvers interface for emptySysResolvers.
|
|
func (emptySysResolvers) Addrs() (addrs []netip.AddrPort) {
|
|
return nil
|
|
}
|
|
|
|
func loadTestData(t *testing.T, casesFileName string, cases any) {
|
|
t.Helper()
|
|
|
|
var f *os.File
|
|
f, err := os.Open(filepath.Join("testdata", casesFileName))
|
|
require.NoError(t, err)
|
|
testutil.CleanupAndRequireSuccess(t, f.Close)
|
|
|
|
err = json.NewDecoder(f).Decode(cases)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
const (
|
|
jsonExt = ".json"
|
|
|
|
// testBlockedRespTTL is the TTL for blocked responses to use in tests.
|
|
testBlockedRespTTL = 10
|
|
)
|
|
|
|
func TestDNSForwardHTTP_handleGetConfig(t *testing.T) {
|
|
filterConf := &filtering.Config{
|
|
ProtectionEnabled: true,
|
|
BlockingMode: filtering.BlockingModeDefault,
|
|
BlockedResponseTTL: testBlockedRespTTL,
|
|
SafeBrowsingEnabled: true,
|
|
SafeBrowsingCacheSize: 1000,
|
|
SafeSearchConf: filtering.SafeSearchConfig{Enabled: true},
|
|
SafeSearchCacheSize: 1000,
|
|
ParentalCacheSize: 1000,
|
|
CacheTime: 30,
|
|
}
|
|
forwardConf := ServerConfig{
|
|
UDPListenAddrs: []*net.UDPAddr{},
|
|
TCPListenAddrs: []*net.TCPAddr{},
|
|
Config: Config{
|
|
UpstreamDNS: []string{"8.8.8.8:53", "8.8.4.4:53"},
|
|
FallbackDNS: []string{"9.9.9.10"},
|
|
RatelimitSubnetLenIPv4: 24,
|
|
RatelimitSubnetLenIPv6: 56,
|
|
UpstreamMode: UpstreamModeLoadBalance,
|
|
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
|
|
},
|
|
ConfigModified: func() {},
|
|
ServePlainDNS: true,
|
|
}
|
|
s := createTestServer(t, filterConf, forwardConf)
|
|
s.sysResolvers = &emptySysResolvers{}
|
|
|
|
require.NoError(t, s.Start())
|
|
testutil.CleanupAndRequireSuccess(t, s.Stop)
|
|
|
|
defaultConf := s.conf
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
testCases := []struct {
|
|
conf func() ServerConfig
|
|
name string
|
|
}{{
|
|
conf: func() ServerConfig {
|
|
return defaultConf
|
|
},
|
|
name: "all_right",
|
|
}, {
|
|
conf: func() ServerConfig {
|
|
conf := defaultConf
|
|
conf.UpstreamMode = UpstreamModeFastestAddr
|
|
|
|
return conf
|
|
},
|
|
name: "fastest_addr",
|
|
}, {
|
|
conf: func() ServerConfig {
|
|
conf := defaultConf
|
|
conf.UpstreamMode = UpstreamModeParallel
|
|
|
|
return conf
|
|
},
|
|
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)
|
|
|
|
s.conf = tc.conf()
|
|
s.handleGetConfig(w, nil)
|
|
|
|
cType := w.Header().Get(httphdr.ContentType)
|
|
assert.Equal(t, aghhttp.HdrValApplicationJSON, cType)
|
|
assert.JSONEq(t, string(caseWant), w.Body.String())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDNSForwardHTTP_handleSetConfig(t *testing.T) {
|
|
filterConf := &filtering.Config{
|
|
ProtectionEnabled: true,
|
|
BlockingMode: filtering.BlockingModeDefault,
|
|
BlockedResponseTTL: testBlockedRespTTL,
|
|
SafeBrowsingEnabled: true,
|
|
SafeBrowsingCacheSize: 1000,
|
|
SafeSearchConf: filtering.SafeSearchConfig{Enabled: true},
|
|
SafeSearchCacheSize: 1000,
|
|
ParentalCacheSize: 1000,
|
|
CacheTime: 30,
|
|
}
|
|
forwardConf := ServerConfig{
|
|
UDPListenAddrs: []*net.UDPAddr{},
|
|
TCPListenAddrs: []*net.TCPAddr{},
|
|
Config: Config{
|
|
UpstreamDNS: []string{"8.8.8.8:53", "8.8.4.4:53"},
|
|
RatelimitSubnetLenIPv4: 24,
|
|
RatelimitSubnetLenIPv6: 56,
|
|
UpstreamMode: UpstreamModeLoadBalance,
|
|
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
|
|
},
|
|
ConfigModified: func() {},
|
|
ServePlainDNS: true,
|
|
}
|
|
s := createTestServer(t, filterConf, forwardConf)
|
|
s.sysResolvers = &emptySysResolvers{}
|
|
|
|
defaultConf := s.conf
|
|
|
|
err := s.Start()
|
|
assert.NoError(t, err)
|
|
testutil.CleanupAndRequireSuccess(t, s.Stop)
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
testCases := []struct {
|
|
name string
|
|
wantSet string
|
|
}{{
|
|
name: "upstream_dns",
|
|
wantSet: "",
|
|
}, {
|
|
name: "bootstraps",
|
|
wantSet: "",
|
|
}, {
|
|
name: "blocking_mode_good",
|
|
wantSet: "",
|
|
}, {
|
|
name: "blocking_mode_bad",
|
|
wantSet: "validating dns config: " +
|
|
"blocking_ipv4 must be valid ipv4 on custom_ip blocking_mode",
|
|
}, {
|
|
name: "ratelimit",
|
|
wantSet: "",
|
|
}, {
|
|
name: "ratelimit_subnet_len",
|
|
wantSet: "",
|
|
}, {
|
|
name: "ratelimit_whitelist_not_ip",
|
|
wantSet: `decoding request: ParseAddr("not.ip"): unexpected character (at "not.ip")`,
|
|
}, {
|
|
name: "edns_cs_enabled",
|
|
wantSet: "",
|
|
}, {
|
|
name: "edns_cs_use_custom",
|
|
wantSet: "",
|
|
}, {
|
|
name: "edns_cs_use_custom_bad_ip",
|
|
wantSet: "decoding request: ParseAddr(\"bad.ip\"): unexpected character (at \"bad.ip\")",
|
|
}, {
|
|
name: "dnssec_enabled",
|
|
wantSet: "",
|
|
}, {
|
|
name: "cache_size",
|
|
wantSet: "",
|
|
}, {
|
|
name: "upstream_mode_parallel",
|
|
wantSet: "",
|
|
}, {
|
|
name: "upstream_mode_fastest_addr",
|
|
wantSet: "",
|
|
}, {
|
|
name: "upstream_dns_bad",
|
|
wantSet: `validating dns config: upstream servers: parsing error at index 0: ` +
|
|
`cannot prepare the upstream: invalid address !!!: bad hostname "!!!": ` +
|
|
`bad top-level domain name label "!!!": bad top-level domain name label rune '!'`,
|
|
}, {
|
|
name: "bootstraps_bad",
|
|
wantSet: `validating dns config: checking bootstrap a: not a bootstrap: ParseAddr("a"): ` +
|
|
`unable to parse IP`,
|
|
}, {
|
|
name: "cache_bad_ttl",
|
|
wantSet: `validating dns config: cache_ttl_min must be less than or equal to cache_ttl_max`,
|
|
}, {
|
|
name: "upstream_mode_bad",
|
|
wantSet: `validating dns config: upstream_mode: incorrect value "somethingelse"`,
|
|
}, {
|
|
name: "local_ptr_upstreams_good",
|
|
wantSet: "",
|
|
}, {
|
|
name: "local_ptr_upstreams_bad",
|
|
wantSet: `validating dns config: private upstream servers: ` +
|
|
`bad arpa domain name "non.arpa": not a reversed ip network`,
|
|
}, {
|
|
name: "local_ptr_upstreams_null",
|
|
wantSet: "",
|
|
}, {
|
|
name: "fallbacks",
|
|
wantSet: "",
|
|
}, {
|
|
name: "blocked_response_ttl",
|
|
wantSet: "",
|
|
}, {
|
|
name: "multiple_domain_specific_upstreams",
|
|
wantSet: "",
|
|
}}
|
|
|
|
var data map[string]struct {
|
|
Req json.RawMessage `json:"req"`
|
|
Want json.RawMessage `json:"want"`
|
|
}
|
|
|
|
testData := t.Name() + jsonExt
|
|
loadTestData(t, testData, &data)
|
|
|
|
for _, tc := range testCases {
|
|
// NOTE: Do not use require.Contains, because the size of the data
|
|
// prevents it from printing a meaningful error message.
|
|
caseData, ok := data[tc.name]
|
|
require.Truef(t, ok, "%q does not contain test data for test case %s", testData, tc.name)
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Cleanup(func() {
|
|
s.dnsFilter.SetBlockingMode(filtering.BlockingModeDefault, netip.Addr{}, netip.Addr{})
|
|
s.conf = defaultConf
|
|
s.conf.Config.EDNSClientSubnet = &EDNSClientSubnet{}
|
|
s.dnsFilter.SetBlockedResponseTTL(testBlockedRespTTL)
|
|
})
|
|
|
|
rBody := io.NopCloser(bytes.NewReader(caseData.Req))
|
|
var r *http.Request
|
|
r, err = http.NewRequest(http.MethodPost, "http://example.com", rBody)
|
|
require.NoError(t, err)
|
|
|
|
s.handleSetConfig(w, r)
|
|
assert.Equal(t, tc.wantSet, strings.TrimSuffix(w.Body.String(), "\n"))
|
|
w.Body.Reset()
|
|
|
|
s.handleGetConfig(w, nil)
|
|
assert.JSONEq(t, string(caseData.Want), w.Body.String())
|
|
w.Body.Reset()
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsCommentOrEmpty(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
want assert.BoolAssertionFunc
|
|
str string
|
|
}{{
|
|
want: assert.True,
|
|
str: "",
|
|
}, {
|
|
want: assert.True,
|
|
str: "# comment",
|
|
}, {
|
|
want: assert.False,
|
|
str: "1.2.3.4",
|
|
}} {
|
|
tc.want(t, IsCommentOrEmpty(tc.str))
|
|
}
|
|
}
|
|
|
|
func newLocalUpstreamListener(t *testing.T, port uint16, handler dns.Handler) (real netip.AddrPort) {
|
|
t.Helper()
|
|
|
|
startCh := make(chan struct{})
|
|
upsSrv := &dns.Server{
|
|
Addr: netip.AddrPortFrom(netutil.IPv4Localhost(), port).String(),
|
|
Net: "tcp",
|
|
Handler: handler,
|
|
NotifyStartedFunc: func() { close(startCh) },
|
|
}
|
|
go func() {
|
|
err := upsSrv.ListenAndServe()
|
|
require.NoError(testutil.PanicT{}, err)
|
|
}()
|
|
|
|
<-startCh
|
|
testutil.CleanupAndRequireSuccess(t, upsSrv.Shutdown)
|
|
|
|
return testutil.RequireTypeAssert[*net.TCPAddr](t, upsSrv.Listener.Addr()).AddrPort()
|
|
}
|
|
|
|
func TestServer_HandleTestUpstreamDNS(t *testing.T) {
|
|
hdlr := dns.HandlerFunc(func(w dns.ResponseWriter, m *dns.Msg) {
|
|
err := w.WriteMsg(new(dns.Msg).SetReply(m))
|
|
require.NoError(testutil.PanicT{}, err)
|
|
})
|
|
|
|
ups := (&url.URL{
|
|
Scheme: "tcp",
|
|
Host: newLocalUpstreamListener(t, 0, hdlr).String(),
|
|
}).String()
|
|
|
|
const (
|
|
upsTimeout = 100 * time.Millisecond
|
|
|
|
hostsFileName = "hosts"
|
|
upstreamHost = "custom.localhost"
|
|
)
|
|
|
|
hostsListener := newLocalUpstreamListener(t, 0, hdlr)
|
|
hostsUps := (&url.URL{
|
|
Scheme: "tcp",
|
|
Host: netutil.JoinHostPort(upstreamHost, hostsListener.Port()),
|
|
}).String()
|
|
|
|
hc, err := aghnet.NewHostsContainer(
|
|
fstest.MapFS{
|
|
hostsFileName: &fstest.MapFile{
|
|
Data: []byte(hostsListener.Addr().String() + " " + upstreamHost),
|
|
},
|
|
},
|
|
&aghtest.FSWatcher{
|
|
OnStart: func() (_ error) { panic("not implemented") },
|
|
OnEvents: func() (e <-chan struct{}) { return nil },
|
|
OnAdd: func(_ string) (err error) { return nil },
|
|
OnClose: func() (err error) { return nil },
|
|
},
|
|
hostsFileName,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
srv := createTestServer(t, &filtering.Config{
|
|
BlockingMode: filtering.BlockingModeDefault,
|
|
EtcHosts: hc,
|
|
}, ServerConfig{
|
|
UDPListenAddrs: []*net.UDPAddr{{}},
|
|
TCPListenAddrs: []*net.TCPAddr{{}},
|
|
UpstreamTimeout: upsTimeout,
|
|
Config: Config{
|
|
UpstreamMode: UpstreamModeLoadBalance,
|
|
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
|
|
},
|
|
ServePlainDNS: true,
|
|
})
|
|
srv.etcHosts = upstream.NewHostsResolver(hc)
|
|
startDeferStop(t, srv)
|
|
|
|
testCases := []struct {
|
|
body map[string]any
|
|
wantResp map[string]any
|
|
name string
|
|
}{{
|
|
body: map[string]any{
|
|
"upstream_dns": []string{hostsUps},
|
|
},
|
|
wantResp: map[string]any{
|
|
hostsUps: "OK",
|
|
},
|
|
name: "etc_hosts",
|
|
}, {
|
|
body: map[string]any{
|
|
"upstream_dns": []string{ups, "#this.is.comment"},
|
|
},
|
|
wantResp: map[string]any{
|
|
ups: "OK",
|
|
},
|
|
name: "comment_mix",
|
|
}}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
var reqBody []byte
|
|
reqBody, err = json.Marshal(tc.body)
|
|
require.NoError(t, err)
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
var r *http.Request
|
|
r, err = http.NewRequest(http.MethodPost, "", bytes.NewReader(reqBody))
|
|
require.NoError(t, err)
|
|
|
|
srv.handleTestUpstreamDNS(w, r)
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
|
|
resp := map[string]any{}
|
|
err = json.NewDecoder(w.Body).Decode(&resp)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, tc.wantResp, resp)
|
|
})
|
|
}
|
|
|
|
t.Run("timeout", func(t *testing.T) {
|
|
slowHandler := dns.HandlerFunc(func(w dns.ResponseWriter, m *dns.Msg) {
|
|
time.Sleep(upsTimeout * 2)
|
|
writeErr := w.WriteMsg(new(dns.Msg).SetReply(m))
|
|
require.NoError(testutil.PanicT{}, writeErr)
|
|
})
|
|
sleepyUps := (&url.URL{
|
|
Scheme: "tcp",
|
|
Host: newLocalUpstreamListener(t, 0, slowHandler).String(),
|
|
}).String()
|
|
|
|
req := map[string]any{
|
|
"upstream_dns": []string{sleepyUps},
|
|
}
|
|
|
|
var reqBody []byte
|
|
reqBody, err = json.Marshal(req)
|
|
require.NoError(t, err)
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
var r *http.Request
|
|
r, err = http.NewRequest(http.MethodPost, "", bytes.NewReader(reqBody))
|
|
require.NoError(t, err)
|
|
|
|
srv.handleTestUpstreamDNS(w, r)
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
|
|
resp := map[string]any{}
|
|
err = json.NewDecoder(w.Body).Decode(&resp)
|
|
require.NoError(t, err)
|
|
|
|
require.Contains(t, resp, sleepyUps)
|
|
require.IsType(t, "", resp[sleepyUps])
|
|
sleepyRes, _ := resp[sleepyUps].(string)
|
|
|
|
// TODO(e.burkov): Improve the format of an error in dnsproxy.
|
|
assert.True(t, strings.HasSuffix(sleepyRes, "i/o timeout"))
|
|
})
|
|
}
|