baseline: api+gui traffic mode + candidates picker
Snapshot before app-launcher (cgroup/mark) work; ignore binaries/backups.
This commit is contained in:
886
selective-vpn-api/app/dns_settings.go
Normal file
886
selective-vpn-api/app/dns_settings.go
Normal file
@@ -0,0 +1,886 @@
|
||||
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"
|
||||
}
|
||||
Reference in New Issue
Block a user