package app import ( "context" "encoding/json" "fmt" "io" "net" "net/http" "os" "path/filepath" "sort" "strings" "sync" "time" ) // --------------------------------------------------------------------- // DNS settings + SmartDNS control // --------------------------------------------------------------------- // EN: DNS control-plane handlers and storage helpers. // EN: This unit keeps resolver mode, SmartDNS address, SmartDNS service control, // EN: and dns-upstreams.conf in one place for GUI and backend consistency. // RU: Обработчики DNS control-plane и helper-функции хранения. // RU: Этот модуль держит в одном месте режим резолвера, адрес SmartDNS, // RU: управление сервисом SmartDNS и dns-upstreams.conf для консистентности GUI и backend. // --------------------------------------------------------------------- // EN: `handleDNSUpstreams` is an HTTP handler for dns upstreams. // RU: `handleDNSUpstreams` - HTTP-обработчик для dns upstreams. // --------------------------------------------------------------------- func handleDNSUpstreams(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: writeJSON(w, http.StatusOK, loadDNSUpstreamsConf()) case http.MethodPost: var cfg DNSUpstreams if r.Body != nil { defer r.Body.Close() if err := json.NewDecoder(io.LimitReader(r.Body, 1<<20)).Decode(&cfg); err != nil && err != io.EOF { http.Error(w, "bad json", http.StatusBadRequest) return } } if err := saveDNSUpstreamsConf(cfg); err != nil { http.Error(w, "write error", http.StatusInternalServerError) return } writeJSON(w, http.StatusOK, map[string]any{ "ok": true, "cfg": loadDNSUpstreamsConf(), }) default: http.Error(w, "method not allowed", http.StatusMethodNotAllowed) } } func handleDNSUpstreamPool(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: items := loadDNSUpstreamPoolState() writeJSON(w, http.StatusOK, DNSUpstreamPoolState{Items: items}) case http.MethodPost: var body DNSUpstreamPoolState if r.Body != nil { defer r.Body.Close() if err := json.NewDecoder(io.LimitReader(r.Body, 1<<20)).Decode(&body); err != nil && err != io.EOF { http.Error(w, "bad json", http.StatusBadRequest) return } } if err := saveDNSUpstreamPoolState(body.Items); err != nil { http.Error(w, "write error", http.StatusInternalServerError) return } writeJSON(w, http.StatusOK, DNSUpstreamPoolState{Items: loadDNSUpstreamPoolState()}) default: http.Error(w, "method not allowed", http.StatusMethodNotAllowed) } } // --------------------------------------------------------------------- // EN: `handleDNSStatus` is an HTTP handler for dns status. // RU: `handleDNSStatus` - HTTP-обработчик для dns status. // --------------------------------------------------------------------- func handleDNSStatus(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } mode := loadDNSMode() writeJSON(w, http.StatusOK, makeDNSStatusResponse(mode)) } var dnsBenchmarkDefaultDomains = []string{ "cloudflare.com", "google.com", "telegram.org", "github.com", "youtube.com", "twitter.com", } func handleDNSBenchmark(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } var req DNSBenchmarkRequest if r.Body != nil { defer r.Body.Close() if err := json.NewDecoder(io.LimitReader(r.Body, 1<<20)).Decode(&req); err != nil && err != io.EOF { http.Error(w, "bad json", http.StatusBadRequest) return } } upstreams := normalizeBenchmarkUpstreams(req.Upstreams) if len(upstreams) == 0 { pool := loadDNSUpstreamPoolState() if len(pool) > 0 { tmp := make([]DNSBenchmarkUpstream, 0, len(pool)) for _, item := range pool { tmp = append(tmp, DNSBenchmarkUpstream{Addr: item.Addr, Enabled: item.Enabled}) } upstreams = normalizeBenchmarkUpstreams(tmp) } if len(upstreams) == 0 { cfg := loadDNSUpstreamsConf() upstreams = normalizeBenchmarkUpstreamStrings([]string{ cfg.Default1, cfg.Default2, cfg.Meta1, cfg.Meta2, }) } } if len(upstreams) == 0 { http.Error(w, "no upstreams", http.StatusBadRequest) return } domains := normalizeBenchmarkDomains(req.Domains) if len(domains) == 0 { domains = append(domains, dnsBenchmarkDefaultDomains...) } timeoutMS := req.TimeoutMS if timeoutMS <= 0 { timeoutMS = 1800 } if timeoutMS < 300 { timeoutMS = 300 } if timeoutMS > 5000 { timeoutMS = 5000 } attempts := req.Attempts if attempts <= 0 { attempts = 1 } if attempts > 3 { attempts = 3 } concurrency := req.Concurrency if concurrency <= 0 { concurrency = 6 } if concurrency < 1 { concurrency = 1 } if concurrency > 32 { concurrency = 32 } if concurrency > len(upstreams) { concurrency = len(upstreams) } results := make([]DNSBenchmarkResult, 0, len(upstreams)) var mu sync.Mutex var wg sync.WaitGroup sem := make(chan struct{}, concurrency) timeout := time.Duration(timeoutMS) * time.Millisecond for _, upstream := range upstreams { wg.Add(1) sem <- struct{}{} go func(upstream string) { defer wg.Done() defer func() { <-sem }() result := benchmarkDNSUpstream(upstream, domains, timeout, attempts) mu.Lock() results = append(results, result) mu.Unlock() }(upstream) } wg.Wait() sort.Slice(results, func(i, j int) bool { if results[i].Score == results[j].Score { if results[i].AvgMS == results[j].AvgMS { if results[i].OK == results[j].OK { return results[i].Upstream < results[j].Upstream } return results[i].OK > results[j].OK } return results[i].AvgMS < results[j].AvgMS } return results[i].Score > results[j].Score }) resp := DNSBenchmarkResponse{ Results: results, DomainsUsed: domains, TimeoutMS: timeoutMS, AttemptsPerDomain: attempts, } resp.RecommendedDefault = benchmarkTopN(results, 2, upstreams) resp.RecommendedMeta = benchmarkTopN(results, 2, upstreams) writeJSON(w, http.StatusOK, resp) } func normalizeBenchmarkUpstreams(in []DNSBenchmarkUpstream) []string { if len(in) == 0 { return nil } out := make([]string, 0, len(in)) seen := map[string]struct{}{} for _, item := range in { n := normalizeDNSUpstream(item.Addr, "53") if n == "" { continue } if _, ok := seen[n]; ok { continue } seen[n] = struct{}{} out = append(out, n) } return out } func normalizeBenchmarkUpstreamStrings(in []string) []string { out := make([]string, 0, len(in)) seen := map[string]struct{}{} for _, raw := range in { n := normalizeDNSUpstream(raw, "53") if n == "" { continue } if _, ok := seen[n]; ok { continue } seen[n] = struct{}{} out = append(out, n) } return out } func normalizeBenchmarkDomains(in []string) []string { if len(in) == 0 { return nil } out := make([]string, 0, len(in)) seen := map[string]struct{}{} for _, raw := range in { d := strings.TrimSuffix(strings.ToLower(strings.TrimSpace(raw)), ".") if d == "" || strings.HasPrefix(d, "#") { continue } if _, ok := seen[d]; ok { continue } seen[d] = struct{}{} out = append(out, d) } if len(out) > 100 { out = out[:100] } return out } func benchmarkDNSUpstream(upstream string, domains []string, timeout time.Duration, attempts int) DNSBenchmarkResult { res := DNSBenchmarkResult{Upstream: upstream} durations := make([]int, 0, len(domains)*attempts) for _, host := range domains { for i := 0; i < attempts; i++ { start := time.Now() _, err := dnsLookupAOnce(host, upstream, timeout) elapsed := int(time.Since(start).Milliseconds()) if elapsed < 1 { elapsed = 1 } res.Attempts++ if err != nil { res.Fail++ switch classifyDNSError(err) { case dnsErrorNXDomain: res.NXDomain++ case dnsErrorTimeout: res.Timeout++ case dnsErrorTemporary: res.Temporary++ default: res.Other++ } continue } res.OK++ durations = append(durations, elapsed) } } if len(durations) > 0 { sort.Ints(durations) sum := 0 for _, d := range durations { sum += d } res.AvgMS = sum / len(durations) idx := int(float64(len(durations)-1) * 0.95) if idx < 0 { idx = 0 } res.P95MS = durations[idx] } total := res.Attempts if total > 0 { okRate := float64(res.OK) / float64(total) timeoutRate := float64(res.Timeout) / float64(total) nxRate := float64(res.NXDomain) / float64(total) avg := float64(res.AvgMS) if avg <= 0 { avg = float64(timeout.Milliseconds()) } res.Score = okRate*100.0 - timeoutRate*45.0 - nxRate*12.0 - (avg / 30.0) } timeoutRate := 0.0 if res.Attempts > 0 { timeoutRate = float64(res.Timeout) / float64(res.Attempts) } switch { case res.OK == 0 || timeoutRate >= 0.15 || res.AvgMS > 400: res.Color = "red" case res.AvgMS < 200 && timeoutRate == 0: res.Color = "green" default: res.Color = "yellow" } return res } func dnsLookupAOnce(host string, upstream string, timeout time.Duration) ([]string, error) { server, port := splitDNS(upstream) if server == "" { return nil, fmt.Errorf("upstream empty") } if port == "" { port = "53" } addr := net.JoinHostPort(server, port) resolver := &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 := resolver.LookupHost(ctx, host) cancel() if err != nil { return nil, err } seen := map[string]struct{}{} out := make([]string, 0, len(records)) for _, ip := range records { if isPrivateIPv4(ip) { continue } if _, ok := seen[ip]; ok { continue } seen[ip] = struct{}{} out = append(out, ip) } if len(out) == 0 { return nil, fmt.Errorf("no public ips") } return out, nil } func benchmarkTopN(results []DNSBenchmarkResult, n int, fallback []string) []string { out := make([]string, 0, n) for _, item := range results { if item.OK <= 0 { continue } out = append(out, item.Upstream) if len(out) >= n { return out } } for _, item := range fallback { if len(out) >= n { break } dup := false for _, e := range out { if e == item { dup = true break } } if !dup { out = append(out, item) } } return out } // --------------------------------------------------------------------- // EN: `handleDNSModeSet` is an HTTP handler for dns mode set. // RU: `handleDNSModeSet` - HTTP-обработчик для dns mode set. // --------------------------------------------------------------------- func handleDNSModeSet(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } var req DNSModeRequest if r.Body != nil { defer r.Body.Close() if err := json.NewDecoder(io.LimitReader(r.Body, 1<<20)).Decode(&req); err != nil && err != io.EOF { http.Error(w, "bad json", http.StatusBadRequest) return } } mode := loadDNSMode() mode.Mode = normalizeDNSResolverMode(req.Mode, req.ViaSmartDNS) mode.ViaSmartDNS = mode.Mode != DNSModeDirect if strings.TrimSpace(req.SmartDNSAddr) != "" { mode.SmartDNSAddr = req.SmartDNSAddr } if err := saveDNSMode(mode); err != nil { http.Error(w, "write error", http.StatusInternalServerError) return } mode = loadDNSMode() writeJSON(w, http.StatusOK, makeDNSStatusResponse(mode)) } // --------------------------------------------------------------------- // EN: `handleDNSSmartdnsService` is an HTTP handler for dns smartdns service. // RU: `handleDNSSmartdnsService` - HTTP-обработчик для dns smartdns service. // --------------------------------------------------------------------- func handleDNSSmartdnsService(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } var body struct { Action string `json:"action"` } if r.Body != nil { defer r.Body.Close() _ = json.NewDecoder(io.LimitReader(r.Body, 1<<20)).Decode(&body) } action := strings.ToLower(strings.TrimSpace(body.Action)) if action == "" { action = "restart" } switch action { case "start", "stop", "restart": default: http.Error(w, "unknown action", http.StatusBadRequest) return } res := runSmartdnsUnitAction(action) mode := loadDNSMode() rt := smartDNSRuntimeSnapshot() writeJSON(w, http.StatusOK, map[string]any{ "ok": res.OK, "message": res.Message, "exitCode": res.ExitCode, "stdout": res.Stdout, "stderr": res.Stderr, "unit_state": smartdnsUnitState(), "via_smartdns": mode.ViaSmartDNS, "smartdns_addr": mode.SmartDNSAddr, "mode": mode.Mode, "runtime_nftset": rt.Enabled, "wildcard_source": rt.WildcardSource, }) } func makeDNSStatusResponse(mode DNSMode) DNSStatusResponse { rt := smartDNSRuntimeSnapshot() resp := DNSStatusResponse{ ViaSmartDNS: mode.ViaSmartDNS, SmartDNSAddr: mode.SmartDNSAddr, Mode: mode.Mode, UnitState: smartdnsUnitState(), RuntimeNftset: rt.Enabled, WildcardSource: rt.WildcardSource, RuntimeCfgPath: rt.ConfigPath, } if rt.Message != "" { resp.RuntimeCfgError = rt.Message } return resp } // --------------------------------------------------------------------- // EN: `handleSmartdnsService` is an HTTP handler for smartdns service. // RU: `handleSmartdnsService` - HTTP-обработчик для smartdns service. // --------------------------------------------------------------------- func handleSmartdnsService(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: writeJSON(w, http.StatusOK, map[string]string{"state": smartdnsUnitState()}) case http.MethodPost: var body struct { Action string `json:"action"` } if r.Body != nil { defer r.Body.Close() _ = json.NewDecoder(io.LimitReader(r.Body, 1<<20)).Decode(&body) } action := strings.ToLower(strings.TrimSpace(body.Action)) if action == "" { action = "restart" } switch action { case "start", "stop", "restart": default: http.Error(w, "unknown action", http.StatusBadRequest) return } writeJSON(w, http.StatusOK, runSmartdnsUnitAction(action)) default: http.Error(w, "method not allowed", http.StatusMethodNotAllowed) } } // --------------------------------------------------------------------- // smartdns runtime accelerator state // --------------------------------------------------------------------- func handleSmartdnsRuntime(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: writeJSON(w, http.StatusOK, smartDNSRuntimeSnapshot()) case http.MethodPost: var body SmartDNSRuntimeRequest if r.Body != nil { defer r.Body.Close() if err := json.NewDecoder(io.LimitReader(r.Body, 1<<20)).Decode(&body); err != nil && err != io.EOF { http.Error(w, "bad json", http.StatusBadRequest) return } } if body.Enabled == nil { http.Error(w, "enabled is required", http.StatusBadRequest) return } prev := loadSmartDNSRuntimeState(nil) next := prev next.Enabled = *body.Enabled if err := saveSmartDNSRuntimeState(next); err != nil { http.Error(w, "runtime state write error", http.StatusInternalServerError) return } changed, err := applySmartDNSRuntimeConfig(next.Enabled) if err != nil { _ = saveSmartDNSRuntimeState(prev) http.Error(w, "runtime config apply error: "+err.Error(), http.StatusInternalServerError) return } restart := true if body.Restart != nil { restart = *body.Restart } restarted := false msg := "" if restart && smartdnsUnitState() == "active" { res := runSmartdnsUnitAction("restart") restarted = res.OK if !res.OK { msg = "runtime config changed, but smartdns restart failed: " + strings.TrimSpace(res.Message) } } if msg == "" { msg = fmt.Sprintf("smartdns runtime set: enabled=%t changed=%t restarted=%t", next.Enabled, changed, restarted) } appendTraceLineTo(smartdnsLogPath, "smartdns", msg) resp := smartDNSRuntimeSnapshot() resp.Changed = changed resp.Restarted = restarted resp.Message = msg writeJSON(w, http.StatusOK, resp) default: http.Error(w, "method not allowed", http.StatusMethodNotAllowed) } } // --------------------------------------------------------------------- // EN: `handleSmartdnsPrewarm` forces DNS lookups for wildcard domains via SmartDNS. // EN: This warms agvpn_dyn4 in realtime through SmartDNS nftset runtime integration. // RU: `handleSmartdnsPrewarm` принудительно резолвит wildcard-домены через SmartDNS. // RU: Это прогревает agvpn_dyn4 в realtime через runtime-интеграцию SmartDNS nftset. // --------------------------------------------------------------------- func handleSmartdnsPrewarm(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } var body struct { Limit int `json:"limit"` Workers int `json:"workers"` TimeoutMS int `json:"timeout_ms"` AggressiveSubs bool `json:"aggressive_subs"` } if r.Body != nil { defer r.Body.Close() _ = json.NewDecoder(io.LimitReader(r.Body, 1<<20)).Decode(&body) } writeJSON(w, http.StatusOK, runSmartdnsPrewarm(body.Limit, body.Workers, body.TimeoutMS, body.AggressiveSubs)) } func runSmartdnsPrewarm(limit, workers, timeoutMS int, aggressiveSubs bool) cmdResult { mode := loadDNSMode() runtimeEnabled := smartDNSRuntimeEnabled() source := "resolver" if runtimeEnabled { source = "smartdns_runtime" } smartdnsAddr := normalizeSmartDNSAddr(mode.SmartDNSAddr) if smartdnsAddr == "" { smartdnsAddr = resolveDefaultSmartDNSAddr() } if smartdnsAddr == "" { return cmdResult{OK: false, Message: "SmartDNS address is empty"} } wildcards := loadSmartDNSWildcardDomains(nil) if len(wildcards) == 0 { msg := "prewarm skipped: wildcard list is empty" appendTraceLineTo(smartdnsLogPath, "smartdns", msg) return cmdResult{OK: true, Message: msg} } aggressive := aggressiveSubs || prewarmAggressiveFromEnv() // Default prewarm is wildcard-only (no subs fan-out). subs := []string{} subsPerBaseLimit := 0 if aggressive { subs = loadList(domainDir + "/subs.txt") subsPerBaseLimit = envInt("RESOLVE_SUBS_PER_BASE_LIMIT", 0) if subsPerBaseLimit < 0 { subsPerBaseLimit = 0 } } domainSet := make(map[string]struct{}, len(wildcards)*(len(subs)+1)) for _, d := range wildcards { d = strings.TrimSpace(d) if d == "" { continue } domainSet[d] = struct{}{} if aggressive && !isGoogleLike(d) { maxSubs := len(subs) if subsPerBaseLimit > 0 && subsPerBaseLimit < maxSubs { maxSubs = subsPerBaseLimit } for i := 0; i < maxSubs; i++ { domainSet[subs[i]+"."+d] = struct{}{} } } } domains := make([]string, 0, len(domainSet)) for d := range domainSet { domains = append(domains, d) } sort.Strings(domains) if limit > 0 && len(domains) > limit { domains = domains[:limit] } if len(domains) == 0 { msg := "prewarm skipped: expanded wildcard list is empty" appendTraceLineTo(smartdnsLogPath, "smartdns", msg) return cmdResult{OK: true, Message: msg} } if workers <= 0 { workers = envInt("SMARTDNS_PREWARM_WORKERS", 24) } if workers < 1 { workers = 1 } if workers > 200 { workers = 200 } if timeoutMS <= 0 { timeoutMS = envInt("SMARTDNS_PREWARM_TIMEOUT_MS", 1800) } if timeoutMS < 200 { timeoutMS = 200 } if timeoutMS > 15000 { timeoutMS = 15000 } timeout := time.Duration(timeoutMS) * time.Millisecond // Ensure runtime set exists before prewarm queries hit SmartDNS nftset hook. _, _, _, _ = runCommandTimeout(5*time.Second, "nft", "add", "table", "inet", "agvpn") _, _, _, _ = runCommandTimeout(5*time.Second, "nft", "add", "set", "inet", "agvpn", "agvpn_dyn4", "{", "type", "ipv4_addr", ";", "flags", "interval", ";", "}") appendTraceLineTo( smartdnsLogPath, "smartdns", fmt.Sprintf( "prewarm start: mode=%s source=%s runtime_nftset=%t smartdns=%s wildcard_domains=%d expanded=%d aggressive_subs=%t workers=%d timeout_ms=%d", mode.Mode, source, runtimeEnabled, smartdnsAddr, len(wildcards), len(domains), aggressive, workers, timeoutMS, ), ) type prewarmItem struct { host string ips []string stats dnsMetrics } jobs := make(chan string, len(domains)) results := make(chan prewarmItem, len(domains)) for i := 0; i < workers; i++ { go func() { for host := range jobs { ips, stats := digA(host, []string{smartdnsAddr}, timeout, nil) results <- prewarmItem{host: host, ips: ips, stats: stats} } }() } for _, host := range domains { jobs <- host } close(jobs) resolvedHosts := 0 totalIPs := 0 errorHosts := 0 stats := dnsMetrics{} resolvedIPSet := map[string]struct{}{} loggedHosts := 0 const maxHostsLog = 200 for i := 0; i < len(domains); i++ { item := <-results stats.merge(item.stats) if item.stats.totalErrors() > 0 { errorHosts++ } if len(item.ips) == 0 { continue } resolvedHosts++ totalIPs += len(item.ips) for _, ip := range item.ips { if strings.TrimSpace(ip) != "" { resolvedIPSet[ip] = struct{}{} } } if loggedHosts < maxHostsLog { appendTraceLineTo(smartdnsLogPath, "smartdns", fmt.Sprintf("prewarm add: %s -> %s", item.host, strings.Join(item.ips, ", "))) loggedHosts++ } } manualAdded := 0 totalDyn := 0 totalDynText := "n/a" if !runtimeEnabled { existing, _ := readNftSetElements("agvpn_dyn4") mergedSet := make(map[string]struct{}, len(existing)+len(resolvedIPSet)) for _, ip := range existing { if strings.TrimSpace(ip) != "" { mergedSet[ip] = struct{}{} } } for ip := range resolvedIPSet { if _, ok := mergedSet[ip]; !ok { manualAdded++ } mergedSet[ip] = struct{}{} } merged := make([]string, 0, len(mergedSet)) for ip := range mergedSet { merged = append(merged, ip) } totalDyn = len(merged) totalDynText = fmt.Sprintf("%d", totalDyn) ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) defer cancel() if err := nftUpdateSetIPsSmart(ctx, "agvpn_dyn4", merged, nil); err != nil { msg := fmt.Sprintf("prewarm manual apply failed: %v", err) appendTraceLineTo(smartdnsLogPath, "smartdns", msg) return cmdResult{OK: false, Message: msg} } appendTraceLineTo( smartdnsLogPath, "smartdns", fmt.Sprintf("prewarm manual merge: existing=%d resolved=%d added=%d total_dyn=%d", len(existing), len(resolvedIPSet), manualAdded, totalDyn), ) } if len(domains) > loggedHosts { appendTraceLineTo( smartdnsLogPath, "smartdns", fmt.Sprintf( "prewarm add: trace truncated, omitted=%d hosts (full wildcard map: %s)", len(domains)-loggedHosts, lastIPsMapDyn, ), ) } msg := fmt.Sprintf( "prewarm done: source=%s expanded=%d resolved=%d total_ips=%d error_hosts=%d dns_attempts=%d dns_ok=%d dns_errors=%d manual_added=%d dyn_total=%s", source, len(domains), resolvedHosts, totalIPs, errorHosts, stats.Attempts, stats.OK, stats.totalErrors(), manualAdded, totalDynText, ) appendTraceLineTo(smartdnsLogPath, "smartdns", msg) if perUpstream := stats.formatPerUpstream(); perUpstream != "" { appendTraceLineTo(smartdnsLogPath, "smartdns", "prewarm dns upstreams: "+perUpstream) } return cmdResult{ OK: true, Message: msg, ExitCode: resolvedHosts, } } func prewarmAggressiveFromEnv() bool { switch strings.ToLower(strings.TrimSpace(os.Getenv("SMARTDNS_PREWARM_AGGRESSIVE"))) { case "1", "true", "yes", "on": return true default: return false } } func loadDNSUpstreamsConfFile() DNSUpstreams { cfg := DNSUpstreams{ Default1: defaultDNS1, Default2: defaultDNS2, Meta1: defaultMeta1, Meta2: defaultMeta2, } data, err := os.ReadFile(dnsUpstreamsConf) if err != nil { return cfg } for _, ln := range strings.Split(string(data), "\n") { 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": if len(vals) > 0 { cfg.Default1 = normalizeDNSUpstream(vals[0], "53") } if len(vals) > 1 { cfg.Default2 = normalizeDNSUpstream(vals[1], "53") } case "meta": if len(vals) > 0 { cfg.Meta1 = normalizeDNSUpstream(vals[0], "53") } if len(vals) > 1 { cfg.Meta2 = normalizeDNSUpstream(vals[1], "53") } } } if cfg.Default1 == "" { cfg.Default1 = defaultDNS1 } if cfg.Default2 == "" { cfg.Default2 = defaultDNS2 } if cfg.Meta1 == "" { cfg.Meta1 = defaultMeta1 } if cfg.Meta2 == "" { cfg.Meta2 = defaultMeta2 } return cfg } func normalizeDNSUpstreamPoolItems(items []DNSUpstreamPoolItem) []DNSUpstreamPoolItem { out := make([]DNSUpstreamPoolItem, 0, len(items)) seen := map[string]struct{}{} for _, item := range items { addr := normalizeDNSUpstream(item.Addr, "53") if addr == "" { continue } if _, ok := seen[addr]; ok { continue } seen[addr] = struct{}{} out = append(out, DNSUpstreamPoolItem{ Addr: addr, Enabled: item.Enabled, }) } return out } func dnsUpstreamPoolFromLegacy(cfg DNSUpstreams) []DNSUpstreamPoolItem { raw := []string{cfg.Default1, cfg.Default2, cfg.Meta1, cfg.Meta2} out := make([]DNSUpstreamPoolItem, 0, len(raw)) for _, item := range raw { n := normalizeDNSUpstream(item, "53") if n == "" { continue } out = append(out, DNSUpstreamPoolItem{Addr: n, Enabled: true}) } return normalizeDNSUpstreamPoolItems(out) } func dnsUpstreamPoolToLegacy(items []DNSUpstreamPoolItem) DNSUpstreams { enabled := make([]string, 0, len(items)) all := make([]string, 0, len(items)) for _, item := range items { n := normalizeDNSUpstream(item.Addr, "53") if n == "" { continue } all = append(all, n) if item.Enabled { enabled = append(enabled, n) } } list := enabled if len(list) == 0 { list = all } if len(list) == 0 { list = []string{defaultDNS1, defaultDNS2, defaultMeta1, defaultMeta2} } pick := func(idx int, fallback string) string { if len(list) == 0 { return fallback } if idx < len(list) { return list[idx] } return list[idx%len(list)] } return DNSUpstreams{ Default1: pick(0, defaultDNS1), Default2: pick(1, defaultDNS2), Meta1: pick(2, defaultMeta1), Meta2: pick(3, defaultMeta2), } } func saveDNSUpstreamPoolFile(items []DNSUpstreamPoolItem) error { state := DNSUpstreamPoolState{Items: normalizeDNSUpstreamPoolItems(items)} if err := os.MkdirAll(filepath.Dir(dnsUpstreamPool), 0o755); err != nil { return err } tmp := dnsUpstreamPool + ".tmp" b, err := json.MarshalIndent(state, "", " ") if err != nil { return err } if err := os.WriteFile(tmp, b, 0o644); err != nil { return err } return os.Rename(tmp, dnsUpstreamPool) } func loadDNSUpstreamPoolState() []DNSUpstreamPoolItem { data, err := os.ReadFile(dnsUpstreamPool) if err == nil { var st DNSUpstreamPoolState if json.Unmarshal(data, &st) == nil { items := normalizeDNSUpstreamPoolItems(st.Items) if len(items) > 0 { return items } } } legacy := loadDNSUpstreamsConfFile() items := dnsUpstreamPoolFromLegacy(legacy) if len(items) > 0 { _ = saveDNSUpstreamPoolFile(items) } return items } func saveDNSUpstreamPoolState(items []DNSUpstreamPoolItem) error { items = normalizeDNSUpstreamPoolItems(items) if len(items) == 0 { items = dnsUpstreamPoolFromLegacy(loadDNSUpstreamsConfFile()) } if err := saveDNSUpstreamPoolFile(items); err != nil { return err } return saveDNSUpstreamsConfFile(dnsUpstreamPoolToLegacy(items)) } func loadEnabledDNSUpstreamPool() []string { items := loadDNSUpstreamPoolState() out := make([]string, 0, len(items)) for _, item := range items { if !item.Enabled { continue } n := normalizeDNSUpstream(item.Addr, "53") if n == "" { continue } out = append(out, n) } return uniqueStrings(out) } func saveDNSUpstreamsConfFile(cfg DNSUpstreams) error { cfg.Default1 = normalizeDNSUpstream(cfg.Default1, "53") cfg.Default2 = normalizeDNSUpstream(cfg.Default2, "53") cfg.Meta1 = normalizeDNSUpstream(cfg.Meta1, "53") cfg.Meta2 = normalizeDNSUpstream(cfg.Meta2, "53") if cfg.Default1 == "" { cfg.Default1 = defaultDNS1 } if cfg.Default2 == "" { cfg.Default2 = defaultDNS2 } if cfg.Meta1 == "" { cfg.Meta1 = defaultMeta1 } if cfg.Meta2 == "" { cfg.Meta2 = defaultMeta2 } content := fmt.Sprintf( "default %s %s\nmeta %s %s\n", cfg.Default1, cfg.Default2, cfg.Meta1, cfg.Meta2, ) if err := os.MkdirAll(filepath.Dir(dnsUpstreamsConf), 0o755); err != nil { return err } tmp := dnsUpstreamsConf + ".tmp" if err := os.WriteFile(tmp, []byte(content), 0o644); err != nil { return err } if err := os.Rename(tmp, dnsUpstreamsConf); err != nil { return err } // Legacy JSON mirror for backward compatibility with older UI/runtime bits. _ = os.MkdirAll(stateDir, 0o755) if b, err := json.MarshalIndent(cfg, "", " "); err == nil { _ = os.WriteFile(dnsUpstreamsPath, b, 0o644) } return nil } // --------------------------------------------------------------------- // EN: `loadDNSUpstreamsConf` loads dns upstreams conf from storage or config. // RU: `loadDNSUpstreamsConf` - загружает dns upstreams conf из хранилища или конфига. // --------------------------------------------------------------------- func loadDNSUpstreamsConf() DNSUpstreams { pool := loadDNSUpstreamPoolState() if len(pool) > 0 { return dnsUpstreamPoolToLegacy(pool) } return loadDNSUpstreamsConfFile() } // --------------------------------------------------------------------- // EN: `saveDNSUpstreamsConf` saves dns upstreams conf to persistent storage. // RU: `saveDNSUpstreamsConf` - сохраняет dns upstreams conf в постоянное хранилище. // --------------------------------------------------------------------- func saveDNSUpstreamsConf(cfg DNSUpstreams) error { if err := saveDNSUpstreamsConfFile(cfg); err != nil { return err } return saveDNSUpstreamPoolFile(dnsUpstreamPoolFromLegacy(cfg)) } // --------------------------------------------------------------------- // EN: `loadDNSMode` loads dns mode from storage or config. // RU: `loadDNSMode` - загружает dns mode из хранилища или конфига. // --------------------------------------------------------------------- func loadDNSMode() DNSMode { mode := DNSMode{ ViaSmartDNS: false, SmartDNSAddr: resolveDefaultSmartDNSAddr(), Mode: DNSModeDirect, } needPersist := false data, err := os.ReadFile(dnsModePath) switch { case err == nil: var stored DNSMode if err := json.Unmarshal(data, &stored); err == nil { mode.Mode = normalizeDNSResolverMode(stored.Mode, stored.ViaSmartDNS) mode.ViaSmartDNS = mode.Mode != DNSModeDirect if strings.TrimSpace(string(stored.Mode)) == "" || stored.ViaSmartDNS != mode.ViaSmartDNS { needPersist = true } if addr := normalizeSmartDNSAddr(stored.SmartDNSAddr); addr != "" { mode.SmartDNSAddr = addr } else { needPersist = true } } else { needPersist = true } case os.IsNotExist(err): needPersist = true } if mode.SmartDNSAddr == "" { mode.SmartDNSAddr = smartDNSDefaultAddr needPersist = true } mode.Mode = normalizeDNSResolverMode(mode.Mode, mode.ViaSmartDNS) mode.ViaSmartDNS = mode.Mode != DNSModeDirect if needPersist { _ = saveDNSMode(mode) } return mode } // --------------------------------------------------------------------- // EN: `saveDNSMode` saves dns mode to persistent storage. // RU: `saveDNSMode` - сохраняет dns mode в постоянное хранилище. // --------------------------------------------------------------------- func saveDNSMode(mode DNSMode) error { mode.Mode = normalizeDNSResolverMode(mode.Mode, mode.ViaSmartDNS) mode.ViaSmartDNS = mode.Mode != DNSModeDirect mode.SmartDNSAddr = normalizeSmartDNSAddr(mode.SmartDNSAddr) if mode.SmartDNSAddr == "" { mode.SmartDNSAddr = resolveDefaultSmartDNSAddr() } if err := os.MkdirAll(stateDir, 0o755); err != nil { return err } tmp := dnsModePath + ".tmp" b, err := json.MarshalIndent(mode, "", " ") if err != nil { return err } if err := os.WriteFile(tmp, b, 0o644); err != nil { return err } return os.Rename(tmp, dnsModePath) } // --------------------------------------------------------------------- // EN: `normalizeDNSResolverMode` normalizes dns resolver mode values. // RU: `normalizeDNSResolverMode` - нормализует значения режима dns резолвера. // --------------------------------------------------------------------- func normalizeDNSResolverMode(mode DNSResolverMode, viaSmartDNS bool) DNSResolverMode { switch DNSResolverMode(strings.ToLower(strings.TrimSpace(string(mode)))) { case DNSModeDirect: return DNSModeDirect case DNSModeSmartDNS: // Legacy value: map old SmartDNS-only selection into hybrid wildcard mode. return DNSModeHybridWildcard case DNSModeHybridWildcard, DNSResolverMode("hybrid"): return DNSModeHybridWildcard default: if viaSmartDNS { return DNSModeHybridWildcard } return DNSModeDirect } } // --------------------------------------------------------------------- // EN: `smartDNSAddr` contains core logic for smart d n s addr. // RU: `smartDNSAddr` - содержит основную логику для smart d n s addr. // --------------------------------------------------------------------- func smartDNSAddr() string { return loadDNSMode().SmartDNSAddr } // --------------------------------------------------------------------- // EN: `smartDNSForced` contains core logic for smart d n s forced. // RU: `smartDNSForced` - содержит основную логику для smart d n s forced. // --------------------------------------------------------------------- func smartDNSForced() bool { v := strings.TrimSpace(strings.ToLower(os.Getenv(smartDNSForceEnv))) switch v { case "1", "true", "yes", "on": return true default: return false } } // --------------------------------------------------------------------- // EN: `smartdnsUnitState` contains core logic for smartdns unit state. // RU: `smartdnsUnitState` - содержит основную логику для smartdns unit state. // --------------------------------------------------------------------- func smartdnsUnitState() string { stdout, _, _, _ := runCommand("systemctl", "is-active", "smartdns-local.service") st := strings.TrimSpace(stdout) if st == "" { return "unknown" } return st } // --------------------------------------------------------------------- // EN: `runSmartdnsUnitAction` runs the workflow for smartdns unit action. // RU: `runSmartdnsUnitAction` - запускает рабочий процесс для smartdns unit action. // --------------------------------------------------------------------- func runSmartdnsUnitAction(action string) cmdResult { stdout, stderr, exitCode, err := runCommand("systemctl", action, "smartdns-local.service") res := cmdResult{ OK: err == nil && exitCode == 0, ExitCode: exitCode, Stdout: stdout, Stderr: stderr, } if err != nil { res.Message = err.Error() } else { res.Message = "smartdns " + action + " done" } return res } // --------------------------------------------------------------------- // EN: `resolveDefaultSmartDNSAddr` resolves default smart d n s addr into concrete values. // RU: `resolveDefaultSmartDNSAddr` - резолвит default smart d n s addr в конкретные значения. // --------------------------------------------------------------------- func resolveDefaultSmartDNSAddr() string { if v := strings.TrimSpace(os.Getenv(smartDNSAddrEnv)); v != "" { if addr := normalizeSmartDNSAddr(v); addr != "" { return addr } } for _, path := range []string{ "/opt/stack/adguardapp/smartdns.conf", "/etc/selective-vpn/smartdns.conf", } { if addr := smartDNSAddrFromConfig(path); addr != "" { return addr } } return smartDNSDefaultAddr } // --------------------------------------------------------------------- // EN: `smartDNSAddrFromConfig` loads smart d n s addr from config. // RU: `smartDNSAddrFromConfig` - загружает smart d n s addr из конфига. // --------------------------------------------------------------------- func smartDNSAddrFromConfig(path string) string { data, err := os.ReadFile(path) if err != nil { return "" } for _, ln := range strings.Split(string(data), "\n") { s := strings.TrimSpace(ln) if s == "" || strings.HasPrefix(s, "#") { continue } if !strings.HasPrefix(strings.ToLower(s), "bind ") { continue } parts := strings.Fields(s) if len(parts) < 2 { continue } if addr := normalizeSmartDNSAddr(parts[1]); addr != "" { return addr } } return "" } // --------------------------------------------------------------------- // EN: `normalizeDNSUpstream` parses dns upstream and returns normalized values. // RU: `normalizeDNSUpstream` - парсит dns upstream и возвращает нормализованные значения. // --------------------------------------------------------------------- func normalizeDNSUpstream(raw string, defaultPort string) string { s := strings.TrimSpace(raw) if s == "" { return "" } s = strings.TrimPrefix(s, "udp://") s = strings.TrimPrefix(s, "tcp://") if strings.Contains(s, "#") { parts := strings.SplitN(s, "#", 2) host := strings.Trim(strings.TrimSpace(parts[0]), "[]") port := strings.TrimSpace(parts[1]) if host == "" { return "" } if port == "" { port = defaultPort } return host + "#" + port } if host, port, err := net.SplitHostPort(s); err == nil { host = strings.Trim(strings.TrimSpace(host), "[]") port = strings.TrimSpace(port) if host == "" { return "" } if port == "" { port = defaultPort } return host + "#" + port } if strings.Count(s, ":") == 1 { parts := strings.SplitN(s, ":", 2) host := strings.TrimSpace(parts[0]) port := strings.TrimSpace(parts[1]) if host != "" && port != "" { return host + "#" + port } } return s } // --------------------------------------------------------------------- // EN: `normalizeSmartDNSAddr` parses smart d n s addr and returns normalized values. // RU: `normalizeSmartDNSAddr` - парсит smart d n s addr и возвращает нормализованные значения. // --------------------------------------------------------------------- func normalizeSmartDNSAddr(raw string) string { s := normalizeDNSUpstream(raw, "6053") if s == "" { return "" } if strings.Contains(s, "#") { return s } return s + "#6053" }