feat(tailscale): add socket option for userspace tailscaled

This commit is contained in:
aiccountant-bot 2026-01-25 15:36:32 +07:00
parent c6cdbb630c
commit 036bb842fd
5 changed files with 51 additions and 15 deletions

View File

@ -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 = {

View File

@ -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(),

View File

@ -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(

View File

@ -477,6 +477,7 @@ export async function startGatewayServer(
const tailscaleCleanup = await startGatewayTailscaleExposure({
tailscaleMode,
resetOnExit: tailscaleConfig.resetOnExit,
socket: tailscaleConfig.socket,
port,
controlUiBasePath,
logTailscale,

View File

@ -101,7 +101,11 @@ export async function findTailscaleBinary(): Promise<string | null> {
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,
});