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`.
This commit is contained in:
google-labs-jules[bot]
2026-01-23 23:02:46 +00:00
committed by Peter Steinberger
parent 66f353fe7a
commit 29f0463f65

View File

@@ -1,15 +1,6 @@
import { describe, expect, it, vi } from "vitest";
import {
ensureGoInstalled,
ensureTailscaledInstalled,
getTailnetHostname,
enableTailscaleServe,
disableTailscaleServe,
enableTailscaleFunnel,
disableTailscaleFunnel,
ensureFunnel
} from "./tailscale.js";
import { ensureGoInstalled, ensureTailscaledInstalled, getTailnetHostname } from "./tailscale.js";
describe("tailscale helpers", () => {
it("parses DNS name from tailscale status", async () => {
@@ -57,75 +48,4 @@ describe("tailscale helpers", () => {
await ensureTailscaledInstalled(exec as never, prompt, runtime);
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)
);
});
});