package app import ( "context" "fmt" "strings" "time" ) func restoreRoutesFromCache() cmdResult { return withRoutesOpLock("routes restore", restoreRoutesFromCacheUnlocked) } func restoreRoutesFromCacheUnlocked() cmdResult { meta, err := loadRoutesClearCacheMeta() if err != nil { return cmdResult{ OK: false, Message: fmt.Sprintf("routes cache missing: %v", err), } } ips := readNonEmptyLines(routesCacheIPs) dynIPs := readNonEmptyLines(routesCacheDyn) routeLines, _ := readLinesFile(routesCacheRT) ensureRoutesTableEntry() removeTrafficRulesForTable() _, _, _, _ = runCommandTimeout(5*time.Second, "ip", "route", "flush", "table", routesTableName()) ignoredRoutes := 0 for _, ln := range routeLines { if err := restoreRouteLine(ln); err != nil { if shouldIgnoreRestoreRouteError(ln, err) { ignoredRoutes++ appendTraceLine("routes", fmt.Sprintf("restore route skipped (%q): %v", ln, err)) continue } return cmdResult{ OK: false, Message: fmt.Sprintf("restore route failed (%q): %v", ln, err), } } } if ignoredRoutes > 0 { appendTraceLine("routes", fmt.Sprintf("restore route: skipped non-critical routes=%d", ignoredRoutes)) } if len(routeLines) == 0 && strings.TrimSpace(meta.Iface) != "" { _, _, _, _ = runCommandTimeout( 5*time.Second, "ip", "-4", "route", "replace", "default", "dev", meta.Iface, "table", routesTableName(), "mtu", policyRouteMTU, ) } _, _, _, _ = 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", "flush", "set", "inet", "agvpn", "agvpn4") _, _, _, _ = runCommandTimeout(5*time.Second, "nft", "flush", "set", "inet", "agvpn", "agvpn_dyn4") ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) defer cancel() if len(ips) > 0 { if err := nftUpdateSetIPsSmart(ctx, "agvpn4", ips, nil); err != nil { return cmdResult{ OK: false, Message: fmt.Sprintf("restore nft cache failed for agvpn4: %v", err), } } } if len(dynIPs) > 0 { if err := nftUpdateSetIPsSmart(ctx, "agvpn_dyn4", dynIPs, nil); err != nil { return cmdResult{ OK: false, Message: fmt.Sprintf("restore nft cache failed for agvpn_dyn4: %v", err), } } } traffic := loadTrafficModeState() iface := strings.TrimSpace(meta.Iface) if iface == "" { iface = detectIfaceFromRoutes(routeLines) } if iface == "" { iface, _ = resolveTrafficIface(traffic.PreferredIface) } if iface != "" { if err := applyTrafficMode(traffic, iface); err != nil { return cmdResult{ OK: false, Message: fmt.Sprintf("cache restored, but traffic mode apply failed: %v", err), } } } _ = cacheCopyOrEmpty(routesCacheIPs, stateDir+"/last-ips.txt") if fileExists(routesCacheMap) { _ = cacheCopyOrEmpty(routesCacheMap, stateDir+"/last-ips-map.txt") } if fileExists(routesCacheMapD) { _ = cacheCopyOrEmpty(routesCacheMapD, lastIPsMapDirect) } if fileExists(routesCacheMapW) { _ = cacheCopyOrEmpty(routesCacheMapW, lastIPsMapDyn) } _ = writeStatusSnapshot(len(ips)+len(dynIPs), iface) return cmdResult{ OK: true, Message: fmt.Sprintf( "routes restored from cache: agvpn4=%d agvpn_dyn4=%d routes=%d iface=%s", len(ips), len(dynIPs), len(routeLines), ifaceOrDash(iface), ), } }