resolver: wave DNS lookup with fallback pool and bounded retries
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"hash/fnv"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
@@ -140,6 +141,15 @@ type wildcardMatcher struct {
|
|||||||
suffix []string
|
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 {
|
func normalizeWildcardDomain(raw string) string {
|
||||||
d := strings.TrimSpace(strings.SplitN(raw, "#", 2)[0])
|
d := strings.TrimSpace(strings.SplitN(raw, "#", 2)[0])
|
||||||
d = strings.ToLower(d)
|
d = strings.ToLower(d)
|
||||||
@@ -248,6 +258,14 @@ func runResolverJob(opts ResolverOpts, logf func(string, ...any)) (resolverResul
|
|||||||
if workers > 500 {
|
if workers > 500 {
|
||||||
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)
|
domainCache := loadDomainCacheState(opts.CachePath, logf)
|
||||||
ptrCache := loadJSONMap(opts.PtrCachePath)
|
ptrCache := loadJSONMap(opts.PtrCachePath)
|
||||||
@@ -266,7 +284,7 @@ func runResolverJob(opts ResolverOpts, logf func(string, ...any)) (resolverResul
|
|||||||
}
|
}
|
||||||
|
|
||||||
if logf != nil {
|
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()
|
start := time.Now()
|
||||||
|
|
||||||
@@ -309,7 +327,7 @@ func runResolverJob(opts ResolverOpts, logf func(string, ...any)) (resolverResul
|
|||||||
for i := 0; i < workers; i++ {
|
for i := 0; i < workers; i++ {
|
||||||
go func() {
|
go func() {
|
||||||
for j := range jobs {
|
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 {
|
results <- struct {
|
||||||
host string
|
host string
|
||||||
ips []string
|
ips []string
|
||||||
@@ -462,7 +480,7 @@ func runResolverJob(opts ResolverOpts, logf func(string, ...any)) (resolverResul
|
|||||||
// DNS resolve helpers
|
// 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
|
useMeta := false
|
||||||
for _, m := range metaSpecial {
|
for _, m := range metaSpecial {
|
||||||
if host == m {
|
if host == m {
|
||||||
@@ -484,7 +502,7 @@ func resolveHostGo(host string, cfg dnsConfig, metaSpecial []string, wildcards w
|
|||||||
dnsList = []string{cfg.SmartDNS}
|
dnsList = []string{cfg.SmartDNS}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ips, stats := digA(host, dnsList, 3*time.Second, logf)
|
ips, stats := digA(host, dnsList, timeout, logf)
|
||||||
out := []string{}
|
out := []string{}
|
||||||
seen := map[string]struct{}{}
|
seen := map[string]struct{}{}
|
||||||
for _, ip := range ips {
|
for _, ip := range ips {
|
||||||
@@ -504,9 +522,22 @@ func resolveHostGo(host string, cfg dnsConfig, metaSpecial []string, wildcards w
|
|||||||
// RU: `digA` - содержит основную логику для dig a.
|
// RU: `digA` - содержит основную логику для dig a.
|
||||||
// ---------------------------------------------------------------------
|
// ---------------------------------------------------------------------
|
||||||
func digA(host string, dnsList []string, timeout time.Duration, logf func(string, ...any)) ([]string, dnsMetrics) {
|
func digA(host string, dnsList []string, timeout time.Duration, logf func(string, ...any)) ([]string, dnsMetrics) {
|
||||||
var ips []string
|
|
||||||
stats := dnsMetrics{}
|
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)
|
server, port := splitDNS(entry)
|
||||||
if server == "" {
|
if server == "" {
|
||||||
continue
|
continue
|
||||||
@@ -531,18 +562,25 @@ func digA(host string, dnsList []string, timeout time.Duration, logf func(string
|
|||||||
if logf != nil {
|
if logf != nil {
|
||||||
logf("dns warn %s via %s: kind=%s err=%v", host, addr, kind, err)
|
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
|
continue
|
||||||
}
|
}
|
||||||
stats.addSuccess(addr)
|
stats.addSuccess(addr)
|
||||||
|
var ips []string
|
||||||
for _, ip := range records {
|
for _, ip := range records {
|
||||||
if isPrivateIPv4(ip) {
|
if isPrivateIPv4(ip) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
ips = append(ips, ip)
|
ips = append(ips, ip)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return uniqueStrings(ips), stats
|
return uniqueStrings(ips), stats
|
||||||
}
|
}
|
||||||
|
return nil, stats
|
||||||
|
}
|
||||||
|
|
||||||
func classifyDNSError(err error) dnsErrorKind {
|
func classifyDNSError(err error) dnsErrorKind {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -1066,6 +1104,8 @@ func loadDNSConfig(path string, logf func(string, ...any)) dnsConfig {
|
|||||||
if len(meta) > 0 {
|
if len(meta) > 0 {
|
||||||
cfg.Meta = meta
|
cfg.Meta = meta
|
||||||
}
|
}
|
||||||
|
cfg.Default = mergeDNSUpstreamPools(cfg.Default, resolverFallbackPool())
|
||||||
|
cfg.Meta = mergeDNSUpstreamPools(cfg.Meta, resolverFallbackPool())
|
||||||
if logf != nil {
|
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)
|
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
|
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
|
// text cleanup + IP classifiers
|
||||||
// ---------------------------------------------------------------------
|
// ---------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user