platform: modularize api/gui, add docs-tests-web foundation, and refresh root config

This commit is contained in:
beckline
2026-03-26 22:40:54 +03:00
parent 0e2d7f61ea
commit 6a56d734c2
562 changed files with 70151 additions and 16423 deletions

View File

@@ -0,0 +1,358 @@
package dnscfg
import (
"context"
"fmt"
"net"
"sort"
"strings"
"sync"
"time"
)
const (
BenchmarkProfileQuick = "quick"
BenchmarkProfileLoad = "load"
BenchmarkErrorNXDomain = "nxdomain"
BenchmarkErrorTimeout = "timeout"
BenchmarkErrorTemporary = "temporary"
BenchmarkErrorOther = "other"
)
var BenchmarkDefaultDomains = []string{
"cloudflare.com",
"google.com",
"telegram.org",
"github.com",
"youtube.com",
"twitter.com",
}
type BenchmarkOptions struct {
Profile string
LoadWorkers int
Rounds int
SyntheticPerDomain int
}
type BenchmarkResult struct {
Upstream string
Attempts int
OK int
Fail int
NXDomain int
Timeout int
Temporary int
Other int
AvgMS int
P95MS int
Score float64
Color string
}
func NormalizeBenchmarkProfile(raw string) string {
switch strings.ToLower(strings.TrimSpace(raw)) {
case "", BenchmarkProfileLoad:
return BenchmarkProfileLoad
case BenchmarkProfileQuick:
return BenchmarkProfileQuick
default:
return BenchmarkProfileLoad
}
}
func MakeDNSBenchmarkOptions(profile string, concurrency int) BenchmarkOptions {
if concurrency < 1 {
concurrency = 1
}
if profile == BenchmarkProfileQuick {
return BenchmarkOptions{
Profile: BenchmarkProfileQuick,
LoadWorkers: 1,
Rounds: 1,
SyntheticPerDomain: 0,
}
}
workers := concurrency * 2
if workers < 4 {
workers = 4
}
if workers > 16 {
workers = 16
}
return BenchmarkOptions{
Profile: BenchmarkProfileLoad,
LoadWorkers: workers,
Rounds: 3,
SyntheticPerDomain: 2,
}
}
func NormalizeBenchmarkUpstreamStrings(in []string, normalizeUpstream func(string, string) string) []string {
out := make([]string, 0, len(in))
seen := map[string]struct{}{}
for _, raw := range in {
n := strings.TrimSpace(raw)
if normalizeUpstream != nil {
n = normalizeUpstream(n, "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,
opts BenchmarkOptions,
lookupAOnce func(host, upstream string, timeout time.Duration) ([]string, error),
classifyErr func(error) string,
) BenchmarkResult {
res := BenchmarkResult{Upstream: upstream}
probes := BuildBenchmarkProbeHosts(domains, attempts, opts)
if len(probes) == 0 {
return res
}
durations := make([]int, 0, len(probes))
var mu sync.Mutex
jobs := make(chan string, len(probes))
workers := opts.LoadWorkers
if workers < 1 {
workers = 1
}
if workers > len(probes) {
workers = len(probes)
}
var wg sync.WaitGroup
for i := 0; i < workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for host := range jobs {
start := time.Now()
_, err := lookupAOnce(host, upstream, timeout)
elapsed := int(time.Since(start).Milliseconds())
if elapsed < 1 {
elapsed = 1
}
mu.Lock()
res.Attempts++
durations = append(durations, elapsed)
if err != nil {
res.Fail++
switch strings.ToLower(strings.TrimSpace(classifyErr(err))) {
case BenchmarkErrorNXDomain:
res.NXDomain++
case BenchmarkErrorTimeout:
res.Timeout++
case BenchmarkErrorTemporary:
res.Temporary++
default:
res.Other++
}
} else {
res.OK++
}
mu.Unlock()
}
}()
}
for _, host := range probes {
jobs <- host
}
close(jobs)
wg.Wait()
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)
answeredRate := float64(res.OK+res.NXDomain+res.Temporary+res.Other) / float64(total)
timeoutRate := float64(res.Timeout) / float64(total)
temporaryRate := float64(res.Temporary) / float64(total)
otherRate := float64(res.Other) / float64(total)
avg := float64(res.AvgMS)
if avg <= 0 {
avg = float64(timeout.Milliseconds())
}
p95 := float64(res.P95MS)
if p95 <= 0 {
p95 = avg
}
res.Score = answeredRate*100.0 + okRate*15.0 - timeoutRate*120.0 - temporaryRate*35.0 - otherRate*20.0 - (avg / 25.0) - (p95 / 45.0)
}
timeoutRate := 0.0
answeredRate := 0.0
if res.Attempts > 0 {
timeoutRate = float64(res.Timeout) / float64(res.Attempts)
answeredRate = float64(res.OK+res.NXDomain+res.Temporary+res.Other) / float64(res.Attempts)
}
switch {
case answeredRate < 0.85 || timeoutRate >= 0.10 || res.P95MS > 1800:
res.Color = "red"
case answeredRate >= 0.97 && timeoutRate <= 0.02 && res.P95MS <= 700:
res.Color = "green"
default:
res.Color = "yellow"
}
return res
}
func BuildBenchmarkProbeHosts(domains []string, attempts int, opts BenchmarkOptions) []string {
if len(domains) == 0 {
return nil
}
if attempts < 1 {
attempts = 1
}
rounds := opts.Rounds
if rounds < 1 {
rounds = 1
}
synth := opts.SyntheticPerDomain
if synth < 0 {
synth = 0
}
out := make([]string, 0, len(domains)*attempts*rounds*(1+synth))
for round := 0; round < rounds; round++ {
for _, host := range domains {
for i := 0; i < attempts; i++ {
out = append(out, host)
}
for n := 0; n < synth; n++ {
out = append(out, fmt.Sprintf("svpn-bench-%d-%d.%s", round+1, n+1, host))
}
}
}
if len(out) > 10000 {
out = out[:10000]
}
return out
}
func DNSLookupAOnce(
host string,
upstream string,
timeout time.Duration,
splitDNS func(string) (string, string),
isPrivateIPv4 func(string) bool,
) ([]string, error) {
if splitDNS == nil {
return nil, fmt.Errorf("splitDNS callback is nil")
}
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 != nil && 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 []BenchmarkResult, 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
}

