Feat : Tailscale

This commit is contained in:
decolua 2026-04-10 18:16:14 +07:00
parent 838d9a7a04
commit ed17a8ffac
18 changed files with 1433 additions and 388 deletions

View file

@ -0,0 +1,5 @@
import { NextResponse } from "next/server";
export async function GET() {
return NextResponse.json({ ok: true });
}

View file

@ -1,10 +1,10 @@
import { NextResponse } from "next/server";
import { getTunnelStatus } from "@/lib/tunnel/tunnelManager";
import { getTunnelStatus, getTailscaleStatus } from "@/lib/tunnel/tunnelManager";
export async function GET() {
try {
const status = await getTunnelStatus();
return NextResponse.json(status);
const [tunnel, tailscale] = await Promise.all([getTunnelStatus(), getTailscaleStatus()]);
return NextResponse.json({ tunnel, tailscale });
} catch (error) {
console.error("Tunnel status error:", error);
return NextResponse.json({ error: error.message }, { status: 500 });

View file

@ -0,0 +1,41 @@
import os from "os";
import { execSync } from "child_process";
import { NextResponse } from "next/server";
import { isTailscaleInstalled, isTailscaleLoggedIn, TAILSCALE_SOCKET } from "@/lib/tunnel/tailscale";
const EXTENDED_PATH = `/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:${process.env.PATH || ""}`;
function hasBrew() {
try { execSync("which brew", { stdio: "ignore", env: { ...process.env, PATH: EXTENDED_PATH } }); return true; } catch { return false; }
}
function isDaemonRunning() {
try {
// Use custom socket + --json; exit 0 even when not logged in
execSync(`tailscale --socket ${TAILSCALE_SOCKET} status --json`, {
stdio: "ignore",
env: { ...process.env, PATH: EXTENDED_PATH },
timeout: 3000
});
return true;
} catch {
// Fallback: check if tailscaled process is alive
try {
execSync("pgrep -x tailscaled", { stdio: "ignore", timeout: 2000 });
return true;
} catch { return false; }
}
}
export async function GET() {
try {
const installed = isTailscaleInstalled();
const platform = os.platform();
const brewAvailable = platform === "darwin" && hasBrew();
const daemonRunning = installed ? isDaemonRunning() : false;
const loggedIn = daemonRunning ? isTailscaleLoggedIn() : false;
return NextResponse.json({ installed, loggedIn, platform, brewAvailable, daemonRunning });
} catch (error) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

View file

@ -0,0 +1,12 @@
import { NextResponse } from "next/server";
import { disableTailscale } from "@/lib/tunnel/tunnelManager";
export async function POST() {
try {
const result = await disableTailscale();
return NextResponse.json(result);
} catch (error) {
console.error("Tailscale disable error:", error);
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

View file

@ -0,0 +1,12 @@
import { NextResponse } from "next/server";
import { enableTailscale } from "@/lib/tunnel/tunnelManager";
export async function POST() {
try {
const result = await enableTailscale();
return NextResponse.json(result);
} catch (error) {
console.error("Tailscale enable error:", error);
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

View file

@ -0,0 +1,67 @@
"use server";
import os from "os";
import { execSync } from "child_process";
import { installTailscale } from "@/lib/tunnel/tailscale";
import { getCachedPassword, loadEncryptedPassword, initDbHooks } from "@/mitm/manager";
import { getSettings, updateSettings } from "@/lib/localDb";
import { loadState, generateShortId } from "@/lib/tunnel/state.js";
initDbHooks(getSettings, updateSettings);
const EXTENDED_PATH = `/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:${process.env.PATH || ""}`;
function hasBrew() {
try { execSync("which brew", { stdio: "ignore", env: { ...process.env, PATH: EXTENDED_PATH } }); return true; } catch { return false; }
}
export async function POST(request) {
const body = await request.json().catch(() => ({}));
const platform = os.platform();
const isWindows = platform === "win32";
const isBrew = platform === "darwin" && hasBrew();
const needsPassword = !isWindows && !isBrew;
const sudoPassword = body.sudoPassword || getCachedPassword() || await loadEncryptedPassword() || "";
if (needsPassword && !sudoPassword.trim()) {
return new Response(JSON.stringify({ error: "Sudo password is required" }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}
const shortId = loadState()?.shortId || generateShortId();
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
const send = (event, data) => {
controller.enqueue(encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`));
};
try {
const result = await installTailscale(sudoPassword, shortId, (msg) => {
send("progress", { message: msg });
});
send("done", { success: true, authUrl: result?.authUrl || null });
} catch (error) {
console.error("Tailscale install error:", error);
const msg = error.message?.includes("incorrect password") || error.message?.includes("Sorry")
? "Wrong sudo password"
: error.message;
send("error", { error: msg });
} finally {
controller.close();
}
},
});
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
},
});
}

View file

@ -0,0 +1,14 @@
import { NextResponse } from "next/server";
import { startLogin } from "@/lib/tunnel/tailscale";
import { loadState, generateShortId } from "@/lib/tunnel/state.js";
export async function POST() {
try {
const shortId = loadState()?.shortId || generateShortId();
const result = await startLogin(shortId);
return NextResponse.json(result);
} catch (error) {
console.error("Tailscale login error:", error);
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

View file

@ -0,0 +1,21 @@
"use server";
import { NextResponse } from "next/server";
import { startDaemonWithPassword } from "@/lib/tunnel/tailscale";
import { getCachedPassword, loadEncryptedPassword, initDbHooks } from "@/mitm/manager";
import { getSettings, updateSettings } from "@/lib/localDb";
initDbHooks(getSettings, updateSettings);
export async function POST(request) {
try {
const body = await request.json().catch(() => ({}));
// Use provided password, or fall back to cached/stored MITM password
const password = body.sudoPassword || getCachedPassword() || await loadEncryptedPassword() || "";
await startDaemonWithPassword(password);
return NextResponse.json({ success: true });
} catch (error) {
console.error("Tailscale start daemon error:", error);
return NextResponse.json({ error: error.message }, { status: 500 });
}
}