mirror of
https://github.com/AdguardTeam/AdGuardHome.git
synced 2024-11-28 09:58:52 +03:00
34aa81ca99
Squashed commit of the following: commit e496653b10de52676826ed8e0c461e91405603a8 Merge: db2cd04e960a978c9a
Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Thu Feb 1 18:23:50 2024 +0300 Merge branch 'master' into AG-28327-upstream-config-parser commit db2cd04e981dd24998d87f4935ff6590ea7854cd Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Wed Jan 31 16:21:53 2024 +0300 all: upd proxy commit e8878179b6d094321d56fb2b75c16c1ba8cf637d Merge: ccbbae6d6aa872dfe9
Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Wed Jan 31 16:17:34 2024 +0300 Merge branch 'master' into AG-28327-upstream-config-parser commit ccbbae6d615e110d7d2d4c2a6b35954311153bcf Merge: d947d900e8936c95ec
Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Mon Jan 29 18:31:17 2024 +0300 Merge branch 'master' into AG-28327-upstream-config-parser commit d947d900e1f759159bc9068589ffe852483cfdd0 Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Mon Jan 29 18:26:01 2024 +0300 dnsforward: imp docs commit cf9678c098951e2a4bebae7a3a5808d7de4c14c6 Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Thu Jan 25 14:18:04 2024 +0300 dnsforward: imp code commit 22792a9311cb93b2bb3b804293f87f091b9b81e2 Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Wed Jan 24 13:59:28 2024 +0300 dnsforward: imp code commit 57ddaaaaaf1009c65f0d9d6b2b1671211f194c85 Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Mon Jan 22 20:19:44 2024 +0300 all: add tests commit d6732d13adae4ee46410252a33d092e67da3c34a Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Mon Jan 22 18:44:57 2024 +0300 all: imp errors commit e14456571ce2ef43fb217f45445729ce6299daf6 Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Thu Jan 18 19:05:31 2024 +0300 dnsforward: imp code commit a5c106eae902fbc0a169ef9e4d7bf968f1e40bec Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Mon Jan 15 18:36:30 2024 +0300 dnsforward: imp logs commit 333b8561aa21d778007f808fb8e931ef3e95d721 Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Thu Dec 21 15:06:42 2023 +0300 all: imp tests commit5b19d6b039
Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Fri Dec 15 14:21:58 2023 +0300 all: imp code commit15fbd229de
Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Wed Dec 13 14:49:40 2023 +0300 all: upstream config parser
409 lines
12 KiB
Go
409 lines
12 KiB
Go
package dnsforward
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
|
|
"github.com/AdguardTeam/dnsproxy/proxy"
|
|
"github.com/AdguardTeam/dnsproxy/upstream"
|
|
"github.com/AdguardTeam/golibs/errors"
|
|
"github.com/AdguardTeam/golibs/log"
|
|
"github.com/miekg/dns"
|
|
)
|
|
|
|
// upstreamConfigValidator parses each section of an upstream configuration into
|
|
// a corresponding [*proxy.UpstreamConfig] and checks the actual DNS
|
|
// availability of each upstream.
|
|
type upstreamConfigValidator struct {
|
|
// generalUpstreamResults contains upstream results of a general section.
|
|
generalUpstreamResults map[string]*upstreamResult
|
|
|
|
// fallbackUpstreamResults contains upstream results of a fallback section.
|
|
fallbackUpstreamResults map[string]*upstreamResult
|
|
|
|
// privateUpstreamResults contains upstream results of a private section.
|
|
privateUpstreamResults map[string]*upstreamResult
|
|
|
|
// generalParseResults contains parsing results of a general section.
|
|
generalParseResults []*parseResult
|
|
|
|
// fallbackParseResults contains parsing results of a fallback section.
|
|
fallbackParseResults []*parseResult
|
|
|
|
// privateParseResults contains parsing results of a private section.
|
|
privateParseResults []*parseResult
|
|
}
|
|
|
|
// upstreamResult is a result of parsing of an [upstream.Upstream] within an
|
|
// [proxy.UpstreamConfig].
|
|
type upstreamResult struct {
|
|
// server is the parsed upstream.
|
|
server upstream.Upstream
|
|
|
|
// err is the upstream check error.
|
|
err error
|
|
|
|
// isSpecific is true if the upstream is domain-specific.
|
|
isSpecific bool
|
|
}
|
|
|
|
// parseResult contains a original piece of upstream configuration and a
|
|
// corresponding error.
|
|
type parseResult struct {
|
|
err *proxy.ParseError
|
|
original string
|
|
}
|
|
|
|
// newUpstreamConfigValidator parses the upstream configuration and returns a
|
|
// validator for it. cv already contains the parsed upstreams along with errors
|
|
// related.
|
|
func newUpstreamConfigValidator(
|
|
general []string,
|
|
fallback []string,
|
|
private []string,
|
|
opts *upstream.Options,
|
|
) (cv *upstreamConfigValidator) {
|
|
cv = &upstreamConfigValidator{
|
|
generalUpstreamResults: map[string]*upstreamResult{},
|
|
fallbackUpstreamResults: map[string]*upstreamResult{},
|
|
privateUpstreamResults: map[string]*upstreamResult{},
|
|
}
|
|
|
|
conf, err := proxy.ParseUpstreamsConfig(general, opts)
|
|
cv.generalParseResults = collectErrResults(general, err)
|
|
insertConfResults(conf, cv.generalUpstreamResults)
|
|
|
|
conf, err = proxy.ParseUpstreamsConfig(fallback, opts)
|
|
cv.fallbackParseResults = collectErrResults(fallback, err)
|
|
insertConfResults(conf, cv.fallbackUpstreamResults)
|
|
|
|
conf, err = proxy.ParseUpstreamsConfig(private, opts)
|
|
cv.privateParseResults = collectErrResults(private, err)
|
|
insertConfResults(conf, cv.privateUpstreamResults)
|
|
|
|
return cv
|
|
}
|
|
|
|
// collectErrResults parses err and returns parsing results containing the
|
|
// original upstream configuration line and the corresponding error. err can be
|
|
// nil.
|
|
func collectErrResults(lines []string, err error) (results []*parseResult) {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
|
|
// limit is a maximum length for upstream configuration lines.
|
|
const limit = 80
|
|
|
|
wrapper, ok := err.(errors.WrapperSlice)
|
|
if !ok {
|
|
log.Debug("dnsforward: configvalidator: unwrapping: %s", err)
|
|
|
|
return nil
|
|
}
|
|
|
|
errs := wrapper.Unwrap()
|
|
results = make([]*parseResult, 0, len(errs))
|
|
for i, e := range errs {
|
|
var parseErr *proxy.ParseError
|
|
if !errors.As(e, &parseErr) {
|
|
log.Debug("dnsforward: configvalidator: inserting unexpected error %d: %s", i, err)
|
|
|
|
continue
|
|
}
|
|
|
|
idx := parseErr.Idx
|
|
line := []rune(lines[idx])
|
|
if len(line) > limit {
|
|
line = line[:limit]
|
|
line[limit-1] = '…'
|
|
}
|
|
|
|
results = append(results, &parseResult{
|
|
original: string(line),
|
|
err: parseErr,
|
|
})
|
|
}
|
|
|
|
return results
|
|
}
|
|
|
|
// insertConfResults parses conf and inserts the upstream result into results.
|
|
// It can insert multiple results as well as none.
|
|
func insertConfResults(conf *proxy.UpstreamConfig, results map[string]*upstreamResult) {
|
|
insertListResults(conf.Upstreams, results, false)
|
|
|
|
for _, ups := range conf.DomainReservedUpstreams {
|
|
insertListResults(ups, results, true)
|
|
}
|
|
|
|
for _, ups := range conf.SpecifiedDomainUpstreams {
|
|
insertListResults(ups, results, true)
|
|
}
|
|
}
|
|
|
|
// insertListResults constructs upstream results from the upstream list and
|
|
// inserts them into results. It can insert multiple results as well as none.
|
|
func insertListResults(ups []upstream.Upstream, results map[string]*upstreamResult, specific bool) {
|
|
for _, u := range ups {
|
|
addr := u.Address()
|
|
_, ok := results[addr]
|
|
if ok {
|
|
continue
|
|
}
|
|
|
|
results[addr] = &upstreamResult{
|
|
server: u,
|
|
isSpecific: specific,
|
|
}
|
|
}
|
|
}
|
|
|
|
// check tries to exchange with each successfully parsed upstream and enriches
|
|
// the results with the healthcheck errors. It should not be called after the
|
|
// [upsConfValidator.close] method, since it makes no sense to check the closed
|
|
// upstreams.
|
|
func (cv *upstreamConfigValidator) check() {
|
|
const (
|
|
// testTLD is the special-use fully-qualified domain name for testing
|
|
// the DNS server reachability.
|
|
//
|
|
// See https://datatracker.ietf.org/doc/html/rfc6761#section-6.2.
|
|
testTLD = "test."
|
|
|
|
// inAddrARPATLD is the special-use fully-qualified domain name for PTR
|
|
// IP address resolution.
|
|
//
|
|
// See https://datatracker.ietf.org/doc/html/rfc1035#section-3.5.
|
|
inAddrARPATLD = "in-addr.arpa."
|
|
)
|
|
|
|
commonChecker := &healthchecker{
|
|
hostname: testTLD,
|
|
qtype: dns.TypeA,
|
|
ansEmpty: true,
|
|
}
|
|
|
|
arpaChecker := &healthchecker{
|
|
hostname: inAddrARPATLD,
|
|
qtype: dns.TypePTR,
|
|
ansEmpty: false,
|
|
}
|
|
|
|
wg := &sync.WaitGroup{}
|
|
wg.Add(len(cv.generalUpstreamResults) +
|
|
len(cv.fallbackUpstreamResults) +
|
|
len(cv.privateUpstreamResults))
|
|
|
|
for _, res := range cv.generalUpstreamResults {
|
|
go checkSrv(res, wg, commonChecker)
|
|
}
|
|
for _, res := range cv.fallbackUpstreamResults {
|
|
go checkSrv(res, wg, commonChecker)
|
|
}
|
|
for _, res := range cv.privateUpstreamResults {
|
|
go checkSrv(res, wg, arpaChecker)
|
|
}
|
|
|
|
wg.Wait()
|
|
}
|
|
|
|
// checkSrv runs hc on the server from res, if any, and stores any occurred
|
|
// error in res. wg is always marked done in the end. It is intended to be
|
|
// used as a goroutine.
|
|
func checkSrv(res *upstreamResult, wg *sync.WaitGroup, hc *healthchecker) {
|
|
defer log.OnPanic(fmt.Sprintf("dnsforward: checking upstream %s", res.server.Address()))
|
|
defer wg.Done()
|
|
|
|
res.err = hc.check(res.server)
|
|
if res.err != nil && res.isSpecific {
|
|
res.err = domainSpecificTestError{Err: res.err}
|
|
}
|
|
}
|
|
|
|
// close closes all the upstreams that were successfully parsed. It enriches
|
|
// the results with deferred closing errors.
|
|
func (cv *upstreamConfigValidator) close() {
|
|
all := []map[string]*upstreamResult{
|
|
cv.generalUpstreamResults,
|
|
cv.fallbackUpstreamResults,
|
|
cv.privateUpstreamResults,
|
|
}
|
|
|
|
for _, m := range all {
|
|
for _, r := range m {
|
|
r.err = errors.WithDeferred(r.err, r.server.Close())
|
|
}
|
|
}
|
|
}
|
|
|
|
// sections of the upstream configuration according to the text label of the
|
|
// localization.
|
|
//
|
|
// Keep in sync with client/src/__locales/en.json.
|
|
//
|
|
// TODO(s.chzhen): Refactor.
|
|
const (
|
|
generalTextLabel = "upstream_dns"
|
|
fallbackTextLabel = "fallback_dns_title"
|
|
privateTextLabel = "local_ptr_title"
|
|
)
|
|
|
|
// status returns all the data collected during parsing, healthcheck, and
|
|
// closing of the upstreams. The returned map is keyed by the original upstream
|
|
// configuration piece and contains the corresponding error or "OK" if there was
|
|
// no error.
|
|
func (cv *upstreamConfigValidator) status() (results map[string]string) {
|
|
// Names of the upstream configuration sections for logging.
|
|
const (
|
|
generalSection = "general"
|
|
fallbackSection = "fallback"
|
|
privateSection = "private"
|
|
)
|
|
|
|
results = map[string]string{}
|
|
|
|
for original, res := range cv.generalUpstreamResults {
|
|
upstreamResultToStatus(generalSection, string(original), res, results)
|
|
}
|
|
for original, res := range cv.fallbackUpstreamResults {
|
|
upstreamResultToStatus(fallbackSection, string(original), res, results)
|
|
}
|
|
for original, res := range cv.privateUpstreamResults {
|
|
upstreamResultToStatus(privateSection, string(original), res, results)
|
|
}
|
|
|
|
parseResultToStatus(generalTextLabel, generalSection, cv.generalParseResults, results)
|
|
parseResultToStatus(fallbackTextLabel, fallbackSection, cv.fallbackParseResults, results)
|
|
parseResultToStatus(privateTextLabel, privateSection, cv.privateParseResults, results)
|
|
|
|
return results
|
|
}
|
|
|
|
// upstreamResultToStatus puts "OK" or an error message from res into resMap.
|
|
// section is the name of the upstream configuration section, i.e. "general",
|
|
// "fallback", or "private", and only used for logging.
|
|
//
|
|
// TODO(e.burkov): Currently, the HTTP handler expects that all the results are
|
|
// put together in a single map, which may lead to collisions, see AG-27539.
|
|
// Improve the results compilation.
|
|
func upstreamResultToStatus(
|
|
section string,
|
|
original string,
|
|
res *upstreamResult,
|
|
resMap map[string]string,
|
|
) {
|
|
val := "OK"
|
|
if res.err != nil {
|
|
val = res.err.Error()
|
|
}
|
|
|
|
prevVal := resMap[original]
|
|
switch prevVal {
|
|
case "":
|
|
resMap[original] = val
|
|
case val:
|
|
log.Debug("dnsforward: duplicating %s config line %q", section, original)
|
|
default:
|
|
log.Debug(
|
|
"dnsforward: warning: %s config line %q (%v) had different result %v",
|
|
section,
|
|
val,
|
|
original,
|
|
prevVal,
|
|
)
|
|
}
|
|
}
|
|
|
|
// parseResultToStatus puts parsing error messages from results into resMap.
|
|
// section is the name of the upstream configuration section, i.e. "general",
|
|
// "fallback", or "private", and only used for logging.
|
|
//
|
|
// Parsing error message has the following format:
|
|
//
|
|
// sectionTextLabel line: parsing error
|
|
//
|
|
// Where sectionTextLabel is a section text label of a localization and line is
|
|
// a line number.
|
|
func parseResultToStatus(
|
|
textLabel string,
|
|
section string,
|
|
results []*parseResult,
|
|
resMap map[string]string,
|
|
) {
|
|
for _, res := range results {
|
|
original := res.original
|
|
_, ok := resMap[original]
|
|
if ok {
|
|
log.Debug("dnsforward: duplicating %s parsing error %q", section, original)
|
|
|
|
continue
|
|
}
|
|
|
|
resMap[original] = fmt.Sprintf("%s %d: parsing error", textLabel, res.err.Idx+1)
|
|
}
|
|
}
|
|
|
|
// domainSpecificTestError is a wrapper for errors returned by checkDNS to mark
|
|
// the tested upstream domain-specific and therefore consider its errors
|
|
// non-critical.
|
|
//
|
|
// TODO(a.garipov): Some common mechanism of distinguishing between errors and
|
|
// warnings (non-critical errors) is desired.
|
|
type domainSpecificTestError struct {
|
|
// Err is the actual error occurred during healthcheck test.
|
|
Err error
|
|
}
|
|
|
|
// type check
|
|
var _ error = domainSpecificTestError{}
|
|
|
|
// Error implements the [error] interface for domainSpecificTestError.
|
|
func (err domainSpecificTestError) Error() (msg string) {
|
|
return fmt.Sprintf("WARNING: %s", err.Err)
|
|
}
|
|
|
|
// type check
|
|
var _ errors.Wrapper = domainSpecificTestError{}
|
|
|
|
// Unwrap implements the [errors.Wrapper] interface for domainSpecificTestError.
|
|
func (err domainSpecificTestError) Unwrap() (wrapped error) {
|
|
return err.Err
|
|
}
|
|
|
|
// healthchecker checks the upstream's status by exchanging with it.
|
|
type healthchecker struct {
|
|
// hostname is the name of the host to put into healthcheck DNS request.
|
|
hostname string
|
|
|
|
// qtype is the type of DNS request to use for healthcheck.
|
|
qtype uint16
|
|
|
|
// ansEmpty defines if the answer section within the response is expected to
|
|
// be empty.
|
|
ansEmpty bool
|
|
}
|
|
|
|
// check exchanges with u and validates the response.
|
|
func (h *healthchecker) check(u upstream.Upstream) (err error) {
|
|
req := &dns.Msg{
|
|
MsgHdr: dns.MsgHdr{
|
|
Id: dns.Id(),
|
|
RecursionDesired: true,
|
|
},
|
|
Question: []dns.Question{{
|
|
Name: h.hostname,
|
|
Qtype: h.qtype,
|
|
Qclass: dns.ClassINET,
|
|
}},
|
|
}
|
|
|
|
reply, err := u.Exchange(req)
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't communicate with upstream: %w", err)
|
|
} else if h.ansEmpty && len(reply.Answer) > 0 {
|
|
return errors.Error("wrong response")
|
|
}
|
|
|
|
return nil
|
|
}
|