package app import ( "context" "encoding/json" "errors" "fmt" "net" "net/netip" "os" "regexp" "sort" "strconv" "strings" "time" ) // --------------------------------------------------------------------- // Go resolver // --------------------------------------------------------------------- // EN: Go-based domain resolver pipeline used by routes update. // EN: Handles cache reuse, concurrent DNS lookups, PTR labeling for static entries, // EN: and returns deduplicated IP sets plus IP-to-label mapping artifacts. // RU: Go-резолвер, используемый пайплайном обновления маршрутов. // RU: Обрабатывает кэш, конкурентные DNS-запросы, PTR-лейблы для static entries // RU: и возвращает дедуплицированный список IP и IP-to-label mapping. type dnsErrorKind string const ( dnsErrorNXDomain dnsErrorKind = "nxdomain" dnsErrorTimeout dnsErrorKind = "timeout" dnsErrorTemporary dnsErrorKind = "temporary" dnsErrorOther dnsErrorKind = "other" ) type dnsUpstreamMetrics struct { Attempts int OK int NXDomain int Timeout int Temporary int Other int } type dnsMetrics struct { Attempts int OK int NXDomain int Timeout int Temporary int Other int PerUpstream map[string]*dnsUpstreamMetrics } func (m *dnsMetrics) ensureUpstream(upstream string) *dnsUpstreamMetrics { if m.PerUpstream == nil { m.PerUpstream = map[string]*dnsUpstreamMetrics{} } if us, ok := m.PerUpstream[upstream]; ok { return us } us := &dnsUpstreamMetrics{} m.PerUpstream[upstream] = us return us } func (m *dnsMetrics) addSuccess(upstream string) { m.Attempts++ m.OK++ us := m.ensureUpstream(upstream) us.Attempts++ us.OK++ } func (m *dnsMetrics) addError(upstream string, kind dnsErrorKind) { m.Attempts++ us := m.ensureUpstream(upstream) us.Attempts++ switch kind { case dnsErrorNXDomain: m.NXDomain++ us.NXDomain++ case dnsErrorTimeout: m.Timeout++ us.Timeout++ case dnsErrorTemporary: m.Temporary++ us.Temporary++ default: m.Other++ us.Other++ } } func (m *dnsMetrics) merge(other dnsMetrics) { m.Attempts += other.Attempts m.OK += other.OK m.NXDomain += other.NXDomain m.Timeout += other.Timeout m.Temporary += other.Temporary m.Other += other.Other for upstream, src := range other.PerUpstream { dst := m.ensureUpstream(upstream) dst.Attempts += src.Attempts dst.OK += src.OK dst.NXDomain += src.NXDomain dst.Timeout += src.Timeout dst.Temporary += src.Temporary dst.Other += src.Other } } func (m dnsMetrics) totalErrors() int { return m.NXDomain + m.Timeout + m.Temporary + m.Other } func (m dnsMetrics) formatPerUpstream() string { if len(m.PerUpstream) == 0 { return "" } keys := make([]string, 0, len(m.PerUpstream)) for k := range m.PerUpstream { keys = append(keys, k) } sort.Strings(keys) parts := make([]string, 0, len(keys)) for _, k := range keys { v := m.PerUpstream[k] parts = append(parts, fmt.Sprintf("%s{attempts=%d ok=%d nxdomain=%d timeout=%d temporary=%d other=%d}", k, v.Attempts, v.OK, v.NXDomain, v.Timeout, v.Temporary, v.Other)) } return strings.Join(parts, "; ") } type wildcardMatcher struct { exact map[string]struct{} suffix []string } func normalizeWildcardDomain(raw string) string { d := strings.TrimSpace(strings.SplitN(raw, "#", 2)[0]) d = strings.ToLower(d) d = strings.TrimPrefix(d, "*.") d = strings.TrimPrefix(d, ".") d = strings.TrimSuffix(d, ".") return d } func newWildcardMatcher(domains []string) wildcardMatcher { seen := map[string]struct{}{} m := wildcardMatcher{exact: map[string]struct{}{}} for _, raw := range domains { d := normalizeWildcardDomain(raw) if d == "" { continue } if _, ok := seen[d]; ok { continue } seen[d] = struct{}{} m.exact[d] = struct{}{} m.suffix = append(m.suffix, "."+d) } return m } func (m wildcardMatcher) match(host string) bool { if len(m.exact) == 0 { return false } h := strings.TrimSuffix(strings.ToLower(strings.TrimSpace(host)), ".") if h == "" { return false } if _, ok := m.exact[h]; ok { return true } for _, suffix := range m.suffix { if strings.HasSuffix(h, suffix) { return true } } return false } // --------------------------------------------------------------------- // EN: `runResolverJob` runs the workflow for resolver job. // RU: `runResolverJob` - запускает рабочий процесс для resolver job. // --------------------------------------------------------------------- func runResolverJob(opts ResolverOpts, logf func(string, ...any)) (resolverResult, error) { res := resolverResult{ DomainCache: map[string]any{}, PtrCache: map[string]any{}, } domains := loadList(opts.DomainsPath) metaSpecial := loadList(opts.MetaPath) staticLines := readLinesAllowMissing(opts.StaticPath) wildcards := newWildcardMatcher(opts.SmartDNSWildcards) cfg := loadDNSConfig(opts.DNSConfigPath, logf) if !smartDNSForced() { cfg.Mode = normalizeDNSResolverMode(opts.Mode, opts.ViaSmartDNS) } if addr := normalizeSmartDNSAddr(opts.SmartDNSAddr); addr != "" { cfg.SmartDNS = addr } if cfg.SmartDNS == "" { cfg.SmartDNS = smartDNSAddr() } if cfg.Mode == DNSModeSmartDNS && cfg.SmartDNS != "" { cfg.Default = []string{cfg.SmartDNS} cfg.Meta = []string{cfg.SmartDNS} } if logf != nil { switch cfg.Mode { case DNSModeSmartDNS: logf("resolver dns mode: SmartDNS-only (%s)", cfg.SmartDNS) case DNSModeHybridWildcard: logf("resolver dns mode: hybrid_wildcard smartdns=%s wildcards=%d default=%v meta=%v", cfg.SmartDNS, len(wildcards.exact), cfg.Default, cfg.Meta) default: logf("resolver dns mode: direct default=%v meta=%v", cfg.Default, cfg.Meta) } } ttl := opts.TTL if ttl <= 0 { ttl = 24 * 3600 } // safety clamp: 60s .. 24h if ttl < 60 { ttl = 60 } if ttl > 24*3600 { ttl = 24 * 3600 } workers := opts.Workers if workers <= 0 { workers = 80 } // safety clamp: 1..500 if workers < 1 { workers = 1 } if workers > 500 { workers = 500 } domainCache := loadDomainCacheState(opts.CachePath, logf) ptrCache := loadJSONMap(opts.PtrCachePath) now := int(time.Now().Unix()) cacheSourceForHost := func(host string) domainCacheSource { switch cfg.Mode { case DNSModeSmartDNS: return domainCacheSourceWildcard case DNSModeHybridWildcard: if wildcards.match(host) { return domainCacheSourceWildcard } } return domainCacheSourceDirect } if logf != nil { logf("resolver start: domains=%d ttl=%ds workers=%d", len(domains), ttl, workers) } start := time.Now() fresh := map[string][]string{} var toResolve []string for _, d := range domains { source := cacheSourceForHost(d) if ips, ok := domainCache.get(d, source, now, ttl); ok { fresh[d] = ips if logf != nil { logf("cache hit[%s]: %s -> %v", source, d, ips) } continue } toResolve = append(toResolve, d) } resolved := map[string][]string{} for k, v := range fresh { resolved[k] = v } if logf != nil { logf("resolve: domains=%d cache_hits=%d to_resolve=%d", len(domains), len(fresh), len(toResolve)) } dnsStats := dnsMetrics{} if len(toResolve) > 0 { type job struct { host string } jobs := make(chan job, len(toResolve)) results := make(chan struct { host string ips []string stats dnsMetrics }, len(toResolve)) for i := 0; i < workers; i++ { go func() { for j := range jobs { ips, stats := resolveHostGo(j.host, cfg, metaSpecial, wildcards, logf) results <- struct { host string ips []string stats dnsMetrics }{j.host, ips, stats} } }() } for _, h := range toResolve { jobs <- job{host: h} } close(jobs) for i := 0; i < len(toResolve); i++ { r := <-results dnsStats.merge(r.stats) hostErrors := r.stats.totalErrors() if hostErrors > 0 && logf != nil { logf("resolve errors for %s: total=%d nxdomain=%d timeout=%d temporary=%d other=%d", r.host, hostErrors, r.stats.NXDomain, r.stats.Timeout, r.stats.Temporary, r.stats.Other) } if len(r.ips) > 0 { resolved[r.host] = r.ips source := cacheSourceForHost(r.host) domainCache.set(r.host, source, r.ips, now) if logf != nil { logf("%s -> %v", r.host, r.ips) } } else if logf != nil { logf("%s: no IPs", r.host) } } } staticEntries, staticSkipped := parseStaticEntriesGo(staticLines, logf) staticLabels, ptrLookups, ptrErrors := resolveStaticLabels(staticEntries, cfg, ptrCache, ttl, logf) ipSetAll := map[string]struct{}{} ipSetDirect := map[string]struct{}{} ipSetWildcard := map[string]struct{}{} ipMapAll := map[string]map[string]struct{}{} ipMapDirect := map[string]map[string]struct{}{} ipMapWildcard := map[string]map[string]struct{}{} add := func(set map[string]struct{}, labels map[string]map[string]struct{}, ip, label string) { if ip == "" { return } set[ip] = struct{}{} m := labels[ip] if m == nil { m = map[string]struct{}{} labels[ip] = m } m[label] = struct{}{} } isWildcardHost := func(host string) bool { switch cfg.Mode { case DNSModeSmartDNS: return true case DNSModeHybridWildcard: return wildcards.match(host) default: return false } } for host, ips := range resolved { wildcardHost := isWildcardHost(host) for _, ip := range ips { add(ipSetAll, ipMapAll, ip, host) if wildcardHost { add(ipSetWildcard, ipMapWildcard, ip, host) } else { add(ipSetDirect, ipMapDirect, ip, host) } } } for ipEntry, labels := range staticLabels { for _, lbl := range labels { add(ipSetAll, ipMapAll, ipEntry, lbl) // Static entries are explicit operator rules; keep them in direct set. add(ipSetDirect, ipMapDirect, ipEntry, lbl) } } appendMapPairs := func(dst *[][2]string, labelsByIP map[string]map[string]struct{}) { for ip := range labelsByIP { labels := labelsByIP[ip] for lbl := range labels { *dst = append(*dst, [2]string{ip, lbl}) } } sort.Slice(*dst, func(i, j int) bool { if (*dst)[i][0] == (*dst)[j][0] { return (*dst)[i][1] < (*dst)[j][1] } return (*dst)[i][0] < (*dst)[j][0] }) } appendIPs := func(dst *[]string, set map[string]struct{}) { for ip := range set { *dst = append(*dst, ip) } sort.Strings(*dst) } appendMapPairs(&res.IPMap, ipMapAll) appendMapPairs(&res.DirectIPMap, ipMapDirect) appendMapPairs(&res.WildcardIPMap, ipMapWildcard) appendIPs(&res.IPs, ipSetAll) appendIPs(&res.DirectIPs, ipSetDirect) appendIPs(&res.WildcardIPs, ipSetWildcard) res.DomainCache = domainCache.toMap() res.PtrCache = ptrCache 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", len(domains), len(fresh), len(resolved)-len(fresh), len(domains)-len(resolved), len(staticEntries), staticSkipped, len(res.IPs), len(res.DirectIPs), len(res.WildcardIPs), ptrLookups, ptrErrors, dnsStats.Attempts, dnsStats.OK, dnsStats.NXDomain, dnsStats.Timeout, dnsStats.Temporary, dnsStats.Other, dnsErrors, time.Since(start).Milliseconds(), ) if perUpstream := dnsStats.formatPerUpstream(); perUpstream != "" { logf("resolve dns upstreams: %s", perUpstream) } } return res, nil } // --------------------------------------------------------------------- // DNS resolve helpers // --------------------------------------------------------------------- func resolveHostGo(host string, cfg dnsConfig, metaSpecial []string, wildcards wildcardMatcher, logf func(string, ...any)) ([]string, dnsMetrics) { useMeta := false for _, m := range metaSpecial { if host == m { useMeta = true break } } dnsList := cfg.Default if useMeta { dnsList = cfg.Meta } switch cfg.Mode { case DNSModeSmartDNS: if cfg.SmartDNS != "" { dnsList = []string{cfg.SmartDNS} } case DNSModeHybridWildcard: if cfg.SmartDNS != "" && wildcards.match(host) { dnsList = []string{cfg.SmartDNS} } } ips, stats := digA(host, dnsList, 3*time.Second, logf) out := []string{} seen := map[string]struct{}{} for _, ip := range ips { if isPrivateIPv4(ip) { continue } if _, ok := seen[ip]; !ok { seen[ip] = struct{}{} out = append(out, ip) } } return out, stats } // --------------------------------------------------------------------- // EN: `digA` contains core logic for dig a. // 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 { server, port := splitDNS(entry) if server == "" { continue } if port == "" { port = "53" } addr := net.JoinHostPort(server, port) r := &net.Resolver{ PreferGo: true, Dial: func(ctx context.Context, network, address string) (net.Conn, error) { d := net.Dialer{} return d.DialContext(ctx, "udp", addr) }, } ctx, cancel := context.WithTimeout(context.Background(), timeout) records, err := r.LookupHost(ctx, host) cancel() if err != nil { kind := classifyDNSError(err) stats.addError(addr, kind) if logf != nil { logf("dns warn %s via %s: kind=%s err=%v", host, addr, kind, err) } continue } stats.addSuccess(addr) for _, ip := range records { if isPrivateIPv4(ip) { continue } ips = append(ips, ip) } } return uniqueStrings(ips), stats } func classifyDNSError(err error) dnsErrorKind { if err == nil { return dnsErrorOther } var dnsErr *net.DNSError if errors.As(err, &dnsErr) { if dnsErr.IsNotFound { return dnsErrorNXDomain } if dnsErr.IsTimeout { return dnsErrorTimeout } if dnsErr.IsTemporary { return dnsErrorTemporary } } msg := strings.ToLower(err.Error()) switch { case strings.Contains(msg, "no such host"), strings.Contains(msg, "nxdomain"): return dnsErrorNXDomain case strings.Contains(msg, "i/o timeout"), strings.Contains(msg, "timeout"): return dnsErrorTimeout case strings.Contains(msg, "temporary"): return dnsErrorTemporary default: return dnsErrorOther } } // --------------------------------------------------------------------- // EN: `splitDNS` splits dns into structured parts. // RU: `splitDNS` - разделяет dns на структурированные части. // --------------------------------------------------------------------- func splitDNS(dns string) (string, string) { if strings.Contains(dns, "#") { parts := strings.SplitN(dns, "#", 2) host := strings.TrimSpace(parts[0]) port := strings.TrimSpace(parts[1]) if host == "" { host = "127.0.0.1" } if port == "" { port = "53" } return host, port } return strings.TrimSpace(dns), "" } // --------------------------------------------------------------------- // static entries + PTR labels // --------------------------------------------------------------------- func parseStaticEntriesGo(lines []string, logf func(string, ...any)) (entries [][3]string, skipped int) { for _, ln := range lines { s := strings.TrimSpace(ln) if s == "" || strings.HasPrefix(s, "#") { continue } comment := "" if idx := strings.Index(s, "#"); idx >= 0 { comment = strings.TrimSpace(s[idx+1:]) s = strings.TrimSpace(s[:idx]) } if s == "" || isPrivateIPv4(s) { continue } // validate ip/prefix rawBase := strings.SplitN(s, "/", 2)[0] if strings.Contains(s, "/") { if _, err := netip.ParsePrefix(s); err != nil { skipped++ if logf != nil { logf("static skip invalid prefix %q: %v", s, err) } continue } } else { if _, err := netip.ParseAddr(rawBase); err != nil { skipped++ if logf != nil { logf("static skip invalid ip %q: %v", s, err) } continue } } entries = append(entries, [3]string{s, rawBase, comment}) } return entries, skipped } // --------------------------------------------------------------------- // EN: `resolveStaticLabels` resolves static labels into concrete values. // RU: `resolveStaticLabels` - резолвит static labels в конкретные значения. // --------------------------------------------------------------------- func resolveStaticLabels(entries [][3]string, cfg dnsConfig, ptrCache map[string]any, ttl int, logf func(string, ...any)) (map[string][]string, int, int) { now := int(time.Now().Unix()) result := map[string][]string{} ptrLookups := 0 ptrErrors := 0 dnsForPtr := "" if len(cfg.Default) > 0 { dnsForPtr = cfg.Default[0] } else { dnsForPtr = defaultDNS1 } for _, e := range entries { ipEntry, baseIP, comment := e[0], e[1], e[2] var labels []string if comment != "" { labels = append(labels, "*"+comment) } if comment == "" { if cached, ok := ptrCache[baseIP].(map[string]any); ok { names, _ := cached["names"].([]any) last, _ := cached["last_resolved"].(float64) if len(names) > 0 && last > 0 && now-int(last) <= ttl { for _, n := range names { if s, ok := n.(string); ok && s != "" { labels = append(labels, "*"+s) } } } } if len(labels) == 0 { ptrLookups++ names, err := digPTR(baseIP, dnsForPtr, 3*time.Second, logf) if err != nil { ptrErrors++ } if len(names) > 0 { ptrCache[baseIP] = map[string]any{"names": names, "last_resolved": now} for _, n := range names { labels = append(labels, "*"+n) } } } } if len(labels) == 0 { labels = []string{"*[STATIC-IP]"} } result[ipEntry] = labels if logf != nil { logf("static %s -> %v", ipEntry, labels) } } return result, ptrLookups, ptrErrors } // --------------------------------------------------------------------- // DNS config + cache helpers // --------------------------------------------------------------------- type domainCacheSource string const ( domainCacheSourceDirect domainCacheSource = "direct" domainCacheSourceWildcard domainCacheSource = "wildcard" ) type domainCacheEntry struct { IPs []string `json:"ips"` LastResolved int `json:"last_resolved"` } type domainCacheRecord struct { Direct *domainCacheEntry `json:"direct,omitempty"` Wildcard *domainCacheEntry `json:"wildcard,omitempty"` } type domainCacheState struct { Version int `json:"version"` Domains map[string]domainCacheRecord `json:"domains"` } func newDomainCacheState() domainCacheState { return domainCacheState{ Version: 2, Domains: map[string]domainCacheRecord{}, } } func normalizeCacheIPs(raw []string) []string { seen := map[string]struct{}{} out := make([]string, 0, len(raw)) for _, ip := range raw { ip = strings.TrimSpace(ip) if ip == "" || isPrivateIPv4(ip) { continue } if _, ok := seen[ip]; ok { continue } seen[ip] = struct{}{} out = append(out, ip) } sort.Strings(out) return out } func parseAnyStringSlice(raw any) []string { switch v := raw.(type) { case []string: return append([]string(nil), v...) case []any: out := make([]string, 0, len(v)) for _, x := range v { if s, ok := x.(string); ok { out = append(out, s) } } return out default: return nil } } func parseAnyInt(raw any) (int, bool) { switch v := raw.(type) { case int: return v, true case int64: return int(v), true case float64: return int(v), true case json.Number: n, err := v.Int64() if err != nil { return 0, false } return int(n), true default: return 0, false } } func parseLegacyDomainCacheEntry(raw any) (domainCacheEntry, bool) { m, ok := raw.(map[string]any) if !ok { return domainCacheEntry{}, false } ips := normalizeCacheIPs(parseAnyStringSlice(m["ips"])) if len(ips) == 0 { return domainCacheEntry{}, false } ts, ok := parseAnyInt(m["last_resolved"]) if !ok || ts <= 0 { return domainCacheEntry{}, false } return domainCacheEntry{IPs: ips, LastResolved: ts}, true } func loadDomainCacheState(path string, logf func(string, ...any)) domainCacheState { data, err := os.ReadFile(path) if err != nil || len(data) == 0 { return newDomainCacheState() } var st domainCacheState if err := json.Unmarshal(data, &st); err == nil && st.Domains != nil { if st.Version <= 0 { st.Version = 2 } normalized := newDomainCacheState() for host, rec := range st.Domains { host = strings.TrimSpace(strings.ToLower(host)) if host == "" { 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} } } if nrec.Direct != nil || nrec.Wildcard != nil { normalized.Domains[host] = nrec } } return normalized } // Legacy shape: { "domain.tld": {"ips":[...], "last_resolved":...}, ... } var legacy map[string]any if err := json.Unmarshal(data, &legacy); err != nil { if logf != nil { logf("domain-cache: invalid json at %s, ignore", path) } return newDomainCacheState() } out := newDomainCacheState() migrated := 0 for host, raw := range legacy { host = strings.TrimSpace(strings.ToLower(host)) if host == "" || host == "version" || host == "domains" { continue } entry, ok := parseLegacyDomainCacheEntry(raw) if !ok { continue } rec := out.Domains[host] rec.Direct = &entry out.Domains[host] = rec migrated++ } if logf != nil && migrated > 0 { logf("domain-cache: migrated legacy entries=%d into split cache (direct bucket)", migrated) } return out } func (s domainCacheState) get(domain string, source domainCacheSource, now, ttl int) ([]string, bool) { rec, ok := s.Domains[strings.TrimSpace(strings.ToLower(domain))] if !ok { return nil, false } var entry *domainCacheEntry switch source { case domainCacheSourceWildcard: entry = rec.Wildcard default: entry = rec.Direct } if entry == nil || entry.LastResolved <= 0 { return nil, false } if now-entry.LastResolved > ttl { return nil, false } ips := normalizeCacheIPs(entry.IPs) if len(ips) == 0 { return nil, false } return ips, true } func (s *domainCacheState) set(domain string, source domainCacheSource, ips []string, now int) { host := strings.TrimSpace(strings.ToLower(domain)) if host == "" || now <= 0 { return } norm := normalizeCacheIPs(ips) if len(norm) == 0 { return } if s.Domains == nil { s.Domains = map[string]domainCacheRecord{} } rec := s.Domains[host] entry := &domainCacheEntry{IPs: norm, LastResolved: now} switch source { case domainCacheSourceWildcard: rec.Wildcard = entry default: rec.Direct = entry } s.Domains[host] = rec } func (s domainCacheState) toMap() map[string]any { out := map[string]any{ "version": 2, "domains": map[string]any{}, } domainsAny := out["domains"].(map[string]any) hosts := make([]string, 0, len(s.Domains)) for host := range s.Domains { hosts = append(hosts, host) } sort.Strings(hosts) 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.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 len(recOut) > 0 { domainsAny[host] = recOut } } return out } func digPTR(ip, upstream string, timeout time.Duration, logf func(string, ...any)) ([]string, error) { server, port := splitDNS(upstream) if server == "" { return nil, fmt.Errorf("upstream empty") } if port == "" { port = "53" } addr := net.JoinHostPort(server, port) r := &net.Resolver{ PreferGo: true, Dial: func(ctx context.Context, network, address string) (net.Conn, error) { d := net.Dialer{} return d.DialContext(ctx, "udp", addr) }, } ctx, cancel := context.WithTimeout(context.Background(), timeout) names, err := r.LookupAddr(ctx, ip) cancel() if err != nil { if logf != nil { logf("ptr error %s via %s: %v", ip, addr, err) } return nil, err } seen := map[string]struct{}{} var out []string for _, n := range names { n = strings.TrimSuffix(strings.ToLower(strings.TrimSpace(n)), ".") if n == "" { continue } if _, ok := seen[n]; !ok { seen[n] = struct{}{} out = append(out, n) } } return out, nil } // --------------------------------------------------------------------- // EN: `loadDNSConfig` loads dns config from storage or config. // RU: `loadDNSConfig` - загружает dns config из хранилища или конфига. // --------------------------------------------------------------------- func loadDNSConfig(path string, logf func(string, ...any)) dnsConfig { cfg := dnsConfig{ Default: []string{defaultDNS1, defaultDNS2}, Meta: []string{defaultMeta1, defaultMeta2}, SmartDNS: smartDNSAddr(), Mode: DNSModeDirect, } // 1) Если форсируем SmartDNS — вообще игнорим файл и ходим только через локальный резолвер. if smartDNSForced() { addr := smartDNSAddr() cfg.Default = []string{addr} cfg.Meta = []string{addr} cfg.SmartDNS = addr cfg.Mode = DNSModeSmartDNS if logf != nil { logf("dns-config: SmartDNS forced (%s), ignore %s", addr, path) } return cfg } // 2) Иначе пытаемся прочитать dns-upstreams.conf, как и раньше. data, err := os.ReadFile(path) if err != nil { if logf != nil { logf("dns-config: use built-in defaults, can't read %s: %v", path, err) } return cfg } var def, meta []string lines := strings.Split(string(data), "\n") for _, ln := range lines { s := strings.TrimSpace(ln) if s == "" || strings.HasPrefix(s, "#") { continue } parts := strings.Fields(s) if len(parts) < 2 { continue } key := strings.ToLower(parts[0]) vals := parts[1:] switch key { case "default": for _, v := range vals { if n := normalizeDNSUpstream(v, "53"); n != "" { def = append(def, n) } } case "meta": for _, v := range vals { if n := normalizeDNSUpstream(v, "53"); n != "" { meta = append(meta, n) } } case "smartdns": if len(vals) > 0 { if n := normalizeSmartDNSAddr(vals[0]); n != "" { cfg.SmartDNS = n } } case "mode": if len(vals) > 0 { cfg.Mode = normalizeDNSResolverMode(DNSResolverMode(vals[0]), false) } } } if len(def) > 0 { cfg.Default = def } if len(meta) > 0 { cfg.Meta = meta } 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) } return cfg } // --------------------------------------------------------------------- // EN: `readLinesAllowMissing` reads lines allow missing from input data. // RU: `readLinesAllowMissing` - читает lines allow missing из входных данных. // --------------------------------------------------------------------- func readLinesAllowMissing(path string) []string { data, err := os.ReadFile(path) if err != nil { return nil } return strings.Split(strings.ReplaceAll(string(data), "\r\n", "\n"), "\n") } // --------------------------------------------------------------------- // EN: `loadJSONMap` loads json map from storage or config. // RU: `loadJSONMap` - загружает json map из хранилища или конфига. // --------------------------------------------------------------------- func loadJSONMap(path string) map[string]any { data, err := os.ReadFile(path) if err != nil { return map[string]any{} } var out map[string]any if err := json.Unmarshal(data, &out); err != nil { return map[string]any{} } return out } // --------------------------------------------------------------------- // EN: `saveJSON` saves json to persistent storage. // RU: `saveJSON` - сохраняет json в постоянное хранилище. // --------------------------------------------------------------------- func saveJSON(data any, path string) { tmp := path + ".tmp" b, err := json.MarshalIndent(data, "", " ") if err != nil { return } _ = os.WriteFile(tmp, b, 0o644) _ = os.Rename(tmp, path) } // --------------------------------------------------------------------- // EN: `uniqueStrings` contains core logic for unique strings. // RU: `uniqueStrings` - содержит основную логику для unique strings. // --------------------------------------------------------------------- func uniqueStrings(in []string) []string { seen := map[string]struct{}{} var out []string for _, v := range in { if _, ok := seen[v]; !ok { seen[v] = struct{}{} out = append(out, v) } } return out } // --------------------------------------------------------------------- // text cleanup + IP classifiers // --------------------------------------------------------------------- var reANSI = regexp.MustCompile(`\x1B\[[0-9;]*[A-Za-z]`) func stripANSI(s string) string { return reANSI.ReplaceAllString(s, "") } // --------------------------------------------------------------------- // EN: `isPrivateIPv4` checks whether private i pv4 is true. // RU: `isPrivateIPv4` - проверяет, является ли private i pv4 истинным условием. // --------------------------------------------------------------------- func isPrivateIPv4(ip string) bool { parts := strings.Split(strings.Split(ip, "/")[0], ".") if len(parts) != 4 { return true } vals := make([]int, 4) for i, p := range parts { n, err := strconv.Atoi(p) if err != nil || n < 0 || n > 255 { return true } vals[i] = n } if vals[0] == 10 || vals[0] == 127 || vals[0] == 0 { return true } if vals[0] == 192 && vals[1] == 168 { return true } if vals[0] == 172 && vals[1] >= 16 && vals[1] <= 31 { return true } return false }