feat: use sudo for tailscale configuration commands
To avoid permission denied errors when modifying Tailscale configuration (serve/funnel), we now prepend `sudo -n` to these commands. This ensures that if the user has appropriate sudo privileges (specifically passwordless for these commands or generally), the operation succeeds. If sudo fails (e.g. requires password non-interactively), it will throw an error which is caught and logged as a warning, preserving existing behavior but attempting to escalate privileges first. - Updated `ensureFunnel` to use `sudo -n` for the enabling step. - Updated `enableTailscaleServe`, `disableTailscaleServe`, `enableTailscaleFunnel`, `disableTailscaleFunnel` to use `sudo -n`. - Added tests in `src/infra/tailscale.test.ts` to verify `sudo` usage.
This commit is contained in:
committed by
Peter Steinberger
parent
511a0c22b7
commit
66f353fe7a
@@ -1,6 +1,15 @@
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import { ensureGoInstalled, ensureTailscaledInstalled, getTailnetHostname } from "./tailscale.js";
|
import {
|
||||||
|
ensureGoInstalled,
|
||||||
|
ensureTailscaledInstalled,
|
||||||
|
getTailnetHostname,
|
||||||
|
enableTailscaleServe,
|
||||||
|
disableTailscaleServe,
|
||||||
|
enableTailscaleFunnel,
|
||||||
|
disableTailscaleFunnel,
|
||||||
|
ensureFunnel
|
||||||
|
} from "./tailscale.js";
|
||||||
|
|
||||||
describe("tailscale helpers", () => {
|
describe("tailscale helpers", () => {
|
||||||
it("parses DNS name from tailscale status", async () => {
|
it("parses DNS name from tailscale status", async () => {
|
||||||
@@ -48,4 +57,75 @@ describe("tailscale helpers", () => {
|
|||||||
await ensureTailscaledInstalled(exec as never, prompt, runtime);
|
await ensureTailscaledInstalled(exec as never, prompt, runtime);
|
||||||
expect(exec).toHaveBeenCalledWith("brew", ["install", "tailscale"]);
|
expect(exec).toHaveBeenCalledWith("brew", ["install", "tailscale"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("enableTailscaleServe uses sudo", async () => {
|
||||||
|
const exec = vi.fn().mockResolvedValue({ stdout: "" });
|
||||||
|
await enableTailscaleServe(3000, exec as never);
|
||||||
|
expect(exec).toHaveBeenCalledWith(
|
||||||
|
"sudo",
|
||||||
|
expect.arrayContaining(["-n", "tailscale", "serve", "--bg", "--yes", "3000"]),
|
||||||
|
expect.any(Object)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("disableTailscaleServe uses sudo", async () => {
|
||||||
|
const exec = vi.fn().mockResolvedValue({ stdout: "" });
|
||||||
|
await disableTailscaleServe(exec as never);
|
||||||
|
expect(exec).toHaveBeenCalledWith(
|
||||||
|
"sudo",
|
||||||
|
expect.arrayContaining(["-n", "tailscale", "serve", "reset"]),
|
||||||
|
expect.any(Object)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("enableTailscaleFunnel uses sudo", async () => {
|
||||||
|
const exec = vi.fn().mockResolvedValue({ stdout: "" });
|
||||||
|
await enableTailscaleFunnel(4000, exec as never);
|
||||||
|
expect(exec).toHaveBeenCalledWith(
|
||||||
|
"sudo",
|
||||||
|
expect.arrayContaining(["-n", "tailscale", "funnel", "--bg", "--yes", "4000"]),
|
||||||
|
expect.any(Object)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("disableTailscaleFunnel uses sudo", async () => {
|
||||||
|
const exec = vi.fn().mockResolvedValue({ stdout: "" });
|
||||||
|
await disableTailscaleFunnel(exec as never);
|
||||||
|
expect(exec).toHaveBeenCalledWith(
|
||||||
|
"sudo",
|
||||||
|
expect.arrayContaining(["-n", "tailscale", "funnel", "reset"]),
|
||||||
|
expect.any(Object)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ensureFunnel uses sudo for enabling", async () => {
|
||||||
|
// Mock exec: first call is status (not sudo), second call is enable (sudo)
|
||||||
|
const exec = vi.fn()
|
||||||
|
.mockResolvedValueOnce({ stdout: JSON.stringify({ BackendState: "Running" }) }) // status
|
||||||
|
.mockResolvedValueOnce({ stdout: "" }); // enable
|
||||||
|
|
||||||
|
const runtime = {
|
||||||
|
error: vi.fn(),
|
||||||
|
log: vi.fn(),
|
||||||
|
exit: vi.fn() as unknown as (code: number) => never,
|
||||||
|
};
|
||||||
|
const prompt = vi.fn();
|
||||||
|
|
||||||
|
await ensureFunnel(8080, exec as never, runtime, prompt);
|
||||||
|
|
||||||
|
// First call: check status (no sudo)
|
||||||
|
expect(exec).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
"tailscale",
|
||||||
|
expect.arrayContaining(["funnel", "status", "--json"])
|
||||||
|
);
|
||||||
|
|
||||||
|
// Second call: enable (sudo)
|
||||||
|
expect(exec).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
"sudo",
|
||||||
|
expect.arrayContaining(["-n", "tailscale", "funnel", "--yes", "--bg", "8080"]),
|
||||||
|
expect.any(Object)
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -237,7 +237,7 @@ export async function ensureFunnel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
logVerbose(`Enabling funnel on port ${port}…`);
|
logVerbose(`Enabling funnel on port ${port}…`);
|
||||||
const { stdout } = await exec(tailscaleBin, ["funnel", "--yes", "--bg", `${port}`], {
|
const { stdout } = await exec("sudo", ["-n", tailscaleBin, "funnel", "--yes", "--bg", `${port}`], {
|
||||||
maxBuffer: 200_000,
|
maxBuffer: 200_000,
|
||||||
timeoutMs: 15_000,
|
timeoutMs: 15_000,
|
||||||
});
|
});
|
||||||
@@ -288,7 +288,7 @@ export async function ensureFunnel(
|
|||||||
|
|
||||||
export async function enableTailscaleServe(port: number, exec: typeof runExec = runExec) {
|
export async function enableTailscaleServe(port: number, exec: typeof runExec = runExec) {
|
||||||
const tailscaleBin = await getTailscaleBinary();
|
const tailscaleBin = await getTailscaleBinary();
|
||||||
await exec(tailscaleBin, ["serve", "--bg", "--yes", `${port}`], {
|
await exec("sudo", ["-n", tailscaleBin, "serve", "--bg", "--yes", `${port}`], {
|
||||||
maxBuffer: 200_000,
|
maxBuffer: 200_000,
|
||||||
timeoutMs: 15_000,
|
timeoutMs: 15_000,
|
||||||
});
|
});
|
||||||
@@ -296,7 +296,7 @@ export async function enableTailscaleServe(port: number, exec: typeof runExec =
|
|||||||
|
|
||||||
export async function disableTailscaleServe(exec: typeof runExec = runExec) {
|
export async function disableTailscaleServe(exec: typeof runExec = runExec) {
|
||||||
const tailscaleBin = await getTailscaleBinary();
|
const tailscaleBin = await getTailscaleBinary();
|
||||||
await exec(tailscaleBin, ["serve", "reset"], {
|
await exec("sudo", ["-n", tailscaleBin, "serve", "reset"], {
|
||||||
maxBuffer: 200_000,
|
maxBuffer: 200_000,
|
||||||
timeoutMs: 15_000,
|
timeoutMs: 15_000,
|
||||||
});
|
});
|
||||||
@@ -304,7 +304,7 @@ export async function disableTailscaleServe(exec: typeof runExec = runExec) {
|
|||||||
|
|
||||||
export async function enableTailscaleFunnel(port: number, exec: typeof runExec = runExec) {
|
export async function enableTailscaleFunnel(port: number, exec: typeof runExec = runExec) {
|
||||||
const tailscaleBin = await getTailscaleBinary();
|
const tailscaleBin = await getTailscaleBinary();
|
||||||
await exec(tailscaleBin, ["funnel", "--bg", "--yes", `${port}`], {
|
await exec("sudo", ["-n", tailscaleBin, "funnel", "--bg", "--yes", `${port}`], {
|
||||||
maxBuffer: 200_000,
|
maxBuffer: 200_000,
|
||||||
timeoutMs: 15_000,
|
timeoutMs: 15_000,
|
||||||
});
|
});
|
||||||
@@ -312,7 +312,7 @@ export async function enableTailscaleFunnel(port: number, exec: typeof runExec =
|
|||||||
|
|
||||||
export async function disableTailscaleFunnel(exec: typeof runExec = runExec) {
|
export async function disableTailscaleFunnel(exec: typeof runExec = runExec) {
|
||||||
const tailscaleBin = await getTailscaleBinary();
|
const tailscaleBin = await getTailscaleBinary();
|
||||||
await exec(tailscaleBin, ["funnel", "reset"], {
|
await exec("sudo", ["-n", tailscaleBin, "funnel", "reset"], {
|
||||||
maxBuffer: 200_000,
|
maxBuffer: 200_000,
|
||||||
timeoutMs: 15_000,
|
timeoutMs: 15_000,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user