704 lines
23 KiB
Go
704 lines
23 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/fs"
|
|
"net"
|
|
"os"
|
|
"os/user"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// ---------------------------------------------------------------------
|
|
// основной routesUpdate
|
|
// ---------------------------------------------------------------------
|
|
|
|
// EN: Core selective-routes orchestration pipeline.
|
|
// EN: This unit prepares policy routing, nftables objects, domain expansion,
|
|
// EN: resolver execution, status artifacts, and GUI-facing progress events.
|
|
// RU: Основной orchestration-пайплайн selective-routes.
|
|
// RU: Модуль готовит policy routing, nftables-объекты, расширение доменов,
|
|
// RU: запуск резолвера, статусные артефакты и события прогресса для GUI.
|
|
|
|
// ---------------------------------------------------------------------
|
|
// EN: `routesUpdate` contains core logic for routes update.
|
|
// RU: `routesUpdate` - содержит основную логику для routes update.
|
|
// ---------------------------------------------------------------------
|
|
func routesUpdate(iface string) cmdResult {
|
|
logp := func(format string, args ...any) {
|
|
appendTraceLine("routes", fmt.Sprintf(format, args...))
|
|
}
|
|
heartbeat := func() {
|
|
_ = os.WriteFile(heartbeatFile, []byte{}, 0o644)
|
|
}
|
|
|
|
res := cmdResult{OK: false}
|
|
|
|
iface = normalizePreferredIface(iface)
|
|
if iface == "" {
|
|
iface, _ = resolveTrafficIface(loadTrafficModeState().PreferredIface)
|
|
}
|
|
if iface == "" {
|
|
logp("no active vpn iface, exit 0")
|
|
res.OK = true
|
|
res.Message = "interface not found, skipped"
|
|
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", ";", "}")
|
|
|
|
_, _, _, _ = runCommandTimeout(5*time.Second, "nft", "add", "chain", "inet", "agvpn", "output", "{", "type", "route", "hook", "output", "priority", "mangle;", "policy", "accept;", "}")
|
|
_, _, _, _ = runCommandTimeout(5*time.Second, "nft", "flush", "chain", "inet", "agvpn", "output")
|
|
_, _, _, _ = runCommandTimeout(5*time.Second, "nft", "add", "rule", "inet", "agvpn", "output", "ip", "daddr", "@agvpn4", "meta", "mark", "set", MARK)
|
|
_, _, _, _ = runCommandTimeout(5*time.Second, "nft", "add", "rule", "inet", "agvpn", "output", "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)
|
|
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{}{}
|
|
if !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)
|
|
}
|
|
|
|
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)
|
|
if err != nil {
|
|
logp("Go resolver FAILED: %v", err)
|
|
res.Message = fmt.Sprintf("resolver failed: %v", err)
|
|
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)
|
|
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)
|
|
|
|
heartbeat()
|
|
|
|
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
|
|
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)
|
|
|
|
appendTraceLineTo(
|
|
smartdnsLogPath,
|
|
"smartdns",
|
|
fmt.Sprintf("wildcard sync: mode=%s source=%s domains=%d ips=%d", mode.Mode, source, len(hosts), wildcardIPCount),
|
|
)
|
|
|
|
const maxHostsLog = 200
|
|
for i, host := range hosts {
|
|
if i >= maxHostsLog {
|
|
appendTraceLineTo(
|
|
smartdnsLogPath,
|
|
"smartdns",
|
|
fmt.Sprintf("wildcard sync: +%d domains omitted", len(hosts)-maxHostsLog),
|
|
)
|
|
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)
|
|
}
|