View File

@@ -0,0 +1,136 @@
package dnscfg
import (
"encoding/json"
"os"
"path/filepath"
"strings"
)
func NormalizeResolverMode(mode string, viaSmartDNS bool, directMode string, smartDNSMode string, hybridWildcardMode string) string {
switch strings.ToLower(strings.TrimSpace(mode)) {
case strings.ToLower(strings.TrimSpace(directMode)):
return directMode
case strings.ToLower(strings.TrimSpace(smartDNSMode)):
return hybridWildcardMode
case strings.ToLower(strings.TrimSpace(hybridWildcardMode)), "hybrid":
return hybridWildcardMode
default:
if viaSmartDNS {
return hybridWildcardMode
}
return directMode
}
}
func SmartDNSForced(envRaw string) bool {
switch strings.TrimSpace(strings.ToLower(envRaw)) {
case "1", "true", "yes", "on":
return true
default:
return false
}
}
type ModeState struct {
ViaSmartDNS bool `json:"via_smartdns"`
SmartDNSAddr string `json:"smartdns_addr"`
Mode string `json:"mode"`
}
type ModeConfig struct {
Path string
DirectMode string
DefaultSmartDNSAddr string
NormalizeResolverMode func(mode string, viaSmartDNS bool) string
NormalizeSmartDNSAddr func(raw string) string
}
func LoadMode(cfg ModeConfig) (ModeState, bool) {
mode := ModeState{
ViaSmartDNS: false,
SmartDNSAddr: strings.TrimSpace(cfg.DefaultSmartDNSAddr),
Mode: strings.TrimSpace(cfg.DirectMode),
}
needPersist := false
data, err := os.ReadFile(strings.TrimSpace(cfg.Path))
switch {
case err == nil:
var stored ModeState
if err := json.Unmarshal(data, &stored); err == nil {
normalized, changed := normalizeModeState(stored, cfg)
mode = normalized
if strings.TrimSpace(stored.Mode) == "" || stored.ViaSmartDNS != normalized.ViaSmartDNS || changed {
needPersist = true
}
} else {
needPersist = true
}
case os.IsNotExist(err):
needPersist = true
}
normalized, changed := normalizeModeState(mode, cfg)
mode = normalized
if changed {
needPersist = true
}
return mode, needPersist
}
func SaveMode(cfg ModeConfig, mode ModeState) error {
normalized, _ := normalizeModeState(mode, cfg)
path := strings.TrimSpace(cfg.Path)
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return err
}
tmp := path + ".tmp"
b, err := json.MarshalIndent(normalized, "", " ")
if err != nil {
return err
}
if err := os.WriteFile(tmp, b, 0o644); err != nil {
return err
}
return os.Rename(tmp, path)
}
func normalizeModeState(mode ModeState, cfg ModeConfig) (ModeState, bool) {
changed := false
prevMode := mode.Mode
mode.Mode = normalizeResolverMode(cfg, mode.Mode, mode.ViaSmartDNS)
if mode.Mode != prevMode {
changed = true
}
viaSmartDNS := mode.Mode != strings.TrimSpace(cfg.DirectMode)
if mode.ViaSmartDNS != viaSmartDNS {
mode.ViaSmartDNS = viaSmartDNS
changed = true
}
prevAddr := mode.SmartDNSAddr
mode.SmartDNSAddr = normalizeSmartDNSAddr(cfg, mode.SmartDNSAddr)
if mode.SmartDNSAddr == "" {
mode.SmartDNSAddr = strings.TrimSpace(cfg.DefaultSmartDNSAddr)
}
if mode.SmartDNSAddr != prevAddr {
changed = true
}
return mode, changed
}
func normalizeResolverMode(cfg ModeConfig, mode string, viaSmartDNS bool) string {
if cfg.NormalizeResolverMode == nil {
return strings.TrimSpace(mode)
}
return strings.TrimSpace(cfg.NormalizeResolverMode(mode, viaSmartDNS))
}
func normalizeSmartDNSAddr(cfg ModeConfig, raw string) string {
if cfg.NormalizeSmartDNSAddr == nil {
return strings.TrimSpace(raw)
}
return strings.TrimSpace(cfg.NormalizeSmartDNSAddr(raw))
}

