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