mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 13:38:31 +00:00
fix(tunnel): detect system-installed Tailscale via dual-socket probe
Tailscale installed via apt/snap/brew was reported "Not Installed/Logged In" because 9Router only probed its custom userspace socket. Probe the system socket (/var/run/tailscale/tailscaled.sock) as fallback, add /usr/sbin and /snap/bin to candidate paths and EXTENDED_PATH, and report separate customDaemonRunning/systemDaemonRunning flags. Caching/non-blocking behavior preserved. Co-authored-by: Stefan Pirker <stefan.pirker86@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
committed by
decolua
co-authored by
Cursor
parent
8e31b5ff2f
commit
24a4f0863f
@@ -2,11 +2,11 @@ import os from "os";
|
|||||||
import { exec } from "child_process";
|
import { exec } from "child_process";
|
||||||
import { promisify } from "util";
|
import { promisify } from "util";
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { isTailscaleInstalled, isTailscaleLoggedIn, TAILSCALE_SOCKET } from "@/lib/tunnel";
|
import { isTailscaleInstalled, isTailscaleLoggedIn, isSystemDaemonRunning, getTailscaleBin, TAILSCALE_SOCKET } from "@/lib/tunnel";
|
||||||
import { getCachedPassword, loadEncryptedPassword } from "@/mitm/manager";
|
import { getCachedPassword, loadEncryptedPassword } from "@/mitm/manager";
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
const EXTENDED_PATH = `/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:${process.env.PATH || ""}`;
|
const EXTENDED_PATH = `/usr/local/bin:/opt/homebrew/bin:/usr/sbin:/usr/bin:/bin:/snap/bin:${process.env.PATH || ""}`;
|
||||||
const PROBE_TIMEOUT_MS = 1500;
|
const PROBE_TIMEOUT_MS = 1500;
|
||||||
|
|
||||||
async function hasBrew() {
|
async function hasBrew() {
|
||||||
@@ -16,9 +16,11 @@ async function hasBrew() {
|
|||||||
} catch { return false; }
|
} catch { return false; }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function isDaemonRunning() {
|
async function isCustomDaemonRunning() {
|
||||||
|
const bin = getTailscaleBin();
|
||||||
|
if (!bin) return false;
|
||||||
try {
|
try {
|
||||||
await execAsync(`tailscale --socket ${TAILSCALE_SOCKET} status --json`, {
|
await execAsync(`"${bin}" --socket ${TAILSCALE_SOCKET} status --json`, {
|
||||||
windowsHide: true,
|
windowsHide: true,
|
||||||
env: { ...process.env, PATH: EXTENDED_PATH },
|
env: { ...process.env, PATH: EXTENDED_PATH },
|
||||||
timeout: PROBE_TIMEOUT_MS
|
timeout: PROBE_TIMEOUT_MS
|
||||||
@@ -37,13 +39,15 @@ export async function GET() {
|
|||||||
const installed = isTailscaleInstalled();
|
const installed = isTailscaleInstalled();
|
||||||
const platform = os.platform();
|
const platform = os.platform();
|
||||||
// Run independent probes in parallel — none blocks the event loop
|
// Run independent probes in parallel — none blocks the event loop
|
||||||
const [brewAvailable, daemonRunning] = await Promise.all([
|
const [brewAvailable, customDaemonRunning, systemDaemonRunning] = await Promise.all([
|
||||||
platform === "darwin" ? hasBrew() : Promise.resolve(false),
|
platform === "darwin" ? hasBrew() : Promise.resolve(false),
|
||||||
installed ? isDaemonRunning() : Promise.resolve(false),
|
installed ? isCustomDaemonRunning() : Promise.resolve(false),
|
||||||
|
installed ? Promise.resolve(isSystemDaemonRunning()) : Promise.resolve(false),
|
||||||
]);
|
]);
|
||||||
|
const daemonRunning = customDaemonRunning || systemDaemonRunning;
|
||||||
const loggedIn = daemonRunning ? isTailscaleLoggedIn() : false;
|
const loggedIn = daemonRunning ? isTailscaleLoggedIn() : false;
|
||||||
const hasCachedPassword = !!(getCachedPassword() || await loadEncryptedPassword());
|
const hasCachedPassword = !!(getCachedPassword() || await loadEncryptedPassword());
|
||||||
return NextResponse.json({ installed, loggedIn, platform, brewAvailable, daemonRunning, hasCachedPassword });
|
return NextResponse.json({ installed, loggedIn, platform, brewAvailable, daemonRunning, customDaemonRunning, systemDaemonRunning, hasCachedPassword });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ export {
|
|||||||
isTailscaleRunningStrict,
|
isTailscaleRunningStrict,
|
||||||
isTailscaleLoggedIn,
|
isTailscaleLoggedIn,
|
||||||
isTailscaleLoggedInStrict,
|
isTailscaleLoggedInStrict,
|
||||||
|
isSystemDaemonRunning,
|
||||||
|
getTailscaleBin,
|
||||||
installTailscale,
|
installTailscale,
|
||||||
startLogin,
|
startLogin,
|
||||||
startDaemonWithPassword,
|
startDaemonWithPassword,
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ const TAILSCALE_DIR = path.join(DATA_DIR, "tailscale");
|
|||||||
export const TAILSCALE_SOCKET = path.join(TAILSCALE_DIR, "tailscaled.sock");
|
export const TAILSCALE_SOCKET = path.join(TAILSCALE_DIR, "tailscaled.sock");
|
||||||
const SOCKET_FLAG = IS_WINDOWS ? [] : ["--socket", TAILSCALE_SOCKET];
|
const SOCKET_FLAG = IS_WINDOWS ? [] : ["--socket", TAILSCALE_SOCKET];
|
||||||
|
|
||||||
|
// System daemon socket (sudo install: apt/snap/systemd) — read-only status detection
|
||||||
|
const SYSTEM_TAILSCALE_SOCKET = IS_WINDOWS ? null : "/var/run/tailscale/tailscaled.sock";
|
||||||
|
const SYSTEM_SOCKET_FLAG = SYSTEM_TAILSCALE_SOCKET ? ["--socket", SYSTEM_TAILSCALE_SOCKET] : [];
|
||||||
|
|
||||||
// Well-known Windows install path
|
// Well-known Windows install path
|
||||||
const WINDOWS_TAILSCALE_BIN = "C:\\Program Files\\Tailscale\\tailscale.exe";
|
const WINDOWS_TAILSCALE_BIN = "C:\\Program Files\\Tailscale\\tailscale.exe";
|
||||||
|
|
||||||
@@ -27,7 +31,9 @@ const WINDOWS_TAILSCALE_BIN = "C:\\Program Files\\Tailscale\\tailscale.exe";
|
|||||||
const UNIX_TAILSCALE_CANDIDATES = [
|
const UNIX_TAILSCALE_CANDIDATES = [
|
||||||
"/usr/local/bin/tailscale",
|
"/usr/local/bin/tailscale",
|
||||||
"/opt/homebrew/bin/tailscale",
|
"/opt/homebrew/bin/tailscale",
|
||||||
|
"/usr/sbin/tailscale", // apt package on Debian/Ubuntu
|
||||||
"/usr/bin/tailscale",
|
"/usr/bin/tailscale",
|
||||||
|
"/snap/bin/tailscale", // Snap package
|
||||||
];
|
];
|
||||||
|
|
||||||
// ─── Cache + background refresh (avoid blocking event loop on dead daemon) ──
|
// ─── Cache + background refresh (avoid blocking event loop on dead daemon) ──
|
||||||
@@ -50,7 +56,7 @@ function bgRefreshBin() {
|
|||||||
if (binCache.refreshing) return;
|
if (binCache.refreshing) return;
|
||||||
binCache.refreshing = true;
|
binCache.refreshing = true;
|
||||||
const cmd = IS_WINDOWS ? "where tailscale 2>nul" : "which tailscale 2>/dev/null";
|
const cmd = IS_WINDOWS ? "where tailscale 2>nul" : "which tailscale 2>/dev/null";
|
||||||
execAsync(cmd, { windowsHide: true, timeout: PROBE_TIMEOUT_MS })
|
execAsync(cmd, { windowsHide: true, timeout: PROBE_TIMEOUT_MS, env: { ...process.env, PATH: EXTENDED_PATH } })
|
||||||
.then(({ stdout }) => {
|
.then(({ stdout }) => {
|
||||||
const sys = stdout.trim();
|
const sys = stdout.trim();
|
||||||
binCache.value = sys || fallbackBin();
|
binCache.value = sys || fallbackBin();
|
||||||
@@ -63,7 +69,7 @@ function bgRefreshBin() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Sync getter: returns cached value, triggers background refresh if stale
|
// Sync getter: returns cached value, triggers background refresh if stale
|
||||||
function getTailscaleBin() {
|
export function getTailscaleBin() {
|
||||||
if (Date.now() - binCache.fetchedAt > PROBE_TTL_MS) bgRefreshBin();
|
if (Date.now() - binCache.fetchedAt > PROBE_TTL_MS) bgRefreshBin();
|
||||||
// First call: synchronously probe common install paths (no exec, no event-loop block)
|
// First call: synchronously probe common install paths (no exec, no event-loop block)
|
||||||
if (binCache.value === undefined) {
|
if (binCache.value === undefined) {
|
||||||
@@ -116,12 +122,10 @@ function bgRefreshLoggedIn() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
loggedInCache.refreshing = true;
|
loggedInCache.refreshing = true;
|
||||||
execAsync(`"${bin}" ${SOCKET_FLAG.join(" ")} status --json`, { windowsHide: true, env: { ...process.env, PATH: EXTENDED_PATH }, timeout: PROBE_TIMEOUT_MS })
|
// Dual-socket aware: probe custom socket first, then system socket
|
||||||
.then(({ stdout }) => {
|
probeStatusAsync(bin)
|
||||||
try {
|
.then((json) => {
|
||||||
const json = JSON.parse(stdout);
|
loggedInCache.value = !!json && json.BackendState === "Running" && json.Self?.Online === true;
|
||||||
loggedInCache.value = json.BackendState === "Running" && json.Self?.Online === true;
|
|
||||||
} catch { loggedInCache.value = false; }
|
|
||||||
})
|
})
|
||||||
.catch(() => { loggedInCache.value = false; })
|
.catch(() => { loggedInCache.value = false; })
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
@@ -130,6 +134,19 @@ function bgRefreshLoggedIn() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Probe `status --json` over custom then system socket. Resolves parsed JSON or null. Never blocks event loop.
|
||||||
|
async function probeStatusAsync(bin) {
|
||||||
|
for (const socketArgs of [SOCKET_FLAG, SYSTEM_SOCKET_FLAG]) {
|
||||||
|
try {
|
||||||
|
const { stdout } = await execAsync(`"${bin}" ${socketArgs.join(" ")} status --json`, {
|
||||||
|
windowsHide: true, env: { ...process.env, PATH: EXTENDED_PATH }, timeout: PROBE_TIMEOUT_MS,
|
||||||
|
});
|
||||||
|
return JSON.parse(stdout);
|
||||||
|
} catch { /* try next socket */ }
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Sync getter: never blocks; returns last known state, refreshes in background
|
// Sync getter: never blocks; returns last known state, refreshes in background
|
||||||
export function isTailscaleLoggedIn() {
|
export function isTailscaleLoggedIn() {
|
||||||
if (Date.now() - loggedInCache.fetchedAt > PROBE_TTL_MS) bgRefreshLoggedIn();
|
if (Date.now() - loggedInCache.fetchedAt > PROBE_TTL_MS) bgRefreshLoggedIn();
|
||||||
@@ -185,6 +202,21 @@ export async function isTailscaleRunningStrict() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if a system-level tailscaled is running (uses system socket, not 9Router's custom one).
|
||||||
|
export function isSystemDaemonRunning() {
|
||||||
|
if (IS_WINDOWS || !SYSTEM_TAILSCALE_SOCKET || !fs.existsSync(SYSTEM_TAILSCALE_SOCKET)) return false;
|
||||||
|
const bin = getTailscaleBin();
|
||||||
|
if (!bin) return false;
|
||||||
|
try {
|
||||||
|
const out = execSync(`"${bin}" ${SYSTEM_SOCKET_FLAG.join(" ")} status --json`, {
|
||||||
|
encoding: "utf8", windowsHide: true, env: { ...process.env, PATH: EXTENDED_PATH }, timeout: PROBE_TIMEOUT_MS,
|
||||||
|
});
|
||||||
|
return JSON.parse(out).BackendState === "Running";
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function bgRefreshFunnelUrl(port) {
|
function bgRefreshFunnelUrl(port) {
|
||||||
if (funnelUrlCache.refreshing) return;
|
if (funnelUrlCache.refreshing) return;
|
||||||
const bin = getTailscaleBin();
|
const bin = getTailscaleBin();
|
||||||
@@ -253,7 +285,7 @@ export async function installTailscale(sudoPassword, hostname, onProgress) {
|
|||||||
return startLogin(hostname);
|
return startLogin(hostname);
|
||||||
}
|
}
|
||||||
|
|
||||||
const EXTENDED_PATH = `/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:${process.env.PATH || ""}`;
|
const EXTENDED_PATH = `/usr/local/bin:/opt/homebrew/bin:/usr/sbin:/usr/bin:/bin:/snap/bin:${process.env.PATH || ""}`;
|
||||||
|
|
||||||
function hasBrew() {
|
function hasBrew() {
|
||||||
try { execSync("which brew", { stdio: "ignore", windowsHide: true, env: { ...process.env, PATH: EXTENDED_PATH } }); return true; } catch { return false; }
|
try { execSync("which brew", { stdio: "ignore", windowsHide: true, env: { ...process.env, PATH: EXTENDED_PATH } }); return true; } catch { return false; }
|
||||||
|
|||||||
Reference in New Issue
Block a user