resolver: wave DNS lookup with fallback pool and bounded retries

This commit is contained in:
beckline
2026-02-21 19:56:14 +03:00
parent a5e93888a5
commit 0b28586f31

View File

@@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"hash/fnv"
"net"
"net/netip"
"os"
@@ -140,6 +141,15 @@ type wildcardMatcher struct {
suffix []string
}
var resolverFallbackDNS = []string{
"1.1.1.1#53",
"1.0.0.1#53",
"9.9.9.9#53",
"149.112.112.112#53",
"8.8.8.8#53",
"8.8.4.4#53",
}
func normalizeWildcardDomain(raw string) string {
d := strings.TrimSpace(strings.SplitN(raw, "#", 2)[0])
d = strings.ToLower(d)
@@ -248,6 +258,14 @@ func runResolverJob(opts ResolverOpts, logf func(string, ...any)) (resolverResul
if workers > 500 {
workers = 500
}
dnsTimeoutMs := envInt("RESOLVE_DNS_TIMEOUT_MS", 1800)
if dnsTimeoutMs < 300 {
dnsTimeoutMs = 300
}
if dnsTimeoutMs > 5000 {
dnsTimeoutMs = 5000
}
dnsTimeout := time.Duration(dnsTimeoutMs) * time.Millisecond
domainCache := loadDomainCacheState(opts.CachePath, logf)
ptrCache := loadJSONMap(opts.PtrCachePath)
@@ -266,7 +284,7 @@ func runResolverJob(opts ResolverOpts, logf func(string, ...any)) (resolverResul
}
if logf != nil {
logf("resolver start: domains=%d ttl=%ds workers=%d", len(domains), ttl, workers)
logf("resolver start: domains=%d ttl=%ds workers=%d dns_timeout_ms=%d", len(domains), ttl, workers, dnsTimeoutMs)
}
start := time.Now()
@@ -309,7 +327,7 @@ func runResolverJob(opts ResolverOpts, logf func(string, ...any)) (resolverResul
for i := 0; i < workers; i++ {
go func() {
for j := range jobs {
ips, stats := resolveHostGo(j.host, cfg, metaSpecial, wildcards, logf)
ips, stats := resolveHostGo(j.host, cfg, metaSpecial, wildcards, dnsTimeout, logf)
results <- struct {
host string
ips []string
@@ -462,7 +480,7 @@ func runResolverJob(opts ResolverOpts, logf func(string, ...any)) (resolverResul
// DNS resolve helpers
// ---------------------------------------------------------------------
func resolveHostGo(host string, cfg dnsConfig, metaSpecial []string, wildcards wildcardMatcher, logf func(string, ...any)) ([]string, dnsMetrics) {
func resolveHostGo(host string, cfg dnsConfig, metaSpecial []string, wildcards wildcardMatcher, timeout time.Duration, logf func(string, ...any)) ([]string, dnsMetrics) {
useMeta := false
for _, m := range metaSpecial {
if host == m {
@@ -484,7 +502,7 @@ func resolveHostGo(host string, cfg dnsConfig, metaSpecial []string, wildcards w
dnsList = []string{cfg.SmartDNS}
}
}
ips, stats := digA(host, dnsList, 3*time.Second, logf)
ips, stats := digA(host, dnsList, timeout, logf)
out := []string{}
seen := map[string]struct{}{}
for _, ip := range ips {
@@ -504,9 +522,22 @@ func resolveHostGo(host string, cfg dnsConfig, metaSpecial []string, wildcards w
// RU: `digA` - содержит основную логику для dig a.
// ---------------------------------------------------------------------
func digA(host string, dnsList []string, timeout time.Duration, logf func(string, ...any)) ([]string, dnsMetrics) {
var ips []string
stats := dnsMetrics{}
for _, entry := range dnsList {
if len(dnsList) == 0 {
return nil, stats
}
tryLimit := envInt("RESOLVE_DNS_TRY_LIMIT", 2)
if tryLimit < 1 {
tryLimit = 1
}
if tryLimit > len(dnsList) {
tryLimit = len(dnsList)
}
start := pickDNSStartIndex(host, len(dnsList))
for attempt := 0; attempt < tryLimit; attempt++ {
entry := dnsList[(start+attempt)%len(dnsList)]
server, port := splitDNS(entry)
if server == "" {
continue
@@ -531,17 +562,24 @@ func digA(host string, dnsList []string, timeout time.Duration, logf func(string
if logf != nil {
logf("dns warn %s via %s: kind=%s err=%v", host, addr, kind, err)
}
// NXDOMAIN usually means authoritative negative answer.
// Do not fan out further retries for this host.
if kind == dnsErrorNXDomain {
break
}
continue
}
stats.addSuccess(addr)
var ips []string
for _, ip := range records {
if isPrivateIPv4(ip) {
continue
}
ips = append(ips, ip)
}
}
return uniqueStrings(ips), stats
}
return nil, stats
}
func classifyDNSError(err error) dnsErrorKind {
@@ -1066,6 +1104,8 @@ func loadDNSConfig(path string, logf func(string, ...any)) dnsConfig {
if len(meta) > 0 {
cfg.Meta = meta
}
cfg.Default = mergeDNSUpstreamPools(cfg.Default, resolverFallbackPool())
cfg.Meta = mergeDNSUpstreamPools(cfg.Meta, resolverFallbackPool())
if logf != nil {
logf("dns-config: accept %s: mode=%s smartdns=%s default=%v; meta=%v", path, cfg.Mode, cfg.SmartDNS, cfg.Default, cfg.Meta)
}
@@ -1130,6 +1170,65 @@ func uniqueStrings(in []string) []string {
return out
}
func pickDNSStartIndex(host string, size int) int {
if size <= 1 {
return 0
}
h := fnv.New32a()
_, _ = h.Write([]byte(strings.ToLower(strings.TrimSpace(host))))
return int(h.Sum32() % uint32(size))
}
func resolverFallbackPool() []string {
raw := strings.TrimSpace(os.Getenv("RESOLVE_DNS_FALLBACKS"))
switch strings.ToLower(raw) {
case "off", "none", "0":
return nil
}
candidates := resolverFallbackDNS
if raw != "" {
candidates = nil
fields := strings.FieldsFunc(raw, func(r rune) bool {
return r == ',' || r == ';' || r == ' ' || r == '\n' || r == '\t'
})
for _, f := range fields {
if n := normalizeDNSUpstream(f, "53"); n != "" {
candidates = append(candidates, n)
}
}
}
return uniqueStrings(candidates)
}
func mergeDNSUpstreamPools(primary, fallback []string) []string {
maxUpstreams := envInt("RESOLVE_DNS_MAX_UPSTREAMS", 12)
if maxUpstreams < 1 {
maxUpstreams = 1
}
out := make([]string, 0, len(primary)+len(fallback))
seen := map[string]struct{}{}
add := func(items []string) {
for _, item := range items {
if len(out) >= maxUpstreams {
return
}
n := normalizeDNSUpstream(item, "53")
if n == "" {
continue
}
if _, ok := seen[n]; ok {
continue
}
seen[n] = struct{}{}
out = append(out, n)
}
}
add(primary)
add(fallback)
return out
}
// ---------------------------------------------------------------------
// text cleanup + IP classifiers
// ---------------------------------------------------------------------