View File

@@ -0,0 +1,100 @@
package dnscfg
import "strings"
type Upstreams struct {
Default1 string
Default2 string
Meta1 string
Meta2 string
}
type UpstreamPoolItem struct {
Addr string
Enabled bool
}
func NormalizeUpstreamPoolItems(items []UpstreamPoolItem, normalizeUpstream func(raw string, defaultPort string) string) []UpstreamPoolItem {
if len(items) == 0 {
return nil
}
seen := map[string]struct{}{}
out := make([]UpstreamPoolItem, 0, len(items))
for _, item := range items {
addr := strings.TrimSpace(item.Addr)
if normalizeUpstream != nil {
addr = normalizeUpstream(addr, "53")
}
if addr == "" {
continue
}
if _, ok := seen[addr]; ok {
continue
}
seen[addr] = struct{}{}
out = append(out, UpstreamPoolItem{
Addr: addr,
Enabled: item.Enabled,
})
}
return out
}
func UpstreamPoolFromLegacy(cfg Upstreams, normalizeUpstream func(raw string, defaultPort string) string) []UpstreamPoolItem {
out := []UpstreamPoolItem{
{Addr: cfg.Default1, Enabled: true},
{Addr: cfg.Default2, Enabled: true},
{Addr: cfg.Meta1, Enabled: true},
{Addr: cfg.Meta2, Enabled: true},
}
return NormalizeUpstreamPoolItems(out, normalizeUpstream)
}
func UpstreamPoolToLegacy(items []UpstreamPoolItem, defaults Upstreams, normalizeUpstream func(raw string, defaultPort string) string) Upstreams {
items = NormalizeUpstreamPoolItems(items, normalizeUpstream)
out := defaults
enabled := make([]string, 0, len(items))
for _, item := range items {
if !item.Enabled {
continue
}
addr := strings.TrimSpace(item.Addr)
if normalizeUpstream != nil {
addr = normalizeUpstream(addr, "53")
}
if addr != "" {
enabled = append(enabled, addr)
}
}
if len(enabled) > 0 {
out.Default1 = enabled[0]
}
if len(enabled) > 1 {
out.Default2 = enabled[1]
}
if len(enabled) > 2 {
out.Meta1 = enabled[2]
}
if len(enabled) > 3 {
out.Meta2 = enabled[3]
}
return out
}
func EnabledPool(items []UpstreamPoolItem, normalizeUpstream func(raw string, defaultPort string) string) []string {
items = NormalizeUpstreamPoolItems(items, normalizeUpstream)
out := make([]string, 0, len(items))
for _, item := range items {
if !item.Enabled {
continue
}
addr := strings.TrimSpace(item.Addr)
if normalizeUpstream != nil {
addr = normalizeUpstream(addr, "53")
}
if addr != "" {
out = append(out, addr)
}
}
return out
}

