Files
elmprodvpn/selective-vpn-api/app/dns_settings.go

1216 lines
34 KiB
Go

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)
}
}
// ---------------------------------------------------------------------
// 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 {
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 {
if !item.Enabled {
continue
}
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: +%d domains omitted", len(domains)-loggedHosts))
}
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
}
}
// ---------------------------------------------------------------------
// EN: `loadDNSUpstreamsConf` loads dns upstreams conf from storage or config.
// RU: `loadDNSUpstreamsConf` - загружает dns upstreams conf из хранилища или конфига.
// ---------------------------------------------------------------------
func loadDNSUpstreamsConf() 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
}
// ---------------------------------------------------------------------
// EN: `saveDNSUpstreamsConf` saves dns upstreams conf to persistent storage.
// RU: `saveDNSUpstreamsConf` - сохраняет dns upstreams conf в постоянное хранилище.
// ---------------------------------------------------------------------
func saveDNSUpstreamsConf(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: `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"
}