From a95b659e15dea2694e33465fb7ea8c072d505930 Mon Sep 17 00:00:00 2001 From: Sil3ntVip3r Date: Mon, 3 Aug 2026 01:47:09 -0400 Subject: [PATCH 1/2] proxy: normalize idna upstream domains --- proxy/upstreams.go | 25 +++++++++++++++++++++++-- proxy/upstreams_internal_test.go | 16 ++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/proxy/upstreams.go b/proxy/upstreams.go index 79f659db8..f82b23b10 100644 --- a/proxy/upstreams.go +++ b/proxy/upstreams.go @@ -12,6 +12,7 @@ import ( "github.com/AdguardTeam/golibs/container" "github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/netutil" + "golang.org/x/net/idna" ) // UnqualifiedNames is a key for [UpstreamConfig.DomainReservedUpstreams] map to @@ -221,6 +222,26 @@ func (p *configParser) parseLine(idx int, confLine string) (err error) { return nil } +// normalizeDomainSpec validates domain and returns its canonical ASCII form. +func normalizeDomainSpec(domain string) (normalized string, err error) { + wildcard := strings.HasPrefix(domain, "*.") + domain = strings.TrimPrefix(domain, "*.") + err = netutil.ValidateDomainName(domain) + if err != nil { + return "", err + } + + domain, err = idna.ToASCII(domain) + if err != nil { + return "", fmt.Errorf("converting to ASCII: %w", err) + } + if wildcard { + domain = "*." + domain + } + + return strings.ToLower(domain + labelSep), nil +} + // splitConfigLine parses upstream configuration line and returns list upstream // addresses (one or many), list of domains for which this upstream is reserved // (may be nil). It returns an error if the upstream format is incorrect. @@ -243,12 +264,12 @@ func splitConfigLine(confLine string) (upstreams, domains []string, err error) { continue } - err = netutil.ValidateDomainName(strings.TrimPrefix(confHost, "*.")) + confHost, err = normalizeDomainSpec(confHost) if err != nil { return nil, nil, fmt.Errorf("domain at index %d: %w", i, err) } - domains = append(domains, strings.ToLower(confHost+labelSep)) + domains = append(domains, confHost) } return strings.Fields(upstreamsLine), domains, nil diff --git a/proxy/upstreams_internal_test.go b/proxy/upstreams_internal_test.go index bc13ce570..2b0fea1e6 100644 --- a/proxy/upstreams_internal_test.go +++ b/proxy/upstreams_internal_test.go @@ -117,6 +117,22 @@ func TestUpstreamConfig_GetUpstreamsForDomain(t *testing.T) { } } +func TestUpstreamConfig_GetUpstreamsForDomain_IDNA(t *testing.T) { + t.Parallel() + + const idnaUpstream = "tcp://idna.upstream:53" + + config, err := ParseUpstreamsConfig([]string{ + generalUpstream, + "[/恒天.com/]" + idnaUpstream, + }, nil) + require.NoError(t, err) + testutil.CleanupAndRequireSuccess(t, config.Close) + + ups := config.getUpstreamsForDomain("xn--rss99n.com.") + assertUpstreamsAddrs(t, ups, []string{idnaUpstream}) +} + func TestUpstreamConfig_GetUpstreamsForDS(t *testing.T) { t.Parallel() From a8beba193947c0a402f5c578389586fdd0a3e487 Mon Sep 17 00:00:00 2001 From: Sil3ntVip3r Date: Mon, 3 Aug 2026 02:03:50 -0400 Subject: [PATCH 2/2] proxy: canonicalize idna lookup domains --- proxy/upstreams.go | 10 +++++++++- proxy/upstreams_internal_test.go | 4 ++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/proxy/upstreams.go b/proxy/upstreams.go index f82b23b10..d45c0745b 100644 --- a/proxy/upstreams.go +++ b/proxy/upstreams.go @@ -23,6 +23,14 @@ const UnqualifiedNames = "unqualified_names" // labelSep is a separator between labels of a domain name. const labelSep = "." +// domainSpecIDNA converts domains to their canonical lookup form while +// preserving the permissive domain-name validation used by this parser. +var domainSpecIDNA = idna.New( + idna.MapForLookup(), + idna.StrictDomainName(false), + idna.ValidateLabels(false), +) + // UpstreamConfig maps domain names to upstreams. type UpstreamConfig struct { // DomainReservedUpstreams maps the domains to the upstreams. @@ -231,7 +239,7 @@ func normalizeDomainSpec(domain string) (normalized string, err error) { return "", err } - domain, err = idna.ToASCII(domain) + domain, err = domainSpecIDNA.ToASCII(domain) if err != nil { return "", fmt.Errorf("converting to ASCII: %w", err) } diff --git a/proxy/upstreams_internal_test.go b/proxy/upstreams_internal_test.go index 2b0fea1e6..b01484ea2 100644 --- a/proxy/upstreams_internal_test.go +++ b/proxy/upstreams_internal_test.go @@ -125,12 +125,16 @@ func TestUpstreamConfig_GetUpstreamsForDomain_IDNA(t *testing.T) { config, err := ParseUpstreamsConfig([]string{ generalUpstream, "[/恒天.com/]" + idnaUpstream, + "[/GÖPHER.com/]" + idnaUpstream, }, nil) require.NoError(t, err) testutil.CleanupAndRequireSuccess(t, config.Close) ups := config.getUpstreamsForDomain("xn--rss99n.com.") assertUpstreamsAddrs(t, ups, []string{idnaUpstream}) + + ups = config.getUpstreamsForDomain("xn--gpher-jua.com.") + assertUpstreamsAddrs(t, ups, []string{idnaUpstream}) } func TestUpstreamConfig_GetUpstreamsForDS(t *testing.T) {