Files
elmprodvpn/selective-vpn-api/app/routes_update.go

722 lines
24 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", ";", "}")
// 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)
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)
}