View File

@@ -0,0 +1,392 @@
package dnscfg
import (
"fmt"
"sort"
"strings"
"time"
)
type PrewarmDNSUpstreamMetrics struct {
Attempts int
OK int
NXDomain int
Timeout int
Temporary int
Other int
Skipped int
}
type PrewarmDNSMetrics struct {
Attempts int
OK int
NXDomain int
Timeout int
Temporary int
Other int
Skipped int
PerUpstream map[string]PrewarmDNSUpstreamMetrics
}
func (m *PrewarmDNSMetrics) Merge(other PrewarmDNSMetrics) {
m.Attempts += other.Attempts
m.OK += other.OK
m.NXDomain += other.NXDomain
m.Timeout += other.Timeout
m.Temporary += other.Temporary
m.Other += other.Other
m.Skipped += other.Skipped
if len(other.PerUpstream) == 0 {
return
}
if m.PerUpstream == nil {
m.PerUpstream = map[string]PrewarmDNSUpstreamMetrics{}
}
for upstream, src := range other.PerUpstream {
dst := m.PerUpstream[upstream]
dst.Attempts += src.Attempts
dst.OK += src.OK
dst.NXDomain += src.NXDomain
dst.Timeout += src.Timeout
dst.Temporary += src.Temporary
dst.Other += src.Other
dst.Skipped += src.Skipped
m.PerUpstream[upstream] = dst
}
}
func (m PrewarmDNSMetrics) TotalErrors() int {
return m.NXDomain + m.Timeout + m.Temporary + m.Other
}
func (m PrewarmDNSMetrics) FormatPerUpstream() string {
if len(m.PerUpstream) == 0 {
return ""
}
keys := make([]string, 0, len(m.PerUpstream))
for k := range m.PerUpstream {
keys = append(keys, k)
}
sort.Strings(keys)
parts := make([]string, 0, len(keys))
for _, k := range keys {
v := m.PerUpstream[k]
parts = append(parts, fmt.Sprintf("%s{attempts=%d ok=%d nxdomain=%d timeout=%d temporary=%d other=%d skipped=%d}", k, v.Attempts, v.OK, v.NXDomain, v.Timeout, v.Temporary, v.Other, v.Skipped))
}
return strings.Join(parts, "; ")
}
type PrewarmInput struct {
Mode string
Source string
RuntimeEnabled bool
SmartDNSAddr string
Wildcards []string
AggressiveSubs bool
Subs []string
SubsPerBaseLimit int
Limit int
Workers int
TimeoutMS int
EnvWorkers int
EnvTimeoutMS int
MaxHostsLog int
WildcardMapPath string
}
type PrewarmDeps struct {
IsGoogleLike func(string) bool
EnsureRuntimeSet func()
DigA func(host string, dnsList []string, timeout time.Duration) ([]string, PrewarmDNSMetrics)
ReadDynSet func() ([]string, error)
ApplyDynSet func([]string) error
Logf func(message string)
}
type PrewarmResult struct {
OK bool
Message string
ExitCode int
ResolvedHosts int
}
func RunPrewarm(in PrewarmInput, deps PrewarmDeps) PrewarmResult {
smartdnsAddr := strings.TrimSpace(in.SmartDNSAddr)
if smartdnsAddr == "" {
return PrewarmResult{OK: false, Message: "SmartDNS address is empty"}
}
wildcards := trimNonEmptyUnique(in.Wildcards)
if len(wildcards) == 0 {
msg := "prewarm skipped: wildcard list is empty"
logPrewarm(deps.Logf, msg)
return PrewarmResult{OK: true, Message: msg}
}
aggressive := in.AggressiveSubs
subs := trimNonEmptyUnique(in.Subs)
subsPerBaseLimit := in.SubsPerBaseLimit
if subsPerBaseLimit < 0 {
subsPerBaseLimit = 0
}
domainSet := make(map[string]struct{}, len(wildcards)*(len(subs)+1))
for _, d := range wildcards {
domainSet[d] = struct{}{}
if !aggressive || isGoogleLikeSafe(deps.IsGoogleLike, d) {
continue
}
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 in.Limit > 0 && len(domains) > in.Limit {
domains = domains[:in.Limit]
}
if len(domains) == 0 {
msg := "prewarm skipped: expanded wildcard list is empty"
logPrewarm(deps.Logf, msg)
return PrewarmResult{OK: true, Message: msg}
}
workers := in.Workers
if workers <= 0 {
workers = in.EnvWorkers
if workers <= 0 {
workers = 24
}
}
if workers < 1 {
workers = 1
}
if workers > 200 {
workers = 200
}
timeoutMS := in.TimeoutMS
if timeoutMS <= 0 {
timeoutMS = in.EnvTimeoutMS
if timeoutMS <= 0 {
timeoutMS = 1800
}
}
if timeoutMS < 200 {
timeoutMS = 200
}
if timeoutMS > 15000 {
timeoutMS = 15000
}
timeout := time.Duration(timeoutMS) * time.Millisecond
if deps.EnsureRuntimeSet != nil {
deps.EnsureRuntimeSet()
}
logPrewarm(
deps.Logf,
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",
strings.TrimSpace(in.Mode),
strings.TrimSpace(in.Source),
in.RuntimeEnabled,
smartdnsAddr,
len(wildcards),
len(domains),
aggressive,
workers,
timeoutMS,
),
)
type prewarmItem struct {
host string
ips []string
stats PrewarmDNSMetrics
}
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 := safeDigA(deps.DigA, host, []string{smartdnsAddr}, timeout)
results <- prewarmItem{host: host, ips: ips, stats: stats}
}
}()
}
for _, host := range domains {
jobs <- host
}
close(jobs)
resolvedHosts := 0
totalIPs := 0
errorHosts := 0
stats := PrewarmDNSMetrics{}
resolvedIPSet := map[string]struct{}{}
loggedHosts := 0
maxHostsLog := in.MaxHostsLog
if maxHostsLog <= 0 {
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 {
logPrewarm(deps.Logf, fmt.Sprintf("prewarm add: %s -> %s", item.host, strings.Join(item.ips, ", ")))
loggedHosts++
}
}
manualAdded := 0
totalDynText := "n/a"
if !in.RuntimeEnabled {
existing, _ := safeReadDynSet(deps.ReadDynSet)
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)
}
totalDynText = fmt.Sprintf("%d", len(merged))
if err := safeApplyDynSet(deps.ApplyDynSet, merged); err != nil {
msg := fmt.Sprintf("prewarm manual apply failed: %v", err)
logPrewarm(deps.Logf, msg)
return PrewarmResult{OK: false, Message: msg}
}
logPrewarm(
deps.Logf,
fmt.Sprintf("prewarm manual merge: existing=%d resolved=%d added=%d total_dyn=%d", len(existing), len(resolvedIPSet), manualAdded, len(merged)),
)
}
if len(domains) > loggedHosts {
logPrewarm(
deps.Logf,
fmt.Sprintf(
"prewarm add: trace truncated, omitted=%d hosts (full wildcard map: %s)",
len(domains)-loggedHosts,
strings.TrimSpace(in.WildcardMapPath),
),
)
}
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",
strings.TrimSpace(in.Source),
len(domains),
resolvedHosts,
totalIPs,
errorHosts,
stats.Attempts,
stats.OK,
stats.TotalErrors(),
manualAdded,
totalDynText,
)
logPrewarm(deps.Logf, msg)
if perUpstream := stats.FormatPerUpstream(); perUpstream != "" {
logPrewarm(deps.Logf, "prewarm dns upstreams: "+perUpstream)
}
return PrewarmResult{
OK: true,
Message: msg,
ExitCode: resolvedHosts,
ResolvedHosts: resolvedHosts,
}
}
func logPrewarm(logf func(string), msg string) {
if logf != nil {
logf(msg)
}
}
func safeDigA(
dig func(host string, dnsList []string, timeout time.Duration) ([]string, PrewarmDNSMetrics),
host string,
dnsList []string,
timeout time.Duration,
) ([]string, PrewarmDNSMetrics) {
if dig == nil {
return nil, PrewarmDNSMetrics{}
}
return dig(host, dnsList, timeout)
}
func safeReadDynSet(read func() ([]string, error)) ([]string, error) {
if read == nil {
return nil, nil
}
return read()
}
func safeApplyDynSet(apply func([]string) error, ips []string) error {
if apply == nil {
return fmt.Errorf("apply dyn set callback is nil")
}
return apply(ips)
}
func isGoogleLikeSafe(check func(string) bool, domain string) bool {
if check == nil {
return false
}
return check(domain)
}
func trimNonEmptyUnique(in []string) []string {
if len(in) == 0 {
return nil
}
seen := map[string]struct{}{}
out := make([]string, 0, len(in))
for _, item := range in {
v := strings.TrimSpace(item)
if v == "" {
continue
}
if _, ok := seen[v]; ok {
continue
}
seen[v] = struct{}{}
out = append(out, v)
}
return out
}

