Harden resolver and expand traffic runtime controls
This commit is contained in:
@@ -265,6 +265,23 @@ func runResolverJob(opts ResolverOpts, logf func(string, ...any)) (resolverResul
|
||||
domainCache := loadDomainCacheState(opts.CachePath, logf)
|
||||
ptrCache := loadJSONMap(opts.PtrCachePath)
|
||||
now := int(time.Now().Unix())
|
||||
negTTLNX := envInt("RESOLVE_NEGATIVE_TTL_NX", 6*3600)
|
||||
negTTLTimeout := envInt("RESOLVE_NEGATIVE_TTL_TIMEOUT", 15*60)
|
||||
negTTLTemporary := envInt("RESOLVE_NEGATIVE_TTL_TEMPORARY", 10*60)
|
||||
negTTLOther := envInt("RESOLVE_NEGATIVE_TTL_OTHER", 10*60)
|
||||
clampTTL := func(v int) int {
|
||||
if v < 0 {
|
||||
return 0
|
||||
}
|
||||
if v > 24*3600 {
|
||||
return 24 * 3600
|
||||
}
|
||||
return v
|
||||
}
|
||||
negTTLNX = clampTTL(negTTLNX)
|
||||
negTTLTimeout = clampTTL(negTTLTimeout)
|
||||
negTTLTemporary = clampTTL(negTTLTemporary)
|
||||
negTTLOther = clampTTL(negTTLOther)
|
||||
|
||||
cacheSourceForHost := func(host string) domainCacheSource {
|
||||
switch cfg.Mode {
|
||||
@@ -284,6 +301,7 @@ func runResolverJob(opts ResolverOpts, logf func(string, ...any)) (resolverResul
|
||||
start := time.Now()
|
||||
|
||||
fresh := map[string][]string{}
|
||||
cacheNegativeHits := 0
|
||||
var toResolve []string
|
||||
for _, d := range domains {
|
||||
source := cacheSourceForHost(d)
|
||||
@@ -294,6 +312,13 @@ func runResolverJob(opts ResolverOpts, logf func(string, ...any)) (resolverResul
|
||||
}
|
||||
continue
|
||||
}
|
||||
if kind, age, ok := domainCache.getNegative(d, source, now, negTTLNX, negTTLTimeout, negTTLTemporary, negTTLOther); ok {
|
||||
cacheNegativeHits++
|
||||
if logf != nil {
|
||||
logf("cache neg hit[%s/%s age=%ds]: %s", source, kind, age, d)
|
||||
}
|
||||
continue
|
||||
}
|
||||
toResolve = append(toResolve, d)
|
||||
}
|
||||
|
||||
@@ -303,7 +328,7 @@ func runResolverJob(opts ResolverOpts, logf func(string, ...any)) (resolverResul
|
||||
}
|
||||
|
||||
if logf != nil {
|
||||
logf("resolve: domains=%d cache_hits=%d to_resolve=%d", len(domains), len(fresh), len(toResolve))
|
||||
logf("resolve: domains=%d cache_hits=%d cache_neg_hits=%d to_resolve=%d", len(domains), len(fresh), cacheNegativeHits, len(toResolve))
|
||||
}
|
||||
|
||||
dnsStats := dnsMetrics{}
|
||||
@@ -349,8 +374,16 @@ func runResolverJob(opts ResolverOpts, logf func(string, ...any)) (resolverResul
|
||||
if logf != nil {
|
||||
logf("%s -> %v", r.host, r.ips)
|
||||
}
|
||||
} else if logf != nil {
|
||||
logf("%s: no IPs", r.host)
|
||||
} else {
|
||||
if hostErrors > 0 {
|
||||
source := cacheSourceForHost(r.host)
|
||||
if kind, ok := classifyHostErrorKind(r.stats); ok {
|
||||
domainCache.setError(r.host, source, kind, now)
|
||||
}
|
||||
}
|
||||
if logf != nil {
|
||||
logf("%s: no IPs", r.host)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -443,9 +476,10 @@ func runResolverJob(opts ResolverOpts, logf func(string, ...any)) (resolverResul
|
||||
if logf != nil {
|
||||
dnsErrors := dnsStats.totalErrors()
|
||||
logf(
|
||||
"resolve summary: domains=%d cache_hits=%d resolved_now=%d unresolved=%d static_entries=%d static_skipped=%d unique_ips=%d direct_ips=%d wildcard_ips=%d ptr_lookups=%d ptr_errors=%d dns_attempts=%d dns_ok=%d dns_nxdomain=%d dns_timeout=%d dns_temporary=%d dns_other=%d dns_errors=%d duration_ms=%d",
|
||||
"resolve summary: domains=%d cache_hits=%d cache_neg_hits=%d resolved_now=%d unresolved=%d static_entries=%d static_skipped=%d unique_ips=%d direct_ips=%d wildcard_ips=%d ptr_lookups=%d ptr_errors=%d dns_attempts=%d dns_ok=%d dns_nxdomain=%d dns_timeout=%d dns_temporary=%d dns_other=%d dns_errors=%d duration_ms=%d",
|
||||
len(domains),
|
||||
len(fresh),
|
||||
cacheNegativeHits,
|
||||
len(resolved)-len(fresh),
|
||||
len(domains)-len(resolved),
|
||||
len(staticEntries),
|
||||
@@ -487,17 +521,45 @@ func resolveHostGo(host string, cfg dnsConfig, metaSpecial []string, wildcards w
|
||||
if useMeta {
|
||||
dnsList = cfg.Meta
|
||||
}
|
||||
primaryViaSmartDNS := false
|
||||
switch cfg.Mode {
|
||||
case DNSModeSmartDNS:
|
||||
if cfg.SmartDNS != "" {
|
||||
dnsList = []string{cfg.SmartDNS}
|
||||
primaryViaSmartDNS = true
|
||||
}
|
||||
case DNSModeHybridWildcard:
|
||||
if cfg.SmartDNS != "" && wildcards.match(host) {
|
||||
dnsList = []string{cfg.SmartDNS}
|
||||
primaryViaSmartDNS = true
|
||||
}
|
||||
}
|
||||
ips, stats := digA(host, dnsList, timeout, logf)
|
||||
if len(ips) == 0 &&
|
||||
!primaryViaSmartDNS &&
|
||||
cfg.SmartDNS != "" &&
|
||||
smartDNSFallbackForTimeoutEnabled() &&
|
||||
shouldFallbackToSmartDNS(stats) {
|
||||
if logf != nil {
|
||||
logf(
|
||||
"dns fallback %s: trying smartdns=%s after errors nxdomain=%d timeout=%d temporary=%d other=%d",
|
||||
host,
|
||||
cfg.SmartDNS,
|
||||
stats.NXDomain,
|
||||
stats.Timeout,
|
||||
stats.Temporary,
|
||||
stats.Other,
|
||||
)
|
||||
}
|
||||
fallbackIPs, fallbackStats := digA(host, []string{cfg.SmartDNS}, timeout, logf)
|
||||
stats.merge(fallbackStats)
|
||||
if len(fallbackIPs) > 0 {
|
||||
ips = fallbackIPs
|
||||
if logf != nil {
|
||||
logf("dns fallback %s: resolved via smartdns (%d ips)", host, len(fallbackIPs))
|
||||
}
|
||||
}
|
||||
}
|
||||
out := []string{}
|
||||
seen := map[string]struct{}{}
|
||||
for _, ip := range ips {
|
||||
@@ -512,6 +574,52 @@ func resolveHostGo(host string, cfg dnsConfig, metaSpecial []string, wildcards w
|
||||
return out, stats
|
||||
}
|
||||
|
||||
// smartDNSFallbackForTimeoutEnabled controls direct->SmartDNS fallback behavior.
|
||||
// Default is disabled to avoid overloading SmartDNS on large unresolved batches.
|
||||
// Set RESOLVE_SMARTDNS_TIMEOUT_FALLBACK=1 to enable.
|
||||
func smartDNSFallbackForTimeoutEnabled() bool {
|
||||
v := strings.ToLower(strings.TrimSpace(os.Getenv("RESOLVE_SMARTDNS_TIMEOUT_FALLBACK")))
|
||||
switch v {
|
||||
case "1", "true", "yes", "on":
|
||||
return true
|
||||
case "0", "false", "no", "off":
|
||||
return false
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback is useful only for transport-like errors. If we already got NXDOMAIN,
|
||||
// SmartDNS fallback is unlikely to change result and only adds latency/noise.
|
||||
func shouldFallbackToSmartDNS(stats dnsMetrics) bool {
|
||||
if stats.OK > 0 {
|
||||
return false
|
||||
}
|
||||
if stats.NXDomain > 0 {
|
||||
return false
|
||||
}
|
||||
if stats.Timeout > 0 || stats.Temporary > 0 {
|
||||
return true
|
||||
}
|
||||
return stats.Other > 0
|
||||
}
|
||||
|
||||
func classifyHostErrorKind(stats dnsMetrics) (dnsErrorKind, bool) {
|
||||
if stats.Timeout > 0 {
|
||||
return dnsErrorTimeout, true
|
||||
}
|
||||
if stats.Temporary > 0 {
|
||||
return dnsErrorTemporary, true
|
||||
}
|
||||
if stats.Other > 0 {
|
||||
return dnsErrorOther, true
|
||||
}
|
||||
if stats.NXDomain > 0 {
|
||||
return dnsErrorNXDomain, true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// EN: `digA` contains core logic for dig a.
|
||||
// RU: `digA` - содержит основную логику для dig a.
|
||||
@@ -742,8 +850,10 @@ const (
|
||||
)
|
||||
|
||||
type domainCacheEntry struct {
|
||||
IPs []string `json:"ips"`
|
||||
LastResolved int `json:"last_resolved"`
|
||||
IPs []string `json:"ips,omitempty"`
|
||||
LastResolved int `json:"last_resolved,omitempty"`
|
||||
LastErrorKind string `json:"last_error_kind,omitempty"`
|
||||
LastErrorAt int `json:"last_error_at,omitempty"`
|
||||
}
|
||||
|
||||
type domainCacheRecord struct {
|
||||
@@ -758,7 +868,7 @@ type domainCacheState struct {
|
||||
|
||||
func newDomainCacheState() domainCacheState {
|
||||
return domainCacheState{
|
||||
Version: 2,
|
||||
Version: 3,
|
||||
Domains: map[string]domainCacheRecord{},
|
||||
}
|
||||
}
|
||||
@@ -781,6 +891,41 @@ func normalizeCacheIPs(raw []string) []string {
|
||||
return out
|
||||
}
|
||||
|
||||
func normalizeCacheErrorKind(raw string) (dnsErrorKind, bool) {
|
||||
switch strings.ToLower(strings.TrimSpace(raw)) {
|
||||
case string(dnsErrorNXDomain):
|
||||
return dnsErrorNXDomain, true
|
||||
case string(dnsErrorTimeout):
|
||||
return dnsErrorTimeout, true
|
||||
case string(dnsErrorTemporary):
|
||||
return dnsErrorTemporary, true
|
||||
case string(dnsErrorOther):
|
||||
return dnsErrorOther, true
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeDomainCacheEntry(in *domainCacheEntry) *domainCacheEntry {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := &domainCacheEntry{}
|
||||
ips := normalizeCacheIPs(in.IPs)
|
||||
if len(ips) > 0 && in.LastResolved > 0 {
|
||||
out.IPs = ips
|
||||
out.LastResolved = in.LastResolved
|
||||
}
|
||||
if kind, ok := normalizeCacheErrorKind(in.LastErrorKind); ok && in.LastErrorAt > 0 {
|
||||
out.LastErrorKind = string(kind)
|
||||
out.LastErrorAt = in.LastErrorAt
|
||||
}
|
||||
if out.LastResolved <= 0 && out.LastErrorAt <= 0 {
|
||||
return nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func parseAnyStringSlice(raw any) []string {
|
||||
switch v := raw.(type) {
|
||||
case []string:
|
||||
@@ -842,7 +987,7 @@ func loadDomainCacheState(path string, logf func(string, ...any)) domainCacheSta
|
||||
var st domainCacheState
|
||||
if err := json.Unmarshal(data, &st); err == nil && st.Domains != nil {
|
||||
if st.Version <= 0 {
|
||||
st.Version = 2
|
||||
st.Version = 3
|
||||
}
|
||||
normalized := newDomainCacheState()
|
||||
for host, rec := range st.Domains {
|
||||
@@ -851,18 +996,8 @@ func loadDomainCacheState(path string, logf func(string, ...any)) domainCacheSta
|
||||
continue
|
||||
}
|
||||
nrec := domainCacheRecord{}
|
||||
if rec.Direct != nil {
|
||||
ips := normalizeCacheIPs(rec.Direct.IPs)
|
||||
if len(ips) > 0 && rec.Direct.LastResolved > 0 {
|
||||
nrec.Direct = &domainCacheEntry{IPs: ips, LastResolved: rec.Direct.LastResolved}
|
||||
}
|
||||
}
|
||||
if rec.Wildcard != nil {
|
||||
ips := normalizeCacheIPs(rec.Wildcard.IPs)
|
||||
if len(ips) > 0 && rec.Wildcard.LastResolved > 0 {
|
||||
nrec.Wildcard = &domainCacheEntry{IPs: ips, LastResolved: rec.Wildcard.LastResolved}
|
||||
}
|
||||
}
|
||||
nrec.Direct = normalizeDomainCacheEntry(rec.Direct)
|
||||
nrec.Wildcard = normalizeDomainCacheEntry(rec.Wildcard)
|
||||
if nrec.Direct != nil || nrec.Wildcard != nil {
|
||||
normalized.Domains[host] = nrec
|
||||
}
|
||||
@@ -926,6 +1061,46 @@ func (s domainCacheState) get(domain string, source domainCacheSource, now, ttl
|
||||
return ips, true
|
||||
}
|
||||
|
||||
func (s domainCacheState) getNegative(domain string, source domainCacheSource, now, nxTTL, timeoutTTL, temporaryTTL, otherTTL int) (dnsErrorKind, int, bool) {
|
||||
rec, ok := s.Domains[strings.TrimSpace(strings.ToLower(domain))]
|
||||
if !ok {
|
||||
return "", 0, false
|
||||
}
|
||||
var entry *domainCacheEntry
|
||||
switch source {
|
||||
case domainCacheSourceWildcard:
|
||||
entry = rec.Wildcard
|
||||
default:
|
||||
entry = rec.Direct
|
||||
}
|
||||
if entry == nil || entry.LastErrorAt <= 0 {
|
||||
return "", 0, false
|
||||
}
|
||||
kind, ok := normalizeCacheErrorKind(entry.LastErrorKind)
|
||||
if !ok {
|
||||
return "", 0, false
|
||||
}
|
||||
age := now - entry.LastErrorAt
|
||||
if age < 0 {
|
||||
return "", 0, false
|
||||
}
|
||||
cacheTTL := 0
|
||||
switch kind {
|
||||
case dnsErrorNXDomain:
|
||||
cacheTTL = nxTTL
|
||||
case dnsErrorTimeout:
|
||||
cacheTTL = timeoutTTL
|
||||
case dnsErrorTemporary:
|
||||
cacheTTL = temporaryTTL
|
||||
case dnsErrorOther:
|
||||
cacheTTL = otherTTL
|
||||
}
|
||||
if cacheTTL <= 0 || age > cacheTTL {
|
||||
return "", 0, false
|
||||
}
|
||||
return kind, age, true
|
||||
}
|
||||
|
||||
func (s *domainCacheState) set(domain string, source domainCacheSource, ips []string, now int) {
|
||||
host := strings.TrimSpace(strings.ToLower(domain))
|
||||
if host == "" || now <= 0 {
|
||||
@@ -939,7 +1114,10 @@ func (s *domainCacheState) set(domain string, source domainCacheSource, ips []st
|
||||
s.Domains = map[string]domainCacheRecord{}
|
||||
}
|
||||
rec := s.Domains[host]
|
||||
entry := &domainCacheEntry{IPs: norm, LastResolved: now}
|
||||
entry := &domainCacheEntry{
|
||||
IPs: norm,
|
||||
LastResolved: now,
|
||||
}
|
||||
switch source {
|
||||
case domainCacheSourceWildcard:
|
||||
rec.Wildcard = entry
|
||||
@@ -949,9 +1127,39 @@ func (s *domainCacheState) set(domain string, source domainCacheSource, ips []st
|
||||
s.Domains[host] = rec
|
||||
}
|
||||
|
||||
func (s *domainCacheState) setError(domain string, source domainCacheSource, kind dnsErrorKind, now int) {
|
||||
host := strings.TrimSpace(strings.ToLower(domain))
|
||||
if host == "" || now <= 0 {
|
||||
return
|
||||
}
|
||||
normKind, ok := normalizeCacheErrorKind(string(kind))
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if s.Domains == nil {
|
||||
s.Domains = map[string]domainCacheRecord{}
|
||||
}
|
||||
rec := s.Domains[host]
|
||||
switch source {
|
||||
case domainCacheSourceWildcard:
|
||||
if rec.Wildcard == nil {
|
||||
rec.Wildcard = &domainCacheEntry{}
|
||||
}
|
||||
rec.Wildcard.LastErrorKind = string(normKind)
|
||||
rec.Wildcard.LastErrorAt = now
|
||||
default:
|
||||
if rec.Direct == nil {
|
||||
rec.Direct = &domainCacheEntry{}
|
||||
}
|
||||
rec.Direct.LastErrorKind = string(normKind)
|
||||
rec.Direct.LastErrorAt = now
|
||||
}
|
||||
s.Domains[host] = rec
|
||||
}
|
||||
|
||||
func (s domainCacheState) toMap() map[string]any {
|
||||
out := map[string]any{
|
||||
"version": 2,
|
||||
"version": 3,
|
||||
"domains": map[string]any{},
|
||||
}
|
||||
domainsAny := out["domains"].(map[string]any)
|
||||
@@ -963,16 +1171,32 @@ func (s domainCacheState) toMap() map[string]any {
|
||||
for _, host := range hosts {
|
||||
rec := s.Domains[host]
|
||||
recOut := map[string]any{}
|
||||
if rec.Direct != nil && len(rec.Direct.IPs) > 0 && rec.Direct.LastResolved > 0 {
|
||||
recOut["direct"] = map[string]any{
|
||||
"ips": rec.Direct.IPs,
|
||||
"last_resolved": rec.Direct.LastResolved,
|
||||
if rec.Direct != nil {
|
||||
directOut := map[string]any{}
|
||||
if len(rec.Direct.IPs) > 0 && rec.Direct.LastResolved > 0 {
|
||||
directOut["ips"] = rec.Direct.IPs
|
||||
directOut["last_resolved"] = rec.Direct.LastResolved
|
||||
}
|
||||
if kind, ok := normalizeCacheErrorKind(rec.Direct.LastErrorKind); ok && rec.Direct.LastErrorAt > 0 {
|
||||
directOut["last_error_kind"] = string(kind)
|
||||
directOut["last_error_at"] = rec.Direct.LastErrorAt
|
||||
}
|
||||
if len(directOut) > 0 {
|
||||
recOut["direct"] = directOut
|
||||
}
|
||||
}
|
||||
if rec.Wildcard != nil && len(rec.Wildcard.IPs) > 0 && rec.Wildcard.LastResolved > 0 {
|
||||
recOut["wildcard"] = map[string]any{
|
||||
"ips": rec.Wildcard.IPs,
|
||||
"last_resolved": rec.Wildcard.LastResolved,
|
||||
if rec.Wildcard != nil {
|
||||
wildOut := map[string]any{}
|
||||
if len(rec.Wildcard.IPs) > 0 && rec.Wildcard.LastResolved > 0 {
|
||||
wildOut["ips"] = rec.Wildcard.IPs
|
||||
wildOut["last_resolved"] = rec.Wildcard.LastResolved
|
||||
}
|
||||
if kind, ok := normalizeCacheErrorKind(rec.Wildcard.LastErrorKind); ok && rec.Wildcard.LastErrorAt > 0 {
|
||||
wildOut["last_error_kind"] = string(kind)
|
||||
wildOut["last_error_at"] = rec.Wildcard.LastErrorAt
|
||||
}
|
||||
if len(wildOut) > 0 {
|
||||
recOut["wildcard"] = wildOut
|
||||
}
|
||||
}
|
||||
if len(recOut) > 0 {
|
||||
|
||||
Reference in New Issue
Block a user