diff --git a/src/config/types.gateway.ts b/src/config/types.gateway.ts index d7943acce..52dff6a91 100644 --- a/src/config/types.gateway.ts +++ b/src/config/types.gateway.ts @@ -92,6 +92,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 961ba8ecb..52ea9ac49 100644 --- a/src/config/zod-schema.ts +++ b/src/config/zod-schema.ts @@ -337,6 +337,7 @@ export const OpenClawSchema = 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 efa91be76..cdf567741 100644 --- a/src/gateway/server.impl.ts +++ b/src/gateway/server.impl.ts @@ -488,6 +488,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 abbb561fa..9ab2e8d26 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, }); @@ -362,33 +369,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, });