Pull request: 5193-long-ups-check

Merge in DNS/adguard-home from 5193-long-ups-check to master

Closes #5193.

Squashed commit of the following:

commit 787e6b9950e85b79fe50e16cda5110fe53ec3e80
Merge: f62330bd 01652e6a
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Mon Dec 5 17:07:40 2022 +0300

    Merge branch 'master' into 5193-long-ups-check

commit f62330bd17ec260bc8c7475c8f5ae236059cde35
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Mon Dec 5 16:28:47 2022 +0300

    dnsforward: try to fix linux

commit 64bfacc58d2a4c2929d9c3cf80bc31bfca404d54
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Mon Dec 5 15:55:48 2022 +0300

    all: log changes finally

commit 4331d1c2497a94a95e4eba0ebcb5a813260c188a
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Mon Dec 5 15:26:45 2022 +0300

    all: imp log of chagnes

commit 62ed3c123eda100813a2de2ed95c1446c7a100f0
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Mon Dec 5 15:23:17 2022 +0300

    all: log changes

commit 73f4d59796dc5de51cf9c9953a6a22342e1ce31a
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Mon Dec 5 14:37:18 2022 +0300

    dnsforward: add defer

commit a15072f1ea3845ba135ddd61aa8d67d9c0dcd7ea
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Mon Dec 5 14:30:16 2022 +0300

    dnsforward: imp tests

commit e74219f594094f1e3d0001664ed3f79050747a4d
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Sat Dec 3 16:29:31 2022 +0300

    dnsforward: get rid of wg

commit 165da7dc186285d6ff8b949e12d95e0da5e828eb
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Fri Dec 2 15:42:55 2022 +0300

    dnsforward: add ups check test

commit 3045273997e45e952ba58e9c7fa5bba2d21ad286
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Thu Dec 1 20:28:56 2022 +0300

    dnsforward: imp ups check perf
This commit is contained in:
Eugene Burkov 2022-12-05 17:24:32 +03:00
parent 01652e6ab2
commit 6d1adf74b1
3 changed files with 179 additions and 26 deletions

View File

