| import { loadState, generateShortId } from "../shared/state.js"; |
| import { startFunnel, stopFunnel, isTailscaleRunning, isTailscaleRunningStrict, isTailscaleLoggedIn, isTailscaleLoggedInStrict, startLogin, startDaemonWithPassword, provisionCert } from "./tailscale.js"; |
| import { waitForHealth } from "./healthCheck.js"; |
| import { getSettings, updateSettings } from "@/lib/localDb"; |
| import { getCachedPassword, loadEncryptedPassword, initDbHooks } from "@/mitm/manager"; |
|
|
| initDbHooks(getSettings, updateSettings); |
|
|
| const svc = { |
| cancelToken: { cancelled: false }, |
| spawnInProgress: false, |
| lastRestartAt: 0, |
| activeLocalPort: null, |
| }; |
|
|
| export function getTailscaleService() { return svc; } |
| export function isTailscaleReconnecting() { return svc.spawnInProgress; } |
|
|
| function throwIfCancelled(token) { |
| if (token.cancelled) throw new Error("tailscale cancelled"); |
| } |
|
|
| export async function enableTailscale(localPort = 20128) { |
| console.log(`[Tailscale] enable start (port=${localPort})`); |
| svc.cancelToken = { cancelled: false }; |
| svc.activeLocalPort = localPort; |
| svc.spawnInProgress = true; |
| const token = svc.cancelToken; |
|
|
| try { |
| const sudoPass = getCachedPassword() || await loadEncryptedPassword() || ""; |
| await startDaemonWithPassword(sudoPass); |
| console.log("[Tailscale] daemon ready"); |
| throwIfCancelled(token); |
|
|
| const existing = loadState(); |
| const shortId = existing?.shortId || generateShortId(); |
| const tsHostname = shortId; |
|
|
| const loggedIn = await isTailscaleLoggedInStrict(); |
| console.log(`[Tailscale] loggedIn=${loggedIn}`); |
| if (!loggedIn) { |
| const loginResult = await startLogin(tsHostname); |
| if (loginResult.authUrl) { |
| console.log(`[Tailscale] needs login, authUrl=${loginResult.authUrl}`); |
| return { success: false, needsLogin: true, authUrl: loginResult.authUrl }; |
| } |
| console.log("[Tailscale] login resolved alreadyLoggedIn"); |
| } |
| throwIfCancelled(token); |
|
|
| stopFunnel(); |
| let result; |
| try { |
| console.log("[Tailscale] starting funnel"); |
| result = await startFunnel(localPort); |
| } catch (e) { |
| console.error(`[Tailscale] funnel error: ${e.message}`); |
| |
| if (/NoState|unexpected state|not logged in|Logged ?out|NeedsLogin/i.test(e.message || "")) { |
| console.log("[Tailscale] retry via startLogin"); |
| const loginResult = await startLogin(tsHostname); |
| if (loginResult.authUrl) return { success: false, needsLogin: true, authUrl: loginResult.authUrl }; |
| } |
| throw e; |
| } |
| throwIfCancelled(token); |
|
|
| if (result.funnelNotEnabled) { |
| console.log(`[Tailscale] funnel not enabled, enableUrl=${result.enableUrl}`); |
| return { success: false, funnelNotEnabled: true, enableUrl: result.enableUrl }; |
| } |
|
|
| |
| if (!(await isTailscaleLoggedInStrict()) || !(await isTailscaleRunningStrict())) { |
| console.error("[Tailscale] strict probe failed (device removed?)"); |
| stopFunnel(); |
| return { success: false, error: "Tailscale not connected. Device may have been removed. Please re-login." }; |
| } |
|
|
| await updateSettings({ tailscaleEnabled: true, tailscaleUrl: result.tunnelUrl }); |
| console.log(`[Tailscale] funnel up: ${result.tunnelUrl}`); |
|
|
| |
| const hostname = new URL(result.tunnelUrl).hostname; |
| await provisionCert(hostname); |
|
|
| |
| let reachableNow = false; |
| try { |
| await waitForHealth(result.tunnelUrl, token); |
| reachableNow = true; |
| } catch (he) { |
| if (!he.message.startsWith("Health check timeout")) throw he; |
| console.warn(`[Tailscale] health check timed out, will retry via watchdog`); |
| } |
| console.log(`[Tailscale] enable success (reachable=${reachableNow})`); |
| return { success: true, tunnelUrl: result.tunnelUrl }; |
| } catch (e) { |
| console.error(`[Tailscale] enable error: ${e.message}`); |
| throw e; |
| } finally { |
| svc.spawnInProgress = false; |
| } |
| } |
|
|
| export async function disableTailscale() { |
| console.log("[Tailscale] disable"); |
| svc.cancelToken.cancelled = true; |
| stopFunnel(); |
| await updateSettings({ tailscaleEnabled: false, tailscaleUrl: "" }); |
| return { success: true }; |
| } |
|
|
| export async function getTailscaleStatus() { |
| const settings = await getSettings(); |
| const settingsEnabled = settings.tailscaleEnabled === true; |
| const tunnelUrl = settings.tailscaleUrl || ""; |
| |
| const loggedIn = settingsEnabled ? isTailscaleLoggedIn() : false; |
| const running = loggedIn ? isTailscaleRunning() : false; |
| return { |
| enabled: settingsEnabled && running, |
| settingsEnabled, |
| tunnelUrl, |
| running, |
| loggedIn |
| }; |
| } |
|
|