File size: 5,012 Bytes
88c4c60 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | 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}`);
// Daemon not logged in / not ready → auto-trigger login flow so user stays in-app
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 };
}
// Strict probe: bypass cache so we don't false-negative on first invocation
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}`);
// Provision TLS cert so Funnel can serve HTTPS (non-fatal if fails)
const hostname = new URL(result.tunnelUrl).hostname;
await provisionCert(hostname);
// Verify funnel serves /api/health — timeout is non-fatal (DNS may still be propagating)
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 || "";
// Skip probes entirely when disabled; check login before running (device removed = not logged in)
const loggedIn = settingsEnabled ? isTailscaleLoggedIn() : false;
const running = loggedIn ? isTailscaleRunning() : false;
return {
enabled: settingsEnabled && running,
settingsEnabled,
tunnelUrl,
running,
loggedIn
};
}
|