@ -36,6 +36,7 @@ See also the [v0.107.20 GitHub milestone][ms-v0.107.20].
### Fixed ### Fixed
- Slow upstream checks making the API unresponsive ([#5193]).
- The TLS initialization errors preventing AdGuard Home from starting ([#5189]). - The TLS initialization errors preventing AdGuard Home from starting ([#5189]).
Instead, AdGuard Home disables encryption and shows an error message on the Instead, AdGuard Home disables encryption and shows an error message on the
encryption settings page in the UI, which was the intended previous behavior. encryption settings page in the UI, which was the intended previous behavior.
@ -44,6 +45,7 @@ See also the [v0.107.20 GitHub milestone][ms-v0.107.20].
[#4944]: https://github.com/AdguardTeam/AdGuardHome/issues/4944 [#4944]: https://github.com/AdguardTeam/AdGuardHome/issues/4944
[#5189]: https://github.com/AdguardTeam/AdGuardHome/issues/5189 [#5189]: https://github.com/AdguardTeam/AdGuardHome/issues/5189
[#5190]: https://github.com/AdguardTeam/AdGuardHome/issues/5190 [#5190]: https://github.com/AdguardTeam/AdGuardHome/issues/5190
[#5193]: https://github.com/AdguardTeam/AdGuardHome/issues/5193

View File

@ -566,6 +566,11 @@ type domainSpecificTestError struct {
error error
} }
// Error implements the [error] interface for domainSpecificTestError.
func (err domainSpecificTestError) Error() (msg string) {
return fmt.Sprintf("WARNING: %s", err.error)
}
// checkDNS checks the upstream server defined by upstreamConfigStr using // checkDNS checks the upstream server defined by upstreamConfigStr using
// healthCheck for actually exchange messages. It uses bootstrap to resolve the // healthCheck for actually exchange messages. It uses bootstrap to resolve the
// upstream's address. // upstream's address.
@ -632,41 +637,45 @@ func (s *Server) handleTestUpstreamDNS(w http.ResponseWriter, r *http.Request) {
result := map[string]string{} result := map[string]string{}
bootstraps := req.BootstrapDNS bootstraps := req.BootstrapDNS
timeout := s.conf.UpstreamTimeout timeout := s.conf.UpstreamTimeout
for _, host := range req.Upstreams {
err = checkDNS(host, bootstraps, timeout, checkDNSUpstreamExc)
if err != nil {
log.Info("%v", err)
result[host] = err.Error()
if _, ok := err.(domainSpecificTestError); ok {
result[host] = fmt.Sprintf("WARNING: %s", result[host])
}
continue type upsCheckResult = struct {
} res string
host string
result[host] = "OK"
} }
for _, host := range req.PrivateUpstreams { upsNum := len(req.Upstreams) + len(req.PrivateUpstreams)
err = checkDNS(host, bootstraps, timeout, checkPrivateUpstreamExc) resCh := make(chan upsCheckResult, upsNum)
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()
if _, ok := err.(domainSpecificTestError); ok {
result[host] = fmt.Sprintf("WARNING: %s", result[host])
}
continue checkUps := func(ups string, healthCheck healthCheckFunc) {
res := upsCheckResult{
host: ups,
} }
defer func() { resCh <- res }()
result[host] = "OK" checkErr := checkDNS(ups, bootstraps, timeout, healthCheck)
if checkErr != nil {
res.res = checkErr.Error()
} else {
res.res = "OK"
}
} }
for _, ups := range req.Upstreams {
go checkUps(ups, checkDNSUpstreamExc)
}
for _, ups := range req.PrivateUpstreams {
go checkUps(ups, checkPrivateUpstreamExc)
}
for i := 0; i < upsNum; i++ {
pair := <-resCh
// TODO(e.burkov): The upstreams used for both common and private
// resolving should be reported separately.
result[pair.host] = pair.res
}
close(resCh)
_ = aghhttp.WriteJSONResponse(w, r, result) _ = aghhttp.WriteJSONResponse(w, r, result)
} }

View File

@ -7,16 +7,20 @@ import (
"net" "net"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/netip"
"net/url"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"testing" "testing"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp" "github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet" "github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/filtering" "github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/testutil" "github.com/AdguardTeam/golibs/testutil"
"github.com/miekg/dns"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -392,3 +396,141 @@ func TestValidateUpstreamsPrivate(t *testing.T) {
}) })
} }
} }
func newLocalUpstreamListener(t *testing.T, port int, handler dns.Handler) (real net.Addr) {
startCh := make(chan struct{})
upsSrv := &dns.Server{
Addr: netip.AddrPortFrom(netutil.IPv4Localhost(), uint16(port)).String(),
Net: "tcp",
Handler: handler,
NotifyStartedFunc: func() { close(startCh) },
}
go func() {
t := testutil.PanicT{}
err := upsSrv.ListenAndServe()
require.NoError(t, err)
}()
<-startCh
testutil.CleanupAndRequireSuccess(t, upsSrv.Shutdown)
return upsSrv.Listener.Addr()
}
func TestServer_handleTestUpstreaDNS(t *testing.T) {
goodHandler := dns.HandlerFunc(func(w dns.ResponseWriter, m *dns.Msg) {
err := w.WriteMsg(new(dns.Msg).SetReply(m))
require.NoError(testutil.PanicT{}, err)
})
badHandler := dns.HandlerFunc(func(w dns.ResponseWriter, _ *dns.Msg) {
err := w.WriteMsg(new(dns.Msg))
require.NoError(testutil.PanicT{}, err)
})
goodUps := (&url.URL{
Scheme: "tcp",
Host: newLocalUpstreamListener(t, 0, goodHandler).String(),
}).String()
badUps := (&url.URL{
Scheme: "tcp",
Host: newLocalUpstreamListener(t, 0, badHandler).String(),
}).String()
const upsTimeout = 100 * time.Millisecond
srv := createTestServer(t, &filtering.Config{}, ServerConfig{
UDPListenAddrs: []*net.UDPAddr{{}},
TCPListenAddrs: []*net.TCPAddr{{}},
UpstreamTimeout: upsTimeout,
}, nil)
startDeferStop(t, srv)
testCases := []struct {
body map[string]any
wantResp map[string]any
name string
}{{
body: map[string]any{
"upstream_dns": []string{goodUps},
},
wantResp: map[string]any{
goodUps: "OK",
},
name: "success",
}, {
body: map[string]any{
"upstream_dns": []string{badUps},
},
wantResp: map[string]any{
badUps: `upstream "` + badUps + `" fails to exchange: ` +
`couldn't communicate with upstream: dns: id mismatch`,
},
name: "broken",
}, {
body: map[string]any{
"upstream_dns": []string{goodUps, badUps},
},
wantResp: map[string]any{
goodUps: "OK",
badUps: `upstream "` + badUps + `" fails to exchange: ` +
`couldn't communicate with upstream: dns: id mismatch`,
},
name: "both",
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
reqBody, err := json.Marshal(tc.body)
require.NoError(t, err)
w := httptest.NewRecorder()
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},
}
reqBody, err := json.Marshal(req)
require.NoError(t, err)
w := httptest.NewRecorder()
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"))
})
}