From 036bb842fd12ff1e7bd5570743bd802405a09a99 Mon Sep 17 00:00:00 2001 From: aiccountant-bot Date: Sun, 25 Jan 2026 15:36:32 +0700 Subject: [PATCH] feat(tailscale): add socket option for userspace tailscaled --- src/config/types.gateway.ts | 2 ++ src/config/zod-schema.ts | 1 + src/gateway/server-tailscale.ts | 13 +++++---- src/gateway/server.impl.ts | 1 + src/infra/tailscale.ts | 49 ++++++++++++++++++++++++++------- 5 files changed, 51 insertions(+), 15 deletions(-) diff --git a/src/config/types.gateway.ts b/src/config/types.gateway.ts index 61c0d6f06..52a325e75 100644 --- a/src/config/types.gateway.ts +++ b/src/config/types.gateway.ts @@ -75,6 +75,8 @@ export type GatewayTailscaleConfig = { mode?: GatewayTailscaleMode; /** Reset serve/funnel configuration on shutdown. */ resetOnExit?: boolean; + /** Path to tailscaled socket (for userspace Tailscale). */ + socket?: string; }; export type GatewayRemoteConfig = { diff --git a/src/config/zod-schema.ts b/src/config/zod-schema.ts index a8ea7e70f..e7215d27e 100644 --- a/src/config/zod-schema.ts +++ b/src/config/zod-schema.ts @@ -329,6 +329,7 @@ export const ClawdbotSchema = z .object({ mode: z.union([z.literal("off"), z.literal("serve"), z.literal("funnel")]).optional(), resetOnExit: z.boolean().optional(), + socket: z.string().optional(), }) .strict() .optional(), diff --git a/src/gateway/server-tailscale.ts b/src/gateway/server-tailscale.ts index 3dbdddb56..2829eaf64 100644 --- a/src/gateway/server-tailscale.ts +++ b/src/gateway/server-tailscale.ts @@ -9,6 +9,7 @@ import { export async function startGatewayTailscaleExposure(params: { tailscaleMode: "off" | "serve" | "funnel"; resetOnExit?: boolean; + socket?: string; port: number; controlUiBasePath?: string; logTailscale: { info: (msg: string) => void; warn: (msg: string) => void }; @@ -17,13 +18,15 @@ export async function startGatewayTailscaleExposure(params: { return null; } + const socketOpts = params.socket ? { socket: params.socket } : undefined; + try { if (params.tailscaleMode === "serve") { - await enableTailscaleServe(params.port); + await enableTailscaleServe(params.port, undefined, socketOpts); } else { - await enableTailscaleFunnel(params.port); + await enableTailscaleFunnel(params.port, undefined, socketOpts); } - const host = await getTailnetHostname().catch(() => null); + const host = await getTailnetHostname(undefined, undefined, socketOpts).catch(() => null); if (host) { const uiPath = params.controlUiBasePath ? `${params.controlUiBasePath}/` : "/"; params.logTailscale.info( @@ -45,9 +48,9 @@ export async function startGatewayTailscaleExposure(params: { return async () => { try { if (params.tailscaleMode === "serve") { - await disableTailscaleServe(); + await disableTailscaleServe(undefined, socketOpts); } else { - await disableTailscaleFunnel(); + await disableTailscaleFunnel(undefined, socketOpts); } } catch (err) { params.logTailscale.warn( diff --git a/src/gateway/server.impl.ts b/src/gateway/server.impl.ts index c89e0d699..9640314ed 100644 --- a/src/gateway/server.impl.ts +++ b/src/gateway/server.impl.ts @@ -477,6 +477,7 @@ export async function startGatewayServer( const tailscaleCleanup = await startGatewayTailscaleExposure({ tailscaleMode, resetOnExit: tailscaleConfig.resetOnExit, + socket: tailscaleConfig.socket, port, controlUiBasePath, logTailscale, diff --git a/src/infra/tailscale.ts b/src/infra/tailscale.ts index 8ff340184..840780428 100644 --- a/src/infra/tailscale.ts +++ b/src/infra/tailscale.ts @@ -101,7 +101,11 @@ export async function findTailscaleBinary(): Promise { return null; } -export async function getTailnetHostname(exec: typeof runExec = runExec, detectedBinary?: string) { +export async function getTailnetHostname( + exec: typeof runExec = runExec, + detectedBinary?: string, + opts?: { socket?: string }, +) { // Derive tailnet hostname (or IP fallback) from tailscale status JSON. const candidates = detectedBinary ? [detectedBinary] @@ -111,7 +115,10 @@ export async function getTailnetHostname(exec: typeof runExec = runExec, detecte for (const candidate of candidates) { if (candidate.startsWith("/") && !existsSync(candidate)) continue; try { - const { stdout } = await exec(candidate, ["status", "--json"], { + const args = opts?.socket + ? ["--socket", opts.socket, "status", "--json"] + : ["status", "--json"]; + const { stdout } = await exec(candidate, args, { timeoutMs: 5000, maxBuffer: 400_000, }); @@ -350,33 +357,55 @@ export async function ensureFunnel( } } -export async function enableTailscaleServe(port: number, exec: typeof runExec = runExec) { +export async function enableTailscaleServe( + port: number, + exec: typeof runExec = runExec, + opts?: { socket?: string }, +) { const tailscaleBin = await getTailscaleBinary(); - await execWithSudoFallback(exec, tailscaleBin, ["serve", "--bg", "--yes", `${port}`], { + const args = opts?.socket + ? ["--socket", opts.socket, "serve", "--bg", "--yes", `${port}`] + : ["serve", "--bg", "--yes", `${port}`]; + await execWithSudoFallback(exec, tailscaleBin, args, { maxBuffer: 200_000, timeoutMs: 15_000, }); } -export async function disableTailscaleServe(exec: typeof runExec = runExec) { +export async function disableTailscaleServe( + exec: typeof runExec = runExec, + opts?: { socket?: string }, +) { const tailscaleBin = await getTailscaleBinary(); - await execWithSudoFallback(exec, tailscaleBin, ["serve", "reset"], { + const args = opts?.socket ? ["--socket", opts.socket, "serve", "reset"] : ["serve", "reset"]; + await execWithSudoFallback(exec, tailscaleBin, args, { maxBuffer: 200_000, timeoutMs: 15_000, }); } -export async function enableTailscaleFunnel(port: number, exec: typeof runExec = runExec) { +export async function enableTailscaleFunnel( + port: number, + exec: typeof runExec = runExec, + opts?: { socket?: string }, +) { const tailscaleBin = await getTailscaleBinary(); - await execWithSudoFallback(exec, tailscaleBin, ["funnel", "--bg", "--yes", `${port}`], { + const args = opts?.socket + ? ["--socket", opts.socket, "funnel", "--bg", "--yes", `${port}`] + : ["funnel", "--bg", "--yes", `${port}`]; + await execWithSudoFallback(exec, tailscaleBin, args, { maxBuffer: 200_000, timeoutMs: 15_000, }); } -export async function disableTailscaleFunnel(exec: typeof runExec = runExec) { +export async function disableTailscaleFunnel( + exec: typeof runExec = runExec, + opts?: { socket?: string }, +) { const tailscaleBin = await getTailscaleBinary(); - await execWithSudoFallback(exec, tailscaleBin, ["funnel", "reset"], { + const args = opts?.socket ? ["--socket", opts.socket, "funnel", "reset"] : ["funnel", "reset"]; + await execWithSudoFallback(exec, tailscaleBin, args, { maxBuffer: 200_000, timeoutMs: 15_000, });