View File

@@ -0,0 +1,102 @@
package dnscfg
import (
"net"
"os"
"strings"
)
func ResolveDefaultSmartDNSAddr(addrEnvValue string, configPaths []string, fallback string) string {
if v := strings.TrimSpace(addrEnvValue); v != "" {
if addr := NormalizeSmartDNSAddr(v); addr != "" {
return addr
}
}
for _, path := range configPaths {
if addr := SmartDNSAddrFromConfig(path); addr != "" {
return addr
}
}
return strings.TrimSpace(fallback)
}
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 ""
}
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
}
func NormalizeSmartDNSAddr(raw string) string {
s := NormalizeDNSUpstream(raw, "6053")
if s == "" {
return ""
}
if strings.Contains(s, "#") {
return s
}
return s + "#6053"
}

View File

@@ -0,0 +1,47 @@
package dnscfg
import "strings"
type RunCommandFunc func(name string, args ...string) (stdout string, stderr string, exitCode int, err error)
type CmdResult struct {
OK bool
ExitCode int
Stdout string
Stderr string
Message string
}
func UnitState(run RunCommandFunc, unit string) string {
if run == nil {
return "unknown"
}
stdout, _, _, _ := run("systemctl", "is-active", strings.TrimSpace(unit))
st := strings.TrimSpace(stdout)
if st == "" {
return "unknown"
}
return st
}
func RunUnitAction(run RunCommandFunc, unit, action string) CmdResult {
if run == nil {
return CmdResult{
OK: false,
Message: "run command func is nil",
}
}
stdout, stderr, exitCode, err := run("systemctl", strings.TrimSpace(action), strings.TrimSpace(unit))
res := CmdResult{
OK: err == nil && exitCode == 0,
ExitCode: exitCode,
Stdout: stdout,
Stderr: stderr,
}
if err != nil {
res.Message = err.Error()
} else {
res.Message = strings.TrimSpace(unit) + " " + strings.TrimSpace(action) + " done"
}
return res
}

