Pull request 2107: AG-28327-upstream-config-parser
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 commit 5b19d6b039755577e03ffcc03952724a36f21aa4 Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Fri Dec 15 14:21:58 2023 +0300 all: imp code commit 15fbd229de336425bde107a4f32175b8af41d876 Author: Stanislav Chzhen <s.chzhen@adguard.com> Date: Wed Dec 13 14:49:40 2023 +0300 all: upstream config parser
This commit is contained in:
parent
60a978c9a6
commit
34aa81ca99
|
@ -236,6 +236,7 @@
|
||||||
"updated_upstream_dns_toast": "Upstream servers successfully saved",
|
"updated_upstream_dns_toast": "Upstream servers successfully saved",
|
||||||
"dns_test_ok_toast": "Specified DNS servers are working correctly",
|
"dns_test_ok_toast": "Specified DNS servers are working correctly",
|
||||||
"dns_test_not_ok_toast": "Server \"{{key}}\": could not be used, please check that you've written it correctly",
|
"dns_test_not_ok_toast": "Server \"{{key}}\": could not be used, please check that you've written it correctly",
|
||||||
|
"dns_test_parsing_error_toast": "Section {{section}}: line {{line}}: could not be used, please check that you've written it correctly",
|
||||||
"dns_test_warning_toast": "Upstream \"{{key}}\" does not respond to test requests and may not work properly",
|
"dns_test_warning_toast": "Upstream \"{{key}}\" does not respond to test requests and may not work properly",
|
||||||
"unblock": "Unblock",
|
"unblock": "Unblock",
|
||||||
"block": "Block",
|
"block": "Block",
|
||||||
|
|
|
@ -403,6 +403,11 @@ export const testUpstream = (
|
||||||
const message = upstreamResponse[key];
|
const message = upstreamResponse[key];
|
||||||
if (message.startsWith('WARNING:')) {
|
if (message.startsWith('WARNING:')) {
|
||||||
dispatch(addErrorToast({ error: i18next.t('dns_test_warning_toast', { key }) }));
|
dispatch(addErrorToast({ error: i18next.t('dns_test_warning_toast', { key }) }));
|
||||||
|
} else if (message.endsWith(': parsing error')) {
|
||||||
|
const info = message.substring(0, message.indexOf(':'));
|
||||||
|
const [sectionKey, line] = info.split(' ');
|
||||||
|
const section = i18next.t(sectionKey);
|
||||||
|
dispatch(addErrorToast({ error: i18next.t('dns_test_parsing_error_toast', { section, line }) }));
|
||||||
} else if (message !== 'OK') {
|
} else if (message !== 'OK') {
|
||||||
dispatch(addErrorToast({ error: i18next.t('dns_test_not_ok_toast', { key }) }));
|
dispatch(addErrorToast({ error: i18next.t('dns_test_not_ok_toast', { key }) }));
|
||||||
}
|
}
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -3,7 +3,7 @@ module github.com/AdguardTeam/AdGuardHome
|
||||||
go 1.20
|
go 1.20
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/AdguardTeam/dnsproxy v0.63.1
|
github.com/AdguardTeam/dnsproxy v0.64.1
|
||||||
github.com/AdguardTeam/golibs v0.19.0
|
github.com/AdguardTeam/golibs v0.19.0
|
||||||
github.com/AdguardTeam/urlfilter v0.17.3
|
github.com/AdguardTeam/urlfilter v0.17.3
|
||||||
github.com/NYTimes/gziphandler v1.1.1
|
github.com/NYTimes/gziphandler v1.1.1
|
||||||
|
|
4
go.sum
4
go.sum
|
@ -1,5 +1,5 @@
|
||||||
github.com/AdguardTeam/dnsproxy v0.63.1 h1:CilxSuLYcuYpbPCGB7w41UUqWRMu3dvj4c9TvkIrpBg=
|
github.com/AdguardTeam/dnsproxy v0.64.1 h1:Cv2nyNYjUeUxouTQmM0aVTR7LWuhCr/Lu+h3DIAWhG8=
|
||||||
github.com/AdguardTeam/dnsproxy v0.63.1/go.mod h1:dRRAFOjrq4QYM92jGs4lt4BoY0Dm3EY3HkaleoM2Feo=
|
github.com/AdguardTeam/dnsproxy v0.64.1/go.mod h1:dRRAFOjrq4QYM92jGs4lt4BoY0Dm3EY3HkaleoM2Feo=
|
||||||
github.com/AdguardTeam/golibs v0.19.0 h1:y/x+Xn3pDg1ZfQ+QEZapPJqaeVYUIMp/EODMtVhn7PM=
|
github.com/AdguardTeam/golibs v0.19.0 h1:y/x+Xn3pDg1ZfQ+QEZapPJqaeVYUIMp/EODMtVhn7PM=
|
||||||
github.com/AdguardTeam/golibs v0.19.0/go.mod h1:3WunclLLfrVAq7fYQRhd6f168FHOEMssnipVXCxDL/w=
|
github.com/AdguardTeam/golibs v0.19.0/go.mod h1:3WunclLLfrVAq7fYQRhd6f168FHOEMssnipVXCxDL/w=
|
||||||
github.com/AdguardTeam/urlfilter v0.17.3 h1:fg/ObbnO0Cv6aw0tW6N/ETDMhhNvmcUUOZ7HlmKC3rw=
|
github.com/AdguardTeam/urlfilter v0.17.3 h1:fg/ObbnO0Cv6aw0tW6N/ETDMhhNvmcUUOZ7HlmKC3rw=
|
||||||
|
|
|
@ -2,54 +2,56 @@ package dnsforward
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
"golang.org/x/exp/slices"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// upstreamConfigValidator parses the [*proxy.UpstreamConfig] and checks the
|
// upstreamConfigValidator parses each section of an upstream configuration into
|
||||||
// actual DNS availability of each upstream.
|
// a corresponding [*proxy.UpstreamConfig] and checks the actual DNS
|
||||||
|
// availability of each upstream.
|
||||||
type upstreamConfigValidator struct {
|
type upstreamConfigValidator struct {
|
||||||
// general is the general upstream configuration.
|
// generalUpstreamResults contains upstream results of a general section.
|
||||||
general []*upstreamResult
|
generalUpstreamResults map[string]*upstreamResult
|
||||||
|
|
||||||
// fallback is the fallback upstream configuration.
|
// fallbackUpstreamResults contains upstream results of a fallback section.
|
||||||
fallback []*upstreamResult
|
fallbackUpstreamResults map[string]*upstreamResult
|
||||||
|
|
||||||
// private is the private upstream configuration.
|
// privateUpstreamResults contains upstream results of a private section.
|
||||||
private []*upstreamResult
|
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 validation of an [upstream.Upstream] within an
|
// upstreamResult is a result of parsing of an [upstream.Upstream] within an
|
||||||
// [proxy.UpstreamConfig].
|
// [proxy.UpstreamConfig].
|
||||||
type upstreamResult struct {
|
type upstreamResult struct {
|
||||||
// server is the parsed upstream. It is nil when there was an error during
|
// server is the parsed upstream.
|
||||||
// parsing.
|
|
||||||
server upstream.Upstream
|
server upstream.Upstream
|
||||||
|
|
||||||
// err is the error either from parsing or from checking the upstream.
|
// err is the upstream check error.
|
||||||
err error
|
err error
|
||||||
|
|
||||||
// original is the piece of configuration that have either been turned to an
|
|
||||||
// upstream or caused an error.
|
|
||||||
original string
|
|
||||||
|
|
||||||
// isSpecific is true if the upstream is domain-specific.
|
// isSpecific is true if the upstream is domain-specific.
|
||||||
isSpecific bool
|
isSpecific bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// compare compares two [upstreamResult]s. It returns 0 if they are equal, -1
|
// parseResult contains a original piece of upstream configuration and a
|
||||||
// if ur should be sorted before other, and 1 otherwise.
|
// corresponding error.
|
||||||
//
|
type parseResult struct {
|
||||||
// TODO(e.burkov): Perhaps it makes sense to sort the results with errors near
|
err *proxy.ParseError
|
||||||
// the end.
|
original string
|
||||||
func (ur *upstreamResult) compare(other *upstreamResult) (res int) {
|
|
||||||
return strings.Compare(ur.original, other.original)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// newUpstreamConfigValidator parses the upstream configuration and returns a
|
// newUpstreamConfigValidator parses the upstream configuration and returns a
|
||||||
|
@ -61,97 +63,99 @@ func newUpstreamConfigValidator(
|
||||||
private []string,
|
private []string,
|
||||||
opts *upstream.Options,
|
opts *upstream.Options,
|
||||||
) (cv *upstreamConfigValidator) {
|
) (cv *upstreamConfigValidator) {
|
||||||
cv = &upstreamConfigValidator{}
|
cv = &upstreamConfigValidator{
|
||||||
|
generalUpstreamResults: map[string]*upstreamResult{},
|
||||||
|
fallbackUpstreamResults: map[string]*upstreamResult{},
|
||||||
|
privateUpstreamResults: map[string]*upstreamResult{},
|
||||||
|
}
|
||||||
|
|
||||||
for _, line := range general {
|
conf, err := proxy.ParseUpstreamsConfig(general, opts)
|
||||||
cv.general = cv.insertLineResults(cv.general, line, opts)
|
cv.generalParseResults = collectErrResults(general, err)
|
||||||
}
|
insertConfResults(conf, cv.generalUpstreamResults)
|
||||||
for _, line := range fallback {
|
|
||||||
cv.fallback = cv.insertLineResults(cv.fallback, line, opts)
|
conf, err = proxy.ParseUpstreamsConfig(fallback, opts)
|
||||||
}
|
cv.fallbackParseResults = collectErrResults(fallback, err)
|
||||||
for _, line := range private {
|
insertConfResults(conf, cv.fallbackUpstreamResults)
|
||||||
cv.private = cv.insertLineResults(cv.private, line, opts)
|
|
||||||
}
|
conf, err = proxy.ParseUpstreamsConfig(private, opts)
|
||||||
|
cv.privateParseResults = collectErrResults(private, err)
|
||||||
|
insertConfResults(conf, cv.privateUpstreamResults)
|
||||||
|
|
||||||
return cv
|
return cv
|
||||||
}
|
}
|
||||||
|
|
||||||
// insertLineResults parses line and inserts the result into s. It can insert
|
// collectErrResults parses err and returns parsing results containing the
|
||||||
// multiple results as well as none.
|
// original upstream configuration line and the corresponding error. err can be
|
||||||
func (cv *upstreamConfigValidator) insertLineResults(
|
// nil.
|
||||||
s []*upstreamResult,
|
func collectErrResults(lines []string, err error) (results []*parseResult) {
|
||||||
line string,
|
if err == nil {
|
||||||
opts *upstream.Options,
|
return nil
|
||||||
) (result []*upstreamResult) {
|
|
||||||
upstreams, isSpecific, err := splitUpstreamLine(line)
|
|
||||||
if err != nil {
|
|
||||||
return cv.insert(s, &upstreamResult{
|
|
||||||
err: err,
|
|
||||||
original: line,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, upstreamAddr := range upstreams {
|
// limit is a maximum length for upstream configuration lines.
|
||||||
var res *upstreamResult
|
const limit = 80
|
||||||
if upstreamAddr != "#" {
|
|
||||||
res = cv.parseUpstream(upstreamAddr, opts)
|
wrapper, ok := err.(errors.WrapperSlice)
|
||||||
} else if !isSpecific {
|
if !ok {
|
||||||
res = &upstreamResult{
|
log.Debug("dnsforward: configvalidator: unwrapping: %s", err)
|
||||||
err: errNotDomainSpecific,
|
|
||||||
original: upstreamAddr,
|
return nil
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
res.isSpecific = isSpecific
|
idx := parseErr.Idx
|
||||||
s = cv.insert(s, res)
|
line := []rune(lines[idx])
|
||||||
|
if len(line) > limit {
|
||||||
|
line = line[:limit]
|
||||||
|
line[limit-1] = '…'
|
||||||
}
|
}
|
||||||
|
|
||||||
return s
|
results = append(results, &parseResult{
|
||||||
|
original: string(line),
|
||||||
|
err: parseErr,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// insert inserts r into slice in a sorted order, except duplicates. slice must
|
return results
|
||||||
// not be nil.
|
|
||||||
func (cv *upstreamConfigValidator) insert(
|
|
||||||
s []*upstreamResult,
|
|
||||||
r *upstreamResult,
|
|
||||||
) (result []*upstreamResult) {
|
|
||||||
i, has := slices.BinarySearchFunc(s, r, (*upstreamResult).compare)
|
|
||||||
if has {
|
|
||||||
log.Debug("dnsforward: duplicate configuration %q", r.original)
|
|
||||||
|
|
||||||
return s
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return slices.Insert(s, i, r)
|
// 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseUpstream parses addr and returns the result of parsing. It returns nil
|
for _, ups := range conf.SpecifiedDomainUpstreams {
|
||||||
// if the specified server points at the default upstream server which is
|
insertListResults(ups, results, true)
|
||||||
// validated separately.
|
|
||||||
func (cv *upstreamConfigValidator) parseUpstream(
|
|
||||||
addr string,
|
|
||||||
opts *upstream.Options,
|
|
||||||
) (r *upstreamResult) {
|
|
||||||
// Check if the upstream has a valid protocol prefix.
|
|
||||||
//
|
|
||||||
// TODO(e.burkov): Validate the domain name.
|
|
||||||
if proto, _, ok := strings.Cut(addr, "://"); ok {
|
|
||||||
if !slices.Contains(protocols, proto) {
|
|
||||||
return &upstreamResult{
|
|
||||||
err: fmt.Errorf("bad protocol %q", proto),
|
|
||||||
original: addr,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ups, err := upstream.AddressToUpstream(addr, opts)
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
return &upstreamResult{
|
results[addr] = &upstreamResult{
|
||||||
server: ups,
|
server: u,
|
||||||
err: err,
|
isSpecific: specific,
|
||||||
original: addr,
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -187,35 +191,30 @@ func (cv *upstreamConfigValidator) check() {
|
||||||
}
|
}
|
||||||
|
|
||||||
wg := &sync.WaitGroup{}
|
wg := &sync.WaitGroup{}
|
||||||
wg.Add(len(cv.general) + len(cv.fallback) + len(cv.private))
|
wg.Add(len(cv.generalUpstreamResults) +
|
||||||
|
len(cv.fallbackUpstreamResults) +
|
||||||
|
len(cv.privateUpstreamResults))
|
||||||
|
|
||||||
for _, res := range cv.general {
|
for _, res := range cv.generalUpstreamResults {
|
||||||
go cv.checkSrv(res, wg, commonChecker)
|
go checkSrv(res, wg, commonChecker)
|
||||||
}
|
}
|
||||||
for _, res := range cv.fallback {
|
for _, res := range cv.fallbackUpstreamResults {
|
||||||
go cv.checkSrv(res, wg, commonChecker)
|
go checkSrv(res, wg, commonChecker)
|
||||||
}
|
}
|
||||||
for _, res := range cv.private {
|
for _, res := range cv.privateUpstreamResults {
|
||||||
go cv.checkSrv(res, wg, arpaChecker)
|
go checkSrv(res, wg, arpaChecker)
|
||||||
}
|
}
|
||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkSrv runs hc on the server from res, if any, and stores any occurred
|
// 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 used to be called in
|
// error in res. wg is always marked done in the end. It is intended to be
|
||||||
// a separate goroutine.
|
// used as a goroutine.
|
||||||
func (cv *upstreamConfigValidator) checkSrv(
|
func checkSrv(res *upstreamResult, wg *sync.WaitGroup, hc *healthchecker) {
|
||||||
res *upstreamResult,
|
defer log.OnPanic(fmt.Sprintf("dnsforward: checking upstream %s", res.server.Address()))
|
||||||
wg *sync.WaitGroup,
|
|
||||||
hc *healthchecker,
|
|
||||||
) {
|
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
|
||||||
if res.server == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
res.err = hc.check(res.server)
|
res.err = hc.check(res.server)
|
||||||
if res.err != nil && res.isSpecific {
|
if res.err != nil && res.isSpecific {
|
||||||
res.err = domainSpecificTestError{Err: res.err}
|
res.err = domainSpecificTestError{Err: res.err}
|
||||||
|
@ -225,65 +224,126 @@ func (cv *upstreamConfigValidator) checkSrv(
|
||||||
// close closes all the upstreams that were successfully parsed. It enriches
|
// close closes all the upstreams that were successfully parsed. It enriches
|
||||||
// the results with deferred closing errors.
|
// the results with deferred closing errors.
|
||||||
func (cv *upstreamConfigValidator) close() {
|
func (cv *upstreamConfigValidator) close() {
|
||||||
for _, slice := range [][]*upstreamResult{cv.general, cv.fallback, cv.private} {
|
all := []map[string]*upstreamResult{
|
||||||
for _, r := range slice {
|
cv.generalUpstreamResults,
|
||||||
if r.server != nil {
|
cv.fallbackUpstreamResults,
|
||||||
|
cv.privateUpstreamResults,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, m := range all {
|
||||||
|
for _, r := range m {
|
||||||
r.err = errors.WithDeferred(r.err, r.server.Close())
|
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
|
// status returns all the data collected during parsing, healthcheck, and
|
||||||
// closing of the upstreams. The returned map is keyed by the original upstream
|
// 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
|
// configuration piece and contains the corresponding error or "OK" if there was
|
||||||
// no error.
|
// no error.
|
||||||
func (cv *upstreamConfigValidator) status() (results map[string]string) {
|
func (cv *upstreamConfigValidator) status() (results map[string]string) {
|
||||||
result := map[string]string{}
|
// Names of the upstream configuration sections for logging.
|
||||||
|
const (
|
||||||
|
generalSection = "general"
|
||||||
|
fallbackSection = "fallback"
|
||||||
|
privateSection = "private"
|
||||||
|
)
|
||||||
|
|
||||||
for _, res := range cv.general {
|
results = map[string]string{}
|
||||||
resultToStatus("general", res, result)
|
|
||||||
|
for original, res := range cv.generalUpstreamResults {
|
||||||
|
upstreamResultToStatus(generalSection, string(original), res, results)
|
||||||
}
|
}
|
||||||
for _, res := range cv.fallback {
|
for original, res := range cv.fallbackUpstreamResults {
|
||||||
resultToStatus("fallback", res, result)
|
upstreamResultToStatus(fallbackSection, string(original), res, results)
|
||||||
}
|
}
|
||||||
for _, res := range cv.private {
|
for original, res := range cv.privateUpstreamResults {
|
||||||
resultToStatus("private", res, result)
|
upstreamResultToStatus(privateSection, string(original), res, results)
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
parseResultToStatus(generalTextLabel, generalSection, cv.generalParseResults, results)
|
||||||
|
parseResultToStatus(fallbackTextLabel, fallbackSection, cv.fallbackParseResults, results)
|
||||||
|
parseResultToStatus(privateTextLabel, privateSection, cv.privateParseResults, results)
|
||||||
|
|
||||||
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
// resultToStatus puts "OK" or an error message from res into resMap. section
|
// upstreamResultToStatus puts "OK" or an error message from res into resMap.
|
||||||
// is the name of the upstream configuration section, i.e. "general",
|
// section is the name of the upstream configuration section, i.e. "general",
|
||||||
// "fallback", or "private", and only used for logging.
|
// "fallback", or "private", and only used for logging.
|
||||||
//
|
//
|
||||||
// TODO(e.burkov): Currently, the HTTP handler expects that all the results are
|
// 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.
|
// put together in a single map, which may lead to collisions, see AG-27539.
|
||||||
// Improve the results compilation.
|
// Improve the results compilation.
|
||||||
func resultToStatus(section string, res *upstreamResult, resMap map[string]string) {
|
func upstreamResultToStatus(
|
||||||
|
section string,
|
||||||
|
original string,
|
||||||
|
res *upstreamResult,
|
||||||
|
resMap map[string]string,
|
||||||
|
) {
|
||||||
val := "OK"
|
val := "OK"
|
||||||
if res.err != nil {
|
if res.err != nil {
|
||||||
val = res.err.Error()
|
val = res.err.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
prevVal := resMap[res.original]
|
prevVal := resMap[original]
|
||||||
switch prevVal {
|
switch prevVal {
|
||||||
case "":
|
case "":
|
||||||
resMap[res.original] = val
|
resMap[original] = val
|
||||||
case val:
|
case val:
|
||||||
log.Debug("dnsforward: duplicating %s config line %q", section, res.original)
|
log.Debug("dnsforward: duplicating %s config line %q", section, original)
|
||||||
default:
|
default:
|
||||||
log.Debug(
|
log.Debug(
|
||||||
"dnsforward: warning: %s config line %q (%v) had different result %v",
|
"dnsforward: warning: %s config line %q (%v) had different result %v",
|
||||||
section,
|
section,
|
||||||
val,
|
val,
|
||||||
res.original,
|
original,
|
||||||
prevVal,
|
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
|
// domainSpecificTestError is a wrapper for errors returned by checkDNS to mark
|
||||||
// the tested upstream domain-specific and therefore consider its errors
|
// the tested upstream domain-specific and therefore consider its errors
|
||||||
// non-critical.
|
// non-critical.
|
||||||
|
@ -342,7 +402,7 @@ func (h *healthchecker) check(u upstream.Upstream) (err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("couldn't communicate with upstream: %w", err)
|
return fmt.Errorf("couldn't communicate with upstream: %w", err)
|
||||||
} else if h.ansEmpty && len(reply.Answer) > 0 {
|
} else if h.ansEmpty && len(reply.Answer) > 0 {
|
||||||
return errWrongResponse
|
return errors.Error("wrong response")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||||
|
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
|
@ -294,7 +295,7 @@ func (req *jsonDNSConfig) checkFallbacks() (err error) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
err = ValidateUpstreams(*req.Fallbacks)
|
_, err = proxy.ParseUpstreamsConfig(*req.Fallbacks, &upstream.Options{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("fallback servers: %w", err)
|
return fmt.Errorf("fallback servers: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -344,7 +345,7 @@ func (req *jsonDNSConfig) validate(privateNets netutil.SubnetSet) (err error) {
|
||||||
// validateUpstreamDNSServers returns an error if any field of req is invalid.
|
// validateUpstreamDNSServers returns an error if any field of req is invalid.
|
||||||
func (req *jsonDNSConfig) validateUpstreamDNSServers(privateNets netutil.SubnetSet) (err error) {
|
func (req *jsonDNSConfig) validateUpstreamDNSServers(privateNets netutil.SubnetSet) (err error) {
|
||||||
if req.Upstreams != nil {
|
if req.Upstreams != nil {
|
||||||
err = ValidateUpstreams(*req.Upstreams)
|
_, err = proxy.ParseUpstreamsConfig(*req.Upstreams, &upstream.Options{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("upstream servers: %w", err)
|
return fmt.Errorf("upstream servers: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -580,9 +581,6 @@ func (s *Server) handleTestUpstreamDNS(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
req.Upstreams = stringutil.FilterOut(req.Upstreams, IsCommentOrEmpty)
|
|
||||||
req.FallbackDNS = stringutil.FilterOut(req.FallbackDNS, IsCommentOrEmpty)
|
|
||||||
req.PrivateUpstreams = stringutil.FilterOut(req.PrivateUpstreams, IsCommentOrEmpty)
|
|
||||||
req.BootstrapDNS = stringutil.FilterOut(req.BootstrapDNS, IsCommentOrEmpty)
|
req.BootstrapDNS = stringutil.FilterOut(req.BootstrapDNS, IsCommentOrEmpty)
|
||||||
|
|
||||||
opts := &upstream.Options{
|
opts := &upstream.Options{
|
||||||
|
|
|
@ -223,8 +223,9 @@ func TestDNSForwardHTTP_handleSetConfig(t *testing.T) {
|
||||||
wantSet: "",
|
wantSet: "",
|
||||||
}, {
|
}, {
|
||||||
name: "upstream_dns_bad",
|
name: "upstream_dns_bad",
|
||||||
wantSet: `validating dns config: ` +
|
wantSet: `validating dns config: upstream servers: parsing error at index 0: ` +
|
||||||
`upstream servers: validating upstream "!!!": not an ip:port`,
|
`cannot prepare the upstream: invalid address !!!: bad hostname "!!!": ` +
|
||||||
|
`bad top-level domain name label "!!!": bad top-level domain name label rune '!'`,
|
||||||
}, {
|
}, {
|
||||||
name: "bootstraps_bad",
|
name: "bootstraps_bad",
|
||||||
wantSet: `validating dns config: checking bootstrap a: not a bootstrap: ParseAddr("a"): ` +
|
wantSet: `validating dns config: checking bootstrap a: not a bootstrap: ParseAddr("a"): ` +
|
||||||
|
@ -313,98 +314,6 @@ func TestIsCommentOrEmpty(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestValidateUpstreams(t *testing.T) {
|
|
||||||
const sdnsStamp = `sdns://AQMAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_J` +
|
|
||||||
`S3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczE` +
|
|
||||||
`uYWRndWFyZC5jb20`
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
wantErr string
|
|
||||||
set []string
|
|
||||||
}{{
|
|
||||||
name: "empty",
|
|
||||||
wantErr: ``,
|
|
||||||
set: nil,
|
|
||||||
}, {
|
|
||||||
name: "comment",
|
|
||||||
wantErr: ``,
|
|
||||||
set: []string{"# comment"},
|
|
||||||
}, {
|
|
||||||
name: "no_default",
|
|
||||||
wantErr: `no default upstreams specified`,
|
|
||||||
set: []string{
|
|
||||||
"[/host.com/]1.1.1.1",
|
|
||||||
"[//]tls://1.1.1.1",
|
|
||||||
"[/www.host.com/]#",
|
|
||||||
"[/host.com/google.com/]8.8.8.8",
|
|
||||||
"[/host/]" + sdnsStamp,
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
name: "with_default",
|
|
||||||
wantErr: ``,
|
|
||||||
set: []string{
|
|
||||||
"[/host.com/]1.1.1.1",
|
|
||||||
"[//]tls://1.1.1.1",
|
|
||||||
"[/www.host.com/]#",
|
|
||||||
"[/host.com/google.com/]8.8.8.8",
|
|
||||||
"[/host/]" + sdnsStamp,
|
|
||||||
"8.8.8.8",
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
name: "invalid",
|
|
||||||
wantErr: `validating upstream "dhcp://fake.dns": bad protocol "dhcp"`,
|
|
||||||
set: []string{"dhcp://fake.dns"},
|
|
||||||
}, {
|
|
||||||
name: "invalid",
|
|
||||||
wantErr: `validating upstream "1.2.3.4.5": not an ip:port`,
|
|
||||||
set: []string{"1.2.3.4.5"},
|
|
||||||
}, {
|
|
||||||
name: "invalid",
|
|
||||||
wantErr: `validating upstream "123.3.7m": not an ip:port`,
|
|
||||||
set: []string{"123.3.7m"},
|
|
||||||
}, {
|
|
||||||
name: "invalid",
|
|
||||||
wantErr: `splitting upstream line "[/host.com]tls://dns.adguard.com": ` +
|
|
||||||
`missing separator`,
|
|
||||||
set: []string{"[/host.com]tls://dns.adguard.com"},
|
|
||||||
}, {
|
|
||||||
name: "invalid",
|
|
||||||
wantErr: `validating upstream "[host.ru]#": not an ip:port`,
|
|
||||||
set: []string{"[host.ru]#"},
|
|
||||||
}, {
|
|
||||||
name: "valid_default",
|
|
||||||
wantErr: ``,
|
|
||||||
set: []string{
|
|
||||||
"1.1.1.1",
|
|
||||||
"tls://1.1.1.1",
|
|
||||||
"https://dns.adguard.com/dns-query",
|
|
||||||
sdnsStamp,
|
|
||||||
"udp://dns.google",
|
|
||||||
"udp://8.8.8.8",
|
|
||||||
"[/host.com/]1.1.1.1",
|
|
||||||
"[//]tls://1.1.1.1",
|
|
||||||
"[/www.host.com/]#",
|
|
||||||
"[/host.com/google.com/]8.8.8.8",
|
|
||||||
"[/host/]" + sdnsStamp,
|
|
||||||
"[/пример.рф/]8.8.8.8",
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
name: "bad_domain",
|
|
||||||
wantErr: `splitting upstream line "[/!/]8.8.8.8": domain at index 0: ` +
|
|
||||||
`bad domain name "!": bad top-level domain name label "!": ` +
|
|
||||||
`bad top-level domain name label rune '!'`,
|
|
||||||
set: []string{"[/!/]8.8.8.8"},
|
|
||||||
}}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
err := ValidateUpstreams(tc.set)
|
|
||||||
testutil.AssertErrorMsg(t, tc.wantErr, err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestValidateUpstreamsPrivate(t *testing.T) {
|
func TestValidateUpstreamsPrivate(t *testing.T) {
|
||||||
ss := netutil.SubnetSetFunc(netutil.IsLocallyServed)
|
ss := netutil.SubnetSetFunc(netutil.IsLocallyServed)
|
||||||
|
|
||||||
|
|
|
@ -2,10 +2,8 @@ package dnsforward
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
||||||
|
@ -19,28 +17,6 @@ import (
|
||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
// errNotDomainSpecific is returned when the upstream should be
|
|
||||||
// domain-specific, but isn't.
|
|
||||||
errNotDomainSpecific errors.Error = "not a domain-specific upstream"
|
|
||||||
|
|
||||||
// errMissingSeparator is returned when the domain-specific part of the
|
|
||||||
// upstream configuration line isn't closed.
|
|
||||||
errMissingSeparator errors.Error = "missing separator"
|
|
||||||
|
|
||||||
// errDupSeparator is returned when the domain-specific part of the upstream
|
|
||||||
// configuration line contains more than one ending separator.
|
|
||||||
errDupSeparator errors.Error = "duplicated separator"
|
|
||||||
|
|
||||||
// errNoDefaultUpstreams is returned when there are no default upstreams
|
|
||||||
// specified in the upstream configuration.
|
|
||||||
errNoDefaultUpstreams errors.Error = "no default upstreams specified"
|
|
||||||
|
|
||||||
// errWrongResponse is returned when the checked upstream replies in an
|
|
||||||
// unexpected way.
|
|
||||||
errWrongResponse errors.Error = "wrong response"
|
|
||||||
)
|
|
||||||
|
|
||||||
// loadUpstreams parses upstream DNS servers from the configured file or from
|
// loadUpstreams parses upstream DNS servers from the configured file or from
|
||||||
// the configuration itself.
|
// the configuration itself.
|
||||||
func (s *Server) loadUpstreams() (upstreams []string, err error) {
|
func (s *Server) loadUpstreams() (upstreams []string, err error) {
|
||||||
|
@ -199,84 +175,12 @@ func IsCommentOrEmpty(s string) (ok bool) {
|
||||||
return len(s) == 0 || s[0] == '#'
|
return len(s) == 0 || s[0] == '#'
|
||||||
}
|
}
|
||||||
|
|
||||||
// newUpstreamConfig validates upstreams and returns an appropriate upstream
|
|
||||||
// configuration or nil if it can't be built.
|
|
||||||
//
|
|
||||||
// TODO(e.burkov): Perhaps proxy.ParseUpstreamsConfig should validate upstreams
|
|
||||||
// slice already so that this function may be considered useless.
|
|
||||||
func newUpstreamConfig(upstreams []string) (conf *proxy.UpstreamConfig, err error) {
|
|
||||||
// No need to validate comments and empty lines.
|
|
||||||
upstreams = stringutil.FilterOut(upstreams, IsCommentOrEmpty)
|
|
||||||
if len(upstreams) == 0 {
|
|
||||||
// Consider this case valid since it means the default server should be
|
|
||||||
// used.
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err = validateUpstreamConfig(upstreams)
|
|
||||||
if err != nil {
|
|
||||||
// Don't wrap the error since it's informative enough as is.
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
conf, err = proxy.ParseUpstreamsConfig(
|
|
||||||
upstreams,
|
|
||||||
&upstream.Options{
|
|
||||||
Bootstrap: net.DefaultResolver,
|
|
||||||
Timeout: DefaultTimeout,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
// Don't wrap the error since it's informative enough as is.
|
|
||||||
return nil, err
|
|
||||||
} else if len(conf.Upstreams) == 0 {
|
|
||||||
return nil, errNoDefaultUpstreams
|
|
||||||
}
|
|
||||||
|
|
||||||
return conf, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// validateUpstreamConfig validates each upstream from the upstream
|
|
||||||
// configuration and returns an error if any upstream is invalid.
|
|
||||||
//
|
|
||||||
// TODO(e.burkov): Merge with [upstreamConfigValidator] somehow.
|
|
||||||
func validateUpstreamConfig(conf []string) (err error) {
|
|
||||||
for _, u := range conf {
|
|
||||||
var ups []string
|
|
||||||
var isSpecific bool
|
|
||||||
ups, isSpecific, err = splitUpstreamLine(u)
|
|
||||||
if err != nil {
|
|
||||||
// Don't wrap the error since it's informative enough as is.
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, addr := range ups {
|
|
||||||
_, err = validateUpstream(addr, isSpecific)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("validating upstream %q: %w", addr, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateUpstreams validates each upstream and returns an error if any
|
|
||||||
// upstream is invalid or if there are no default upstreams specified.
|
|
||||||
//
|
|
||||||
// TODO(e.burkov): Merge with [upstreamConfigValidator] somehow.
|
|
||||||
func ValidateUpstreams(upstreams []string) (err error) {
|
|
||||||
_, err = newUpstreamConfig(upstreams)
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateUpstreamsPrivate validates each upstream and returns an error if any
|
// ValidateUpstreamsPrivate validates each upstream and returns an error if any
|
||||||
// upstream is invalid or if there are no default upstreams specified. It also
|
// upstream is invalid or if there are no default upstreams specified. It also
|
||||||
// checks each domain of domain-specific upstreams for being ARPA pointing to
|
// checks each domain of domain-specific upstreams for being ARPA pointing to
|
||||||
// a locally-served network. privateNets must not be nil.
|
// a locally-served network. privateNets must not be nil.
|
||||||
func ValidateUpstreamsPrivate(upstreams []string, privateNets netutil.SubnetSet) (err error) {
|
func ValidateUpstreamsPrivate(upstreams []string, privateNets netutil.SubnetSet) (err error) {
|
||||||
conf, err := newUpstreamConfig(upstreams)
|
conf, err := proxy.ParseUpstreamsConfig(upstreams, &upstream.Options{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("creating config: %w", err)
|
return fmt.Errorf("creating config: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -308,66 +212,3 @@ func ValidateUpstreamsPrivate(upstreams []string, privateNets netutil.SubnetSet)
|
||||||
|
|
||||||
return errors.Annotate(errors.Join(errs...), "checking domain-specific upstreams: %w")
|
return errors.Annotate(errors.Join(errs...), "checking domain-specific upstreams: %w")
|
||||||
}
|
}
|
||||||
|
|
||||||
// protocols are the supported URL schemes for upstreams.
|
|
||||||
var protocols = []string{"h3", "https", "quic", "sdns", "tcp", "tls", "udp"}
|
|
||||||
|
|
||||||
// validateUpstream returns an error if u alongside with domains is not a valid
|
|
||||||
// upstream configuration. useDefault is true if the upstream is
|
|
||||||
// domain-specific and is configured to point at the default upstream server
|
|
||||||
// which is validated separately. The upstream is considered domain-specific
|
|
||||||
// only if domains is at least not nil.
|
|
||||||
func validateUpstream(u string, isSpecific bool) (useDefault bool, err error) {
|
|
||||||
// The special server address '#' means that default server must be used.
|
|
||||||
if useDefault = u == "#" && isSpecific; useDefault {
|
|
||||||
return useDefault, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the upstream has a valid protocol prefix.
|
|
||||||
//
|
|
||||||
// TODO(e.burkov): Validate the domain name.
|
|
||||||
if proto, _, ok := strings.Cut(u, "://"); ok {
|
|
||||||
if !slices.Contains(protocols, proto) {
|
|
||||||
return false, fmt.Errorf("bad protocol %q", proto)
|
|
||||||
}
|
|
||||||
} else if _, err = netip.ParseAddr(u); err == nil {
|
|
||||||
return false, nil
|
|
||||||
} else if _, err = netip.ParseAddrPort(u); err == nil {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// splitUpstreamLine returns the upstreams and the specified domains. domains
|
|
||||||
// is nil when the upstream is not domains-specific. Otherwise it may also be
|
|
||||||
// empty.
|
|
||||||
func splitUpstreamLine(upstreamStr string) (upstreams []string, isSpecific bool, err error) {
|
|
||||||
if !strings.HasPrefix(upstreamStr, "[/") {
|
|
||||||
return []string{upstreamStr}, false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() { err = errors.Annotate(err, "splitting upstream line %q: %w", upstreamStr) }()
|
|
||||||
|
|
||||||
doms, ups, found := strings.Cut(upstreamStr[2:], "/]")
|
|
||||||
if !found {
|
|
||||||
return nil, false, errMissingSeparator
|
|
||||||
} else if strings.Contains(ups, "/]") {
|
|
||||||
return nil, false, errDupSeparator
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, host := range strings.Split(doms, "/") {
|
|
||||||
if host == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
err = netutil.ValidateDomainName(strings.TrimPrefix(host, "*."))
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, fmt.Errorf("domain at index %d: %w", i, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
isSpecific = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.Fields(ups), isSpecific, nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -100,8 +100,7 @@ func TestUpstreamConfigValidator(t *testing.T) {
|
||||||
name: "bad_specification",
|
name: "bad_specification",
|
||||||
general: []string{"[/domain.example/]/]1.2.3.4"},
|
general: []string{"[/domain.example/]/]1.2.3.4"},
|
||||||
want: map[string]string{
|
want: map[string]string{
|
||||||
"[/domain.example/]/]1.2.3.4": `splitting upstream line ` +
|
"[/domain.example/]/]1.2.3.4": generalTextLabel + " 1: parsing error",
|
||||||
`"[/domain.example/]/]1.2.3.4": duplicated separator`,
|
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
name: "all_different",
|
name: "all_different",
|
||||||
|
@ -120,23 +119,9 @@ func TestUpstreamConfigValidator(t *testing.T) {
|
||||||
fallback: []string{"[/example/" + goodUps},
|
fallback: []string{"[/example/" + goodUps},
|
||||||
private: []string{"[/example//bad.123/]" + goodUps},
|
private: []string{"[/example//bad.123/]" + goodUps},
|
||||||
want: map[string]string{
|
want: map[string]string{
|
||||||
`[/example/]/]` + goodUps: `splitting upstream line ` +
|
"[/example/]/]" + goodUps: generalTextLabel + " 1: parsing error",
|
||||||
`"[/example/]/]` + goodUps + `": duplicated separator`,
|
"[/example/" + goodUps: fallbackTextLabel + " 1: parsing error",
|
||||||
`[/example/` + goodUps: `splitting upstream line ` +
|
"[/example//bad.123/]" + goodUps: privateTextLabel + " 1: parsing error",
|
||||||
`"[/example/` + goodUps + `": missing separator`,
|
|
||||||
`[/example//bad.123/]` + goodUps: `splitting upstream line ` +
|
|
||||||
`"[/example//bad.123/]` + goodUps + `": domain at index 2: ` +
|
|
||||||
`bad domain name "bad.123": ` +
|
|
||||||
`bad top-level domain name label "123": all octets are numeric`,
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
name: "non-specific_default",
|
|
||||||
general: []string{
|
|
||||||
"#",
|
|
||||||
"[/example/]#",
|
|
||||||
},
|
|
||||||
want: map[string]string{
|
|
||||||
"#": "not a domain-specific upstream",
|
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
name: "bad_proto",
|
name: "bad_proto",
|
||||||
|
@ -144,7 +129,15 @@ func TestUpstreamConfigValidator(t *testing.T) {
|
||||||
"bad://1.2.3.4",
|
"bad://1.2.3.4",
|
||||||
},
|
},
|
||||||
want: map[string]string{
|
want: map[string]string{
|
||||||
"bad://1.2.3.4": `bad protocol "bad"`,
|
"bad://1.2.3.4": generalTextLabel + " 1: parsing error",
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
name: "truncated_line",
|
||||||
|
general: []string{
|
||||||
|
"This is a very long line. It will cause a parsing error and will be truncated here.",
|
||||||
|
},
|
||||||
|
want: map[string]string{
|
||||||
|
"This is a very long line. It will cause a parsing error and will be truncated …": "upstream_dns 1: parsing error",
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
|
|
@ -613,7 +613,7 @@ func (clients *clientsContainer) check(c *persistentClient) (err error) {
|
||||||
// TODO(s.chzhen): Move to the constructor.
|
// TODO(s.chzhen): Move to the constructor.
|
||||||
slices.Sort(c.Tags)
|
slices.Sort(c.Tags)
|
||||||
|
|
||||||
err = dnsforward.ValidateUpstreams(c.Upstreams)
|
_, err = proxy.ParseUpstreamsConfig(c.Upstreams, &upstream.Options{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid upstream servers: %w", err)
|
return fmt.Errorf("invalid upstream servers: %w", err)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue