platform: modularize api/gui, add docs-tests-web foundation, and refresh root config
This commit is contained in:
@@ -1,17 +1,8 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net"
|
||||
"os"
|
||||
"os/user"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
@@ -50,698 +41,51 @@ func routesUpdate(iface string) cmdResult {
|
||||
return res
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// preflight
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
// ensure dirs
|
||||
_ = os.MkdirAll(stateDir, 0o755)
|
||||
_ = os.MkdirAll(domainDir, 0o755)
|
||||
_ = os.MkdirAll("/etc/selective-vpn", 0o755)
|
||||
|
||||
heartbeat()
|
||||
|
||||
// wait iface up
|
||||
up := false
|
||||
for i := 0; i < 30; i++ {
|
||||
if _, _, code, _ := runCommandTimeout(3*time.Second, "ip", "link", "show", iface); code == 0 {
|
||||
up = true
|
||||
break
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
heartbeat()
|
||||
}
|
||||
if !up {
|
||||
logp("no %s, exit 0", iface)
|
||||
res.OK = true
|
||||
res.Message = "interface not found, skipped"
|
||||
return res
|
||||
}
|
||||
|
||||
// wait DNS (like wait-for-dns.sh)
|
||||
if err := waitDNS(15, 1*time.Second); err != nil {
|
||||
logp("dns not ready: %v", err)
|
||||
res.Message = "dns not ready"
|
||||
return res
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// policy routing setup
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
// rt_tables entry
|
||||
ensureRoutesTableEntry()
|
||||
|
||||
// ip rules: remove old rules pointing to table
|
||||
if out, _, _, _ := runCommandTimeout(5*time.Second, "ip", "rule", "show"); out != "" {
|
||||
for _, line := range strings.Split(out, "\n") {
|
||||
if !strings.Contains(line, "lookup "+routesTableName()) {
|
||||
continue
|
||||
}
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) == 0 {
|
||||
continue
|
||||
}
|
||||
pref := strings.TrimSuffix(fields[0], ":")
|
||||
if pref == "" {
|
||||
continue
|
||||
}
|
||||
_, _, _, _ = runCommandTimeout(5*time.Second, "ip", "rule", "del", "pref", pref)
|
||||
}
|
||||
}
|
||||
|
||||
// clean table and set default route
|
||||
_, _, _, _ = runCommandTimeout(5*time.Second, "ip", "route", "flush", "table", routesTableName())
|
||||
_, _, _, _ = runCommandTimeout(5*time.Second, "ip", "-4", "route", "replace", "default", "dev", iface, "table", routesTableName(), "mtu", policyRouteMTU)
|
||||
// apply traffic mode rules (selective/full_tunnel/direct) over fresh table.
|
||||
trafficState := loadTrafficModeState()
|
||||
trafficIface, trafficIfaceReason := resolveTrafficIface(trafficState.PreferredIface)
|
||||
if trafficIface == "" {
|
||||
trafficIface = iface
|
||||
trafficIfaceReason = "routes-update-iface"
|
||||
}
|
||||
if err := applyTrafficMode(trafficState, trafficIface); err != nil {
|
||||
logp("traffic mode apply failed: mode=%s iface=%s err=%v", trafficState.Mode, iface, err)
|
||||
res.Message = fmt.Sprintf("traffic mode apply failed: %v", err)
|
||||
return res
|
||||
}
|
||||
trafficEval := evaluateTrafficMode(trafficState)
|
||||
logp(
|
||||
"traffic mode: desired=%s applied=%s healthy=%t iface=%s reason=%s",
|
||||
trafficEval.DesiredMode,
|
||||
trafficEval.AppliedMode,
|
||||
trafficEval.Healthy,
|
||||
trafficEval.ActiveIface,
|
||||
trafficEval.Message+" (apply_iface_source="+trafficIfaceReason+")",
|
||||
)
|
||||
|
||||
// ensure default exists
|
||||
if out, _, _, _ := runCommandTimeout(5*time.Second, "ip", "route", "show", "table", routesTableName()); !strings.Contains(out, "default dev "+iface) {
|
||||
_, _, _, _ = runCommandTimeout(5*time.Second, "ip", "-4", "route", "replace", "default", "dev", iface, "table", routesTableName(), "mtu", policyRouteMTU)
|
||||
}
|
||||
|
||||
heartbeat()
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// nft base objects
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
// nft setup
|
||||
_, _, _, _ = runCommandTimeout(5*time.Second, "nft", "add", "table", "inet", "agvpn")
|
||||
_, _, _, _ = runCommandTimeout(5*time.Second, "nft", "add", "set", "inet", "agvpn", "agvpn4", "{", "type", "ipv4_addr", ";", "flags", "interval", ";", "}")
|
||||
_, _, _, _ = runCommandTimeout(5*time.Second, "nft", "add", "set", "inet", "agvpn", "agvpn_dyn4", "{", "type", "ipv4_addr", ";", "flags", "interval", ";", "}")
|
||||
|
||||
// EN: Output chain jumps into:
|
||||
// EN: - output_apps: runtime per-app marks (MARK_DIRECT / MARK_APP)
|
||||
// EN: - output_ips: selective domain IP sets (MARK)
|
||||
// RU: Output chain прыгает в:
|
||||
// RU: - output_apps: runtime per-app marks (MARK_DIRECT / MARK_APP)
|
||||
// RU: - output_ips: селективные доменные IP сеты (MARK)
|
||||
|
||||
_, _, _, _ = runCommandTimeout(5*time.Second, "nft", "add", "chain", "inet", "agvpn", "output", "{", "type", "route", "hook", "output", "priority", "mangle;", "policy", "accept;", "}")
|
||||
_, _, _, _ = runCommandTimeout(5*time.Second, "nft", "add", "chain", "inet", "agvpn", "output_apps")
|
||||
_, _, _, _ = runCommandTimeout(5*time.Second, "nft", "add", "chain", "inet", "agvpn", "output_ips")
|
||||
|
||||
// Base chain: stable jumps only.
|
||||
_, _, _, _ = runCommandTimeout(5*time.Second, "nft", "flush", "chain", "inet", "agvpn", "output")
|
||||
_, _, _, _ = runCommandTimeout(5*time.Second, "nft", "add", "rule", "inet", "agvpn", "output", "jump", "output_apps")
|
||||
_, _, _, _ = runCommandTimeout(5*time.Second, "nft", "add", "rule", "inet", "agvpn", "output", "jump", "output_ips")
|
||||
|
||||
// App chain: runtime rules are managed by traffic_appmarks.go (do not flush here).
|
||||
|
||||
// Domain chain: selective IP sets (resolver output).
|
||||
_, _, _, _ = runCommandTimeout(5*time.Second, "nft", "flush", "chain", "inet", "agvpn", "output_ips")
|
||||
_, _, _, _ = runCommandTimeout(5*time.Second, "nft", "add", "rule", "inet", "agvpn", "output_ips", "ip", "daddr", "@agvpn4", "meta", "mark", "set", MARK)
|
||||
_, _, _, _ = runCommandTimeout(5*time.Second, "nft", "add", "rule", "inet", "agvpn", "output_ips", "ip", "daddr", "@agvpn_dyn4", "meta", "mark", "set", MARK)
|
||||
|
||||
_, _, _, _ = runCommandTimeout(5*time.Second, "nft", "add", "chain", "inet", "agvpn", "prerouting", "{", "type", "filter", "hook", "prerouting", "priority", "mangle;", "policy", "accept;", "}")
|
||||
_, _, _, _ = runCommandTimeout(5*time.Second, "nft", "flush", "chain", "inet", "agvpn", "prerouting")
|
||||
_, _, _, _ = runCommandTimeout(5*time.Second, "nft", "add", "rule", "inet", "agvpn", "prerouting", "iifname", "!=", iface, "ip", "daddr", "@agvpn4", "meta", "mark", "set", MARK)
|
||||
_, _, _, _ = runCommandTimeout(5*time.Second, "nft", "add", "rule", "inet", "agvpn", "prerouting", "iifname", "!=", iface, "ip", "daddr", "@agvpn_dyn4", "meta", "mark", "set", MARK)
|
||||
|
||||
heartbeat()
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// domains + resolver
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
// domain lists
|
||||
bases := loadList(domainDir + "/bases.txt")
|
||||
subs := loadList(domainDir + "/subs.txt")
|
||||
wildcards := loadSmartDNSWildcardDomains(logp)
|
||||
wildcardBaseSet := make(map[string]struct{}, len(wildcards))
|
||||
for _, d := range wildcards {
|
||||
d = strings.TrimSpace(d)
|
||||
if d != "" {
|
||||
wildcardBaseSet[d] = struct{}{}
|
||||
}
|
||||
}
|
||||
wildcardBasesAdded := 0
|
||||
for _, d := range wildcards {
|
||||
d = strings.TrimSpace(d)
|
||||
if d == "" {
|
||||
continue
|
||||
}
|
||||
bases = append(bases, d)
|
||||
wildcardBasesAdded++
|
||||
}
|
||||
subsPerBaseLimit := envInt("RESOLVE_SUBS_PER_BASE_LIMIT", 0)
|
||||
if subsPerBaseLimit < 0 {
|
||||
subsPerBaseLimit = 0
|
||||
}
|
||||
hardCap := envInt("RESOLVE_DOMAINS_HARD_CAP", 0)
|
||||
if hardCap < 0 {
|
||||
hardCap = 0
|
||||
}
|
||||
|
||||
domainSet := make(map[string]struct{})
|
||||
expandedAdded := 0
|
||||
twitterAdded := 0
|
||||
for _, d := range bases {
|
||||
domainSet[d] = struct{}{}
|
||||
_, wildcardBase := wildcardBaseSet[d]
|
||||
// Wildcard bases are now resolved "as-is" (no subs fan-out) to keep
|
||||
// SmartDNS wildcard behavior transparent and avoid synthetic host noise.
|
||||
if !wildcardBase && !isGoogleLike(d) {
|
||||
limit := len(subs)
|
||||
if subsPerBaseLimit > 0 && subsPerBaseLimit < limit {
|
||||
limit = subsPerBaseLimit
|
||||
}
|
||||
for i := 0; i < limit; i++ {
|
||||
fqdn := subs[i] + "." + d
|
||||
if _, ok := domainSet[fqdn]; !ok {
|
||||
expandedAdded++
|
||||
}
|
||||
domainSet[fqdn] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, spec := range twitterSpecial {
|
||||
fqdn := spec + ".twitter.com"
|
||||
if _, ok := domainSet[fqdn]; !ok {
|
||||
twitterAdded++
|
||||
}
|
||||
domainSet[fqdn] = struct{}{}
|
||||
}
|
||||
|
||||
domains := make([]string, 0, len(domainSet))
|
||||
for d := range domainSet {
|
||||
if d != "" {
|
||||
domains = append(domains, d)
|
||||
}
|
||||
}
|
||||
sort.Strings(domains)
|
||||
totalBeforeCap := len(domains)
|
||||
if hardCap > 0 && len(domains) > hardCap {
|
||||
domains = domains[:hardCap]
|
||||
logp("domain cap applied: before=%d after=%d hard_cap=%d", totalBeforeCap, len(domains), hardCap)
|
||||
}
|
||||
logp(
|
||||
"domains expanded: bases=%d subs_total=%d subs_per_base_limit=%d expanded_added=%d twitter_added=%d total_before_cap=%d total_used=%d",
|
||||
len(bases),
|
||||
len(subs),
|
||||
subsPerBaseLimit,
|
||||
expandedAdded,
|
||||
twitterAdded,
|
||||
totalBeforeCap,
|
||||
len(domains),
|
||||
)
|
||||
if wildcardBasesAdded > 0 {
|
||||
logp("domains wildcard seed added: %d base domains from smartdns.conf state", wildcardBasesAdded)
|
||||
appendTraceLineTo(
|
||||
smartdnsLogPath,
|
||||
"smartdns",
|
||||
fmt.Sprintf(
|
||||
"wildcard plan: base_domains=%d sub_expanded=0 (routes update uses pure wildcard bases; subs fan-out only in aggressive prewarm)",
|
||||
wildcardBasesAdded,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
domTmp, _ := os.CreateTemp(stateDir, "domains-*.txt")
|
||||
defer os.Remove(domTmp.Name())
|
||||
for _, d := range domains {
|
||||
_, _ = domTmp.WriteString(d + "\n")
|
||||
}
|
||||
domTmp.Close()
|
||||
|
||||
ipTmp, _ := os.CreateTemp(stateDir, "ips-*.txt")
|
||||
ipTmp.Close()
|
||||
ipMapTmp, _ := os.CreateTemp(stateDir, "ipmap-*.txt")
|
||||
ipMapTmp.Close()
|
||||
ipDirectTmp, _ := os.CreateTemp(stateDir, "ips-direct-*.txt")
|
||||
ipDirectTmp.Close()
|
||||
ipDynTmp, _ := os.CreateTemp(stateDir, "ips-dyn-*.txt")
|
||||
ipDynTmp.Close()
|
||||
ipMapDirectTmp, _ := os.CreateTemp(stateDir, "ipmap-direct-*.txt")
|
||||
ipMapDirectTmp.Close()
|
||||
ipMapDynTmp, _ := os.CreateTemp(stateDir, "ipmap-dyn-*.txt")
|
||||
ipMapDynTmp.Close()
|
||||
|
||||
heartbeat()
|
||||
logp("using Go resolver for domains -> IPs")
|
||||
mode := loadDNSMode()
|
||||
runtimeEnabled := smartDNSRuntimeEnabled()
|
||||
wildcardSource := wildcardFillSource(runtimeEnabled)
|
||||
logp("resolver mode=%s smartdns_addr=%s wildcards=%d", mode.Mode, mode.SmartDNSAddr, len(wildcards))
|
||||
logp("wildcard source baseline: %s (runtime_nftset=%t)", wildcardSource, runtimeEnabled)
|
||||
|
||||
resolveOpts := ResolverOpts{
|
||||
DomainsPath: domTmp.Name(),
|
||||
MetaPath: domainDir + "/meta-special.txt",
|
||||
StaticPath: staticIPsFile,
|
||||
CachePath: stateDir + "/domain-cache.json",
|
||||
PtrCachePath: stateDir + "/ptr-cache.json",
|
||||
TraceLog: traceLogPath,
|
||||
TTL: envInt("RESOLVE_TTL", 24*3600),
|
||||
Workers: envInt("RESOLVE_JOBS", 40),
|
||||
DNSConfigPath: dnsUpstreamsConf,
|
||||
ViaSmartDNS: mode.ViaSmartDNS, // legacy fallback for older clients/state
|
||||
Mode: mode.Mode,
|
||||
SmartDNSAddr: mode.SmartDNSAddr,
|
||||
SmartDNSWildcards: wildcards,
|
||||
}
|
||||
|
||||
resJob, err := runResolverJob(resolveOpts, logp)
|
||||
skip, message, err := routesUpdateStagePreflight(iface, logp, heartbeat)
|
||||
if err != nil {
|
||||
logp("Go resolver FAILED: %v", err)
|
||||
res.Message = fmt.Sprintf("resolver failed: %v", err)
|
||||
res.Message = message
|
||||
return res
|
||||
}
|
||||
if skip {
|
||||
res.OK = true
|
||||
res.Message = message
|
||||
return res
|
||||
}
|
||||
|
||||
if err := writeLines(ipTmp.Name(), resJob.IPs); err != nil {
|
||||
logp("write ips failed: %v", err)
|
||||
res.Message = fmt.Sprintf("write ips failed: %v", err)
|
||||
if err := routesUpdateStagePolicy(iface, logp); err != nil {
|
||||
res.Message = err.Error()
|
||||
return res
|
||||
}
|
||||
if err := writeMapPairs(ipMapTmp.Name(), resJob.IPMap); err != nil {
|
||||
logp("write ip_map failed: %v", err)
|
||||
res.Message = fmt.Sprintf("write ip_map failed: %v", err)
|
||||
return res
|
||||
}
|
||||
if err := writeLines(ipDirectTmp.Name(), resJob.DirectIPs); err != nil {
|
||||
logp("write direct ips failed: %v", err)
|
||||
res.Message = fmt.Sprintf("write direct ips failed: %v", err)
|
||||
return res
|
||||
}
|
||||
if err := writeLines(ipDynTmp.Name(), resJob.WildcardIPs); err != nil {
|
||||
logp("write wildcard ips failed: %v", err)
|
||||
res.Message = fmt.Sprintf("write wildcard ips failed: %v", err)
|
||||
return res
|
||||
}
|
||||
if err := writeMapPairs(ipMapDirectTmp.Name(), resJob.DirectIPMap); err != nil {
|
||||
logp("write direct ip_map failed: %v", err)
|
||||
res.Message = fmt.Sprintf("write direct ip_map failed: %v", err)
|
||||
return res
|
||||
}
|
||||
if err := writeMapPairs(ipMapDynTmp.Name(), resJob.WildcardIPMap); err != nil {
|
||||
logp("write wildcard ip_map failed: %v", err)
|
||||
res.Message = fmt.Sprintf("write wildcard ip_map failed: %v", err)
|
||||
return res
|
||||
}
|
||||
saveJSON(resJob.DomainCache, resolveOpts.CachePath)
|
||||
saveJSON(resJob.PtrCache, resolveOpts.PtrCachePath)
|
||||
|
||||
heartbeat()
|
||||
|
||||
ipCount := len(resJob.IPs)
|
||||
directIPCount := len(resJob.DirectIPs)
|
||||
wildcardIPCount := len(resJob.WildcardIPs)
|
||||
domainCount := countDomainsFromPairs(resJob.IPMap)
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// nft population
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
// nft load через умный апдейтер
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
progressCb := func(percent int, msg string) {
|
||||
logp("NFT progress: %d%% - %s", percent, msg)
|
||||
heartbeat()
|
||||
events.push("routes_nft_progress", map[string]any{
|
||||
"percent": percent,
|
||||
"message": msg,
|
||||
})
|
||||
}
|
||||
|
||||
progressRange := func(start, end int, prefix string) ProgressCallback {
|
||||
if progressCb == nil {
|
||||
return nil
|
||||
}
|
||||
if end < start {
|
||||
end = start
|
||||
}
|
||||
return func(percent int, msg string) {
|
||||
if percent < 0 {
|
||||
percent = 0
|
||||
}
|
||||
if percent > 100 {
|
||||
percent = 100
|
||||
}
|
||||
scaled := start + (end-start)*percent/100
|
||||
if strings.TrimSpace(msg) == "" {
|
||||
msg = "updating"
|
||||
}
|
||||
progressCb(scaled, prefix+": "+msg)
|
||||
}
|
||||
}
|
||||
|
||||
if err := nftUpdateSetIPsSmart(ctx, "agvpn4", resJob.DirectIPs, progressRange(0, 50, "agvpn4")); err != nil {
|
||||
logp("nft set update failed for agvpn4: %v", err)
|
||||
res.Message = fmt.Sprintf("nft update failed for agvpn4: %v", err)
|
||||
return res
|
||||
}
|
||||
if err := nftUpdateSetIPsSmart(ctx, "agvpn_dyn4", resJob.WildcardIPs, progressRange(50, 100, "agvpn_dyn4")); err != nil {
|
||||
logp("nft set update failed for agvpn_dyn4: %v", err)
|
||||
res.Message = fmt.Sprintf("nft update failed for agvpn_dyn4: %v", err)
|
||||
return res
|
||||
}
|
||||
|
||||
logp("summary: domains=%d, unique_ips=%d direct_ips=%d wildcard_ips=%d", len(domains), ipCount, directIPCount, wildcardIPCount)
|
||||
logp("updated agvpn4 with %d IPs (direct + static)", directIPCount)
|
||||
logp("updated agvpn_dyn4 with %d IPs (wildcard, source=%s)", wildcardIPCount, wildcardSource)
|
||||
logWildcardSmartDNSTrace(mode, wildcardSource, resJob.WildcardIPMap, wildcardIPCount)
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// artifacts + status
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
// copy artifacts
|
||||
_ = copyFile(ipTmp.Name(), lastIPsPath)
|
||||
_ = copyFile(ipMapTmp.Name(), lastIPsMapPath)
|
||||
_ = copyFile(ipDirectTmp.Name(), lastIPsDirect)
|
||||
_ = copyFile(ipDynTmp.Name(), lastIPsDyn)
|
||||
_ = copyFile(ipMapDirectTmp.Name(), lastIPsMapDirect)
|
||||
_ = copyFile(ipMapDynTmp.Name(), lastIPsMapDyn)
|
||||
|
||||
now := time.Now().Format(time.RFC3339)
|
||||
status := Status{
|
||||
Timestamp: now,
|
||||
IPCount: ipCount,
|
||||
DomainCount: domainCount,
|
||||
Iface: iface,
|
||||
Table: routesTableName(),
|
||||
Mark: MARK,
|
||||
}
|
||||
statusData, _ := json.MarshalIndent(status, "", " ")
|
||||
_ = os.WriteFile(statusFilePath, statusData, 0o644)
|
||||
|
||||
chownDev(
|
||||
traceLogPath,
|
||||
ipTmp.Name(), ipMapTmp.Name(),
|
||||
ipDirectTmp.Name(), ipDynTmp.Name(), ipMapDirectTmp.Name(), ipMapDynTmp.Name(),
|
||||
lastIPsPath, lastIPsMapPath, lastIPsDirect, lastIPsDyn, lastIPsMapDirect, lastIPsMapDyn,
|
||||
statusFilePath,
|
||||
heartbeatFile,
|
||||
)
|
||||
chmodPaths(
|
||||
0o644,
|
||||
ipTmp.Name(), ipMapTmp.Name(),
|
||||
ipDirectTmp.Name(), ipDynTmp.Name(), ipMapDirectTmp.Name(), ipMapDynTmp.Name(),
|
||||
lastIPsPath, lastIPsMapPath, lastIPsDirect, lastIPsDyn, lastIPsMapDirect, lastIPsMapDyn,
|
||||
statusFilePath,
|
||||
heartbeatFile,
|
||||
)
|
||||
_ = os.Chmod(traceLogPath, 0o666)
|
||||
_ = os.Chmod(stateDir, 0o755)
|
||||
|
||||
routesUpdateStageNftBase(iface)
|
||||
heartbeat()
|
||||
|
||||
resolveStage, err := routesUpdateStageResolve(logp, heartbeat)
|
||||
if err != nil {
|
||||
res.Message = err.Error()
|
||||
return res
|
||||
}
|
||||
defer resolveStage.cleanup()
|
||||
|
||||
if err := routesUpdateStagePopulateNFT(resolveStage, logp, heartbeat); err != nil {
|
||||
res.Message = err.Error()
|
||||
return res
|
||||
}
|
||||
|
||||
if err := routesUpdateStageArtifacts(iface, resolveStage, heartbeat); err != nil {
|
||||
res.Message = err.Error()
|
||||
return res
|
||||
}
|
||||
|
||||
res.OK = true
|
||||
res.Message = fmt.Sprintf("update done: domains=%d unique_ips=%d direct_ips=%d wildcard_ips=%d", len(domains), ipCount, directIPCount, wildcardIPCount)
|
||||
res.ExitCode = ipCount
|
||||
res.Message = fmt.Sprintf(
|
||||
"update done: domains=%d unique_ips=%d direct_ips=%d wildcard_ips=%d",
|
||||
len(resolveStage.domains),
|
||||
resolveStage.ipCount,
|
||||
resolveStage.directIPCount,
|
||||
resolveStage.wildcardIPCount,
|
||||
)
|
||||
res.ExitCode = resolveStage.ipCount
|
||||
return res
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// routesUpdate helpers: table / list / counters
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
func routesTableName() string { return "agvpn" }
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// EN: `routesTableNum` contains core logic for routes table num.
|
||||
// RU: `routesTableNum` - содержит основную логику для routes table num.
|
||||
// ---------------------------------------------------------------------
|
||||
func routesTableNum() string { return "666" }
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// EN: `loadList` loads list from storage or config.
|
||||
// RU: `loadList` - загружает list из хранилища или конфига.
|
||||
// ---------------------------------------------------------------------
|
||||
func loadList(path string) []string {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var out []string
|
||||
for _, ln := range strings.Split(string(data), "\n") {
|
||||
ln = strings.TrimSpace(strings.SplitN(ln, "#", 2)[0])
|
||||
if ln == "" {
|
||||
continue
|
||||
}
|
||||
out = append(out, ln)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// EN: `loadSmartDNSWildcardDomains` loads SmartDNS wildcard domains from canonical API state.
|
||||
// RU: `loadSmartDNSWildcardDomains` - загружает wildcard-домены SmartDNS из каноничного API-состояния.
|
||||
// ---------------------------------------------------------------------
|
||||
func loadSmartDNSWildcardDomains(logf func(string, ...any)) []string {
|
||||
out, source := loadSmartDNSWildcardDomainsState(logf)
|
||||
sort.Strings(out)
|
||||
if logf != nil {
|
||||
logf("smartdns wildcards loaded: source=%s count=%d", source, len(out))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// EN: `isGoogleLike` checks whether google like is true.
|
||||
// RU: `isGoogleLike` - проверяет, является ли google like истинным условием.
|
||||
// ---------------------------------------------------------------------
|
||||
func isGoogleLike(d string) bool {
|
||||
low := strings.ToLower(d)
|
||||
for _, base := range googleLikeDomains {
|
||||
if low == base || strings.HasSuffix(low, "."+base) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// EN: `readNonEmptyLines` reads non empty lines from input data.
|
||||
// RU: `readNonEmptyLines` - читает non empty lines из входных данных.
|
||||
// ---------------------------------------------------------------------
|
||||
func readNonEmptyLines(path string) []string {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var out []string
|
||||
for _, ln := range strings.Split(string(data), "\n") {
|
||||
ln = strings.TrimSpace(ln)
|
||||
if ln != "" {
|
||||
out = append(out, ln)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func writeLines(path string, lines []string) error {
|
||||
if len(lines) == 0 {
|
||||
return os.WriteFile(path, []byte{}, 0o644)
|
||||
}
|
||||
return os.WriteFile(path, []byte(strings.Join(lines, "\n")+"\n"), 0o644)
|
||||
}
|
||||
|
||||
func writeMapPairs(path string, pairs [][2]string) error {
|
||||
if len(pairs) == 0 {
|
||||
return os.WriteFile(path, []byte{}, 0o644)
|
||||
}
|
||||
lines := make([]string, 0, len(pairs))
|
||||
for _, p := range pairs {
|
||||
lines = append(lines, p[0]+"\t"+p[1])
|
||||
}
|
||||
return os.WriteFile(path, []byte(strings.Join(lines, "\n")+"\n"), 0o644)
|
||||
}
|
||||
|
||||
func countDomainsFromPairs(pairs [][2]string) int {
|
||||
seen := make(map[string]struct{})
|
||||
for _, p := range pairs {
|
||||
if len(p) < 2 {
|
||||
continue
|
||||
}
|
||||
d := strings.TrimSpace(p[1])
|
||||
if d == "" || strings.HasPrefix(d, "[") {
|
||||
continue
|
||||
}
|
||||
seen[d] = struct{}{}
|
||||
}
|
||||
return len(seen)
|
||||
}
|
||||
|
||||
func wildcardHostIPMap(pairs [][2]string) map[string][]string {
|
||||
hostToIPs := make(map[string]map[string]struct{})
|
||||
for _, p := range pairs {
|
||||
if len(p) < 2 {
|
||||
continue
|
||||
}
|
||||
ip := strings.TrimSpace(p[0])
|
||||
host := strings.TrimSpace(p[1])
|
||||
if ip == "" || host == "" || strings.HasPrefix(host, "[") {
|
||||
continue
|
||||
}
|
||||
ips := hostToIPs[host]
|
||||
if ips == nil {
|
||||
ips = map[string]struct{}{}
|
||||
hostToIPs[host] = ips
|
||||
}
|
||||
ips[ip] = struct{}{}
|
||||
}
|
||||
|
||||
out := make(map[string][]string, len(hostToIPs))
|
||||
for host, ipset := range hostToIPs {
|
||||
ips := make([]string, 0, len(ipset))
|
||||
for ip := range ipset {
|
||||
ips = append(ips, ip)
|
||||
}
|
||||
sort.Strings(ips)
|
||||
out[host] = ips
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func logWildcardSmartDNSTrace(mode DNSMode, source string, pairs [][2]string, wildcardIPCount int) {
|
||||
lowMode := strings.ToLower(strings.TrimSpace(string(mode.Mode)))
|
||||
if lowMode != string(DNSModeHybridWildcard) && lowMode != string(DNSModeSmartDNS) {
|
||||
return
|
||||
}
|
||||
|
||||
hostMap := wildcardHostIPMap(pairs)
|
||||
hosts := make([]string, 0, len(hostMap))
|
||||
for host := range hostMap {
|
||||
hosts = append(hosts, host)
|
||||
}
|
||||
sort.Strings(hosts)
|
||||
|
||||
const maxHostsLog = 200
|
||||
omitted := 0
|
||||
if len(hosts) > maxHostsLog {
|
||||
omitted = len(hosts) - maxHostsLog
|
||||
}
|
||||
|
||||
appendTraceLineTo(
|
||||
smartdnsLogPath,
|
||||
"smartdns",
|
||||
fmt.Sprintf(
|
||||
"wildcard sync: mode=%s source=%s domains=%d ips=%d logged=%d omitted=%d map=%s",
|
||||
mode.Mode, source, len(hosts), wildcardIPCount, len(hosts)-omitted, omitted, lastIPsMapDyn,
|
||||
),
|
||||
)
|
||||
|
||||
for i, host := range hosts {
|
||||
if i >= maxHostsLog {
|
||||
appendTraceLineTo(
|
||||
smartdnsLogPath,
|
||||
"smartdns",
|
||||
fmt.Sprintf("wildcard sync: trace truncated, %d domains not shown (see %s)", omitted, lastIPsMapDyn),
|
||||
)
|
||||
return
|
||||
}
|
||||
appendTraceLineTo(
|
||||
smartdnsLogPath,
|
||||
"smartdns",
|
||||
fmt.Sprintf("wildcard add: %s -> %s", host, strings.Join(hostMap[host], ", ")),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// EN: `countDomainsFromMap` counts items for domains from map.
|
||||
// RU: `countDomainsFromMap` - считает элементы для domains from map.
|
||||
// ---------------------------------------------------------------------
|
||||
func countDomainsFromMap(path string) int {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
seen := make(map[string]struct{})
|
||||
for _, ln := range strings.Split(string(data), "\n") {
|
||||
ln = strings.TrimSpace(ln)
|
||||
if ln == "" {
|
||||
continue
|
||||
}
|
||||
fields := strings.Fields(ln)
|
||||
if len(fields) < 2 {
|
||||
continue
|
||||
}
|
||||
d := fields[1]
|
||||
if strings.HasPrefix(d, "[") {
|
||||
continue
|
||||
}
|
||||
seen[d] = struct{}{}
|
||||
}
|
||||
return len(seen)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// filesystem helpers
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
func copyFile(src, dst string) error {
|
||||
data, err := os.ReadFile(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(dst, data, 0o644)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// EN: `chownDev` contains core logic for chown dev.
|
||||
// RU: `chownDev` - содержит основную логику для chown dev.
|
||||
// ---------------------------------------------------------------------
|
||||
func chownDev(paths ...string) {
|
||||
usr, err := user.Lookup("dev")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
uid, _ := strconv.Atoi(usr.Uid)
|
||||
gid, _ := strconv.Atoi(usr.Gid)
|
||||
for _, p := range paths {
|
||||
_ = os.Chown(p, uid, gid)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// EN: `chmodPaths` contains core logic for chmod paths.
|
||||
// RU: `chmodPaths` - содержит основную логику для chmod paths.
|
||||
// ---------------------------------------------------------------------
|
||||
func chmodPaths(mode fs.FileMode, paths ...string) {
|
||||
for _, p := range paths {
|
||||
_ = os.Chmod(p, mode)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// readiness helpers
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
func waitDNS(attempts int, delay time.Duration) error {
|
||||
target := "openai.com"
|
||||
for i := 0; i < attempts; i++ {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
_, err := net.DefaultResolver.LookupHost(ctx, target)
|
||||
cancel()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
time.Sleep(delay)
|
||||
}
|
||||
return fmt.Errorf("dns lookup failed after %d attempts", attempts)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user