View File

@@ -0,0 +1,69 @@
package dnscfg
import (
"fmt"
"strings"
)
func NormalizeUpstreams(cfg Upstreams, defaults Upstreams, normalizeUpstream func(raw string, defaultPort string) string) Upstreams {
if normalizeUpstream != nil {
cfg.Default1 = normalizeUpstream(cfg.Default1, "53")
cfg.Default2 = normalizeUpstream(cfg.Default2, "53")
cfg.Meta1 = normalizeUpstream(cfg.Meta1, "53")
cfg.Meta2 = normalizeUpstream(cfg.Meta2, "53")
}
if strings.TrimSpace(cfg.Default1) == "" {
cfg.Default1 = defaults.Default1
}
if strings.TrimSpace(cfg.Default2) == "" {
cfg.Default2 = defaults.Default2
}
if strings.TrimSpace(cfg.Meta1) == "" {
cfg.Meta1 = defaults.Meta1
}
if strings.TrimSpace(cfg.Meta2) == "" {
cfg.Meta2 = defaults.Meta2
}
return cfg
}
func ParseUpstreamsConf(content string, defaults Upstreams, normalizeUpstream func(raw string, defaultPort string) string) Upstreams {
cfg := defaults
for _, ln := range strings.Split(content, "\n") {
s := strings.TrimSpace(ln)
if s == "" || strings.HasPrefix(s, "#") {
continue
}
parts := strings.Fields(s)
if len(parts) < 2 {
continue
}
key := strings.ToLower(strings.TrimSpace(parts[0]))
vals := parts[1:]
switch key {
case "default":
if len(vals) > 0 {
cfg.Default1 = vals[0]
}
if len(vals) > 1 {
cfg.Default2 = vals[1]
}
case "meta":
if len(vals) > 0 {
cfg.Meta1 = vals[0]
}
if len(vals) > 1 {
cfg.Meta2 = vals[1]
}
}
}
return NormalizeUpstreams(cfg, defaults, normalizeUpstream)
}
func RenderUpstreamsConf(cfg Upstreams) string {
return fmt.Sprintf(
"default %s %s\nmeta %s %s\n",
cfg.Default1, cfg.Default2, cfg.Meta1, cfg.Meta2,
)
}