Files

147 lines
4.6 KiB
Go

package app
import (
"fmt"
"log"
"os"
"strings"
"time"
)
// ---------------------------------------------------------------------
// autoloop
// ---------------------------------------------------------------------
// EN: Long-running VPN autoloop worker that keeps the tunnel connected,
// EN: updates login/license state, enforces policy route defaults, and emits events.
// RU: Долгоживущий воркер VPN autoloop, который поддерживает соединение,
// RU: обновляет login/license state, чинит policy route и публикует события.
func runAutoloop(iface, table string, mtu int, stateDirPath, defaultLoc string) {
locFile := stateDirPath + "/adguard-location.txt"
logFile := stateDirPath + "/adguard-autoloop.log"
loginStateFile := stateDirPath + "/adguard-login.json"
licenseTTL := 3600 * time.Second
statusTimeout := 8 * time.Second
connectTimeout := 25 * time.Second
disconnectTimeout := 8 * time.Second
licenseTimeout := 10 * time.Second
lastLicense := time.Time{}
_ = os.MkdirAll(stateDirPath, 0o755)
log.Printf("autoloop: start iface=%s table=%s mtu=%d", iface, table, mtu)
logLine := func(msg string) {
line := fmt.Sprintf("%s autoloop: %s\n", time.Now().Format(time.RFC3339), msg)
f, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
if err == nil {
defer f.Close()
_, _ = f.WriteString(line)
}
fmt.Print(line)
}
writeLoginState := func(state, email, msg string) {
writeAutoloopLoginState(loginStateFile, state, email, msg)
}
fixPolicy := func() {
_, stderr, _, err := runCommandTimeout(5*time.Second,
"ip", "-4", "route", "replace",
"default", "dev", iface,
"table", table,
"mtu", fmt.Sprintf("%d", mtu),
)
if err != nil {
logLine("route: FAILED to set default dev " + iface +
" table " + table + ": " + stderr)
} else {
logLine("route: default dev " + iface + " table " + table +
" mtu " + fmt.Sprintf("%d", mtu) + " OK")
}
}
updateLoginStateFromText := func(text string) {
updateAutoloopLoginStateFromText(text, writeLoginState, logLine)
}
updateLicense := func() {
now := time.Now()
if !lastLicense.IsZero() && now.Sub(lastLicense) < licenseTTL {
return
}
lastLicense = now
out, _, _, _ := runCommandTimeout(licenseTimeout, adgvpnCLI, "license")
out = stripANSI(out)
updateLoginStateFromText(out)
}
writeLoginState("unknown", "", "not checked yet")
updateLicense()
for {
statusOut, _, exitCode, err := runCommandTimeout(statusTimeout, adgvpnCLI, "status")
statusOut = stripANSI(statusOut)
if err != nil {
logLine(fmt.Sprintf("status: ERROR exit=%d err=%v raw=%q", exitCode, err, statusOut))
}
if isAutoloopConnected(statusOut) {
logLine("status: CONNECTED; raw: " + statusOut)
fixPolicy()
updateLicense()
events.push("autoloop_status_changed", map[string]string{
"status_word": "CONNECTED",
"raw_text": statusOut,
})
time.Sleep(20 * time.Second)
continue
}
logLine("status: DISCONNECTED; raw: " + statusOut)
events.push("autoloop_status_changed", map[string]string{
"status_word": "DISCONNECTED",
"raw_text": statusOut,
})
updateLoginStateFromText(statusOut)
loc := resolveAutoloopLocationSpec(locFile, defaultLoc)
primary := strings.TrimSpace(loc.Primary)
if primary == "" {
primary = strings.TrimSpace(defaultLoc)
}
logLine("reconnecting to " + primary)
_, _, _, _ = runCommandTimeout(disconnectTimeout, adgvpnCLI, "disconnect")
connectOut, _, _, _ := runCommandTimeout(connectTimeout, adgvpnCLI, "connect", "-l", primary, "--log-to-file")
connectOut = stripANSI(connectOut)
logLine("connect raw: " + connectOut)
updateLoginStateFromText(connectOut)
if !isAutoloopConnected(connectOut) && loc.ISO != "" && !strings.EqualFold(loc.ISO, primary) {
logLine("connect fallback to ISO: " + loc.ISO)
fallbackOut, _, _, _ := runCommandTimeout(connectTimeout, adgvpnCLI, "connect", "-l", loc.ISO, "--log-to-file")
fallbackOut = stripANSI(fallbackOut)
logLine("connect fallback raw: " + fallbackOut)
updateLoginStateFromText(fallbackOut)
}
statusAfter, _, _, _ := runCommandTimeout(statusTimeout, adgvpnCLI, "status")
statusAfter = stripANSI(statusAfter)
if isAutoloopConnected(statusAfter) {
logLine("after connect: CONNECTED; raw: " + statusAfter)
fixPolicy()
updateLicense()
events.push("autoloop_status_changed", map[string]string{
"status_word": "CONNECTED",
"raw_text": statusAfter,
})
time.Sleep(20 * time.Second)
continue
}
logLine("after connect: STILL DISCONNECTED; raw: " + statusAfter)
time.Sleep(10 * time.Second)
}
}