887 lines
27 KiB
Go
887 lines
27 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"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))
|
|
}
|
|
|
|
// ---------------------------------------------------------------------
|
|
// 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"
|
|
}
|