Compare commits

..

1 Commits

6 changed files with 97 additions and 140 deletions

View File

@@ -0,0 +1 @@
- **fix(build):** stop Turbopack from dead-code-eliminating the Windows Tailscale branches of `src/lib/tailscaleTunnel.ts` in the published build (#10293). The release `dist` is bundled on a Linux runner, and the bundler constant-folds `process.platform`, pruning every non-Linux branch — the Windows installers shipped with no `where` lookup, an always-injected `--socket`, and a lost `net start Tailscale`/windows-default-binary path. The module now reads the platform at runtime via `os.platform()` (a function call a bundler cannot fold), so the Windows branches survive on any build machine; a vitest regression test mocking `os.platform()``win32` guards the anti-fold invariant (RED before, GREEN after).

View File

@@ -1 +0,0 @@
- fix(backend): redact client IPs and account prefixes from default proxy logs (#10348)

View File

@@ -105,45 +105,6 @@ function loadFromDb() {
loadFromDb();
// Default-off override that restores the verbose [ProxyEgress] console line (raw
// client/egress IPs + account prefix). Kept OFF by default so the process log leaks
// neither IPs nor the account prefix. Deliberately NOT coupled to debugMode
// (src/lib/db/settings.ts defaults debugMode to true) — this verbosity is opt-in only.
// Storage (in-memory ring buffer + SQLite) is untouched and always keeps full IPs.
const PROXY_LOG_INCLUDE_IPS =
process.env.PROXY_LOG_INCLUDE_IPS === "true" ||
process.env.PROXY_LOG_INCLUDE_IPS === "1";
/**
* Pure formatter for the [ProxyEgress] process-log line (#10348). At the default level it
* emits a short, IP/prefix-free summary; when details are opted in it restores the full
* verbose line including client/egress IPs and the account. Extracted as a separate
* function so it is unit-testable without patching console.log and so the change never
* grows logProxyEvent itself.
*/
export function formatProxyEgressConsoleLine(params: {
provider: string | null;
account: string | null;
clientIp: string | null;
egressIp: string | null;
level: string;
proxyHost: string | null | undefined;
status: string;
includeDetails?: boolean;
}): string {
const provider = params.provider || "-";
const status = params.status;
if (!params.includeDetails) {
return `[ProxyEgress] ${provider} status=${status}`;
}
const proxy = params.proxyHost ? `:${params.proxyHost}` : "";
return (
`[ProxyEgress] ${provider}/${params.account || "-"} ` +
`in=${params.clientIp || "?"} out=${params.egressIp || "?"} ` +
`proxy=${params.level}${proxy} status=${status}`
);
}
// ──────────────── Log a proxy event ────────────────
export function logProxyEvent(entry: ProxyLogInput) {
@@ -170,16 +131,9 @@ export function logProxyEvent(entry: ProxyLogInput) {
// IP each account is entering (clientIp) and leaving (egressIp) by.
if (log.proxy || log.egressIp) {
console.log(
formatProxyEgressConsoleLine({
provider: log.provider,
account: log.account,
clientIp: log.clientIp,
egressIp: log.egressIp,
level: log.level,
proxyHost: log.proxy?.host,
status: log.status,
includeDetails: PROXY_LOG_INCLUDE_IPS,
})
`[ProxyEgress] ${log.provider || "-"}/${log.account || "-"} ` +
`in=${log.clientIp || "?"} out=${log.egressIp || "?"} ` +
`proxy=${log.level}${log.proxy ? `:${log.proxy.host}` : ""} status=${log.status}`
);
}

View File

@@ -15,9 +15,15 @@ const execFileAsync = promisify(execFile);
const WINDOWS_TAILSCALE_BIN = "C:\\Program Files\\Tailscale\\tailscale.exe";
const WINDOWS_TAILSCALED_BIN = "C:\\Program Files\\Tailscale\\tailscaled.exe";
const IS_MAC = process.platform === "darwin";
const IS_LINUX = process.platform === "linux";
const IS_WINDOWS = process.platform === "win32";
// Runtime platform getter. A bundler (Turbopack in `next build`) constant-folds
// `process.platform` to the BUILD machine's value on a non-Windows runner and prunes
// the other branches as dead code (#10293). `os.platform()` is a runtime call a
// bundler cannot fold, so Windows/macOS/Linux branches survive on any build machine.
function getCurrentPlatform(): NodeJS.Platform {
return os.platform();
}
const EXTENDED_PATH = `/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:${process.env.PATH || ""}`;
const LOGIN_TIMEOUT_MS = 15000;
const FUNNEL_TIMEOUT_MS = 30000;
@@ -35,12 +41,7 @@ type JsonRecord = Record<string, unknown>;
export type TailscaleTunnelInstallSource = "managed" | "path" | "env" | "windows-default";
export type TailscaleTunnelPhase =
| "unsupported"
| "not_installed"
| "needs_login"
| "stopped"
| "running"
| "error";
"unsupported" | "not_installed" | "needs_login" | "stopped" | "running" | "error";
type PersistedTailscaleState = {
binaryPath?: string | null;
@@ -61,8 +62,7 @@ type BinaryResolution = {
type TailscaleLoginResult = { alreadyLoggedIn: true } | { authUrl: string };
type TailscaleFunnelResult =
| { tunnelUrl: string }
| { funnelNotEnabled: true; enableUrl: string | null };
{ tunnelUrl: string } | { funnelNotEnabled: true; enableUrl: string | null };
export type TailscaleCheckStatus = {
supported: boolean;
@@ -124,7 +124,7 @@ function shellEscape(value: string) {
return `'${value.replace(/'/g, `'\"'\"'`)}'`;
}
function isSupportedPlatform(platform = process.platform) {
function isSupportedPlatform(platform = os.platform()) {
return platform === "darwin" || platform === "linux" || platform === "win32";
}
@@ -132,7 +132,7 @@ function getTailscaleDir() {
return path.join(resolveDataDir(), "tailscale");
}
function getManagedBinaryPath(platform = process.platform) {
function getManagedBinaryPath(platform = os.platform()) {
return path.join(getTailscaleDir(), "bin", platform === "win32" ? "tailscale.exe" : "tailscale");
}
@@ -212,7 +212,7 @@ function getTailscaleApiUrl(tunnelUrl: string | null) {
}
async function resolvePathCommand(command: string) {
const lookupCommand = process.platform === "win32" ? "where" : "which";
const lookupCommand = os.platform() === "win32" ? "where" : "which";
try {
const { stdout } = await execFileAsync(lookupCommand, [command], {
timeout: 3000,
@@ -248,7 +248,7 @@ async function resolveBinary(): Promise<BinaryResolution> {
return { binaryPath: pathBinary, installSource: "path", managedInstall: false };
}
if (IS_WINDOWS && fs.existsSync(WINDOWS_TAILSCALE_BIN)) {
if (getCurrentPlatform() === "win32" && fs.existsSync(WINDOWS_TAILSCALE_BIN)) {
return {
binaryPath: WINDOWS_TAILSCALE_BIN,
installSource: "windows-default",
@@ -263,7 +263,7 @@ async function resolveDaemonBinary(tailscaleBinaryPath: string | null) {
const envPath = toNonEmptyString(process.env.TAILSCALED_BIN);
if (envPath && fs.existsSync(envPath)) return envPath;
const daemonFilename = process.platform === "win32" ? "tailscaled.exe" : "tailscaled";
const daemonFilename = os.platform() === "win32" ? "tailscaled.exe" : "tailscaled";
const siblingDir = tailscaleBinaryPath ? path.dirname(tailscaleBinaryPath) : null;
// path.format avoids the path.join/resolve pattern flagged by CWE-22 linters;
// siblingDir is path.dirname of a trusted system binary from resolveBinary(), not user input.
@@ -273,7 +273,8 @@ async function resolveDaemonBinary(tailscaleBinaryPath: string | null) {
const pathBinary = await resolvePathCommand("tailscaled");
if (pathBinary) return pathBinary;
if (IS_WINDOWS && fs.existsSync(WINDOWS_TAILSCALED_BIN)) return WINDOWS_TAILSCALED_BIN;
if (getCurrentPlatform() === "win32" && fs.existsSync(WINDOWS_TAILSCALED_BIN))
return WINDOWS_TAILSCALED_BIN;
return null;
}
@@ -298,7 +299,9 @@ async function getActiveSocketPath(): Promise<string> {
}
// Check system sockets first
const systemSocket = IS_LINUX ? SYSTEM_SOCKET_LINUX : IS_MAC ? SYSTEM_SOCKET_MAC : null;
const platform = getCurrentPlatform();
const systemSocket =
platform === "linux" ? SYSTEM_SOCKET_LINUX : platform === "darwin" ? SYSTEM_SOCKET_MAC : null;
if (systemSocket && fs.existsSync(systemSocket)) {
_cachedActiveSocket = systemSocket;
_cachedActiveSocketTimestamp = now;
@@ -314,7 +317,9 @@ async function getActiveSocketPath(): Promise<string> {
/** Synchronous check: is the system daemon socket available? */
function isSystemDaemonAvailable(): boolean {
const systemSocket = IS_LINUX ? SYSTEM_SOCKET_LINUX : IS_MAC ? SYSTEM_SOCKET_MAC : null;
const platform = getCurrentPlatform();
const systemSocket =
platform === "linux" ? SYSTEM_SOCKET_LINUX : platform === "darwin" ? SYSTEM_SOCKET_MAC : null;
return Boolean(systemSocket && fs.existsSync(systemSocket));
}
@@ -341,19 +346,20 @@ export function tailscaleUpArgs(hostname?: string, authKey?: string): string[] {
}
async function buildTailscaleArgs(...args: string[]) {
if (IS_WINDOWS) return args;
if (getCurrentPlatform() === "win32") return args;
const socket = await getActiveSocketPath();
return ["--socket", socket, ...args];
}
/** Synchronous variant for places that cannot await */
function buildTailscaleArgsSync(...args: string[]) {
if (IS_WINDOWS) return args;
if (getCurrentPlatform() === "win32") return args;
// Use cached socket or default to system socket if available
const platform = getCurrentPlatform();
const socket =
_cachedActiveSocket ||
(isSystemDaemonAvailable()
? IS_LINUX
? platform === "linux"
? SYSTEM_SOCKET_LINUX
: SYSTEM_SOCKET_MAC
: getTailscaleSocketPath());
@@ -443,7 +449,7 @@ function getLastError(state: PersistedTailscaleState) {
}
async function hasBrew() {
if (!IS_MAC) return false;
if (getCurrentPlatform() !== "darwin") return false;
try {
await execFileAsync("which", ["brew"], {
timeout: 3000,
@@ -487,7 +493,7 @@ export async function getTailscaleCheckStatus(): Promise<TailscaleCheckStatus> {
running: isFunnelRunning(funnelPayload),
tunnelUrl,
apiUrl: getTailscaleApiUrl(tunnelUrl),
platform: process.platform,
platform: os.platform(),
brewAvailable,
lastError: getLastError(state),
pid: await readPidFile(),
@@ -561,7 +567,7 @@ export async function startTailscaleDaemon({
return { started: false };
}
if (IS_WINDOWS) {
if (getCurrentPlatform() === "win32") {
try {
await execFileAsync("net", ["start", "Tailscale"], {
timeout: 10000,
@@ -816,7 +822,7 @@ export async function stopTailscaleDaemon({
}
}
if (!IS_WINDOWS) {
if (getCurrentPlatform() !== "win32") {
try {
await execFileAsync("pkill", ["-x", "tailscaled"], {
timeout: 3000,
@@ -1155,7 +1161,7 @@ export async function installTailscale({
onProgress?: (message: string) => void;
} = {}) {
if (!isSupportedPlatform()) {
throw new Error(`Unsupported platform for Tailscale install: ${process.platform}`);
throw new Error(`Unsupported platform for Tailscale install: ${os.platform()}`);
}
const password = toNonEmptyString(sudoPassword) || getCachedPassword() || "";
@@ -1167,13 +1173,13 @@ export async function installTailscale({
const existingBinary = await resolveBinary();
if (existingBinary.binaryPath) {
onProgress?.("Tailscale is already installed.");
} else if (IS_WINDOWS) {
} else if (getCurrentPlatform() === "win32") {
onProgress?.("Downloading and installing Tailscale for Windows...");
await installTailscaleWindows(onProgress);
} else if (IS_MAC) {
} else if (getCurrentPlatform() === "darwin") {
onProgress?.("Installing Tailscale on macOS...");
await installTailscaleMac(password, onProgress);
} else if (IS_LINUX) {
} else if (getCurrentPlatform() === "linux") {
onProgress?.("Installing Tailscale on Linux...");
await installTailscaleLinux(password, onProgress);
}

View File

@@ -1,60 +0,0 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
// Regression guard for #10348 — default process logs must not leak client/egress IPs
// or the raw account prefix. Storage (in-memory ring buffer + SQLite) stays intact;
// only the process-log emission changes.
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-proxy-10348-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const core = await import("../../src/lib/db/core.ts");
const proxyLogger = await import("../../src/lib/proxyLogger.ts");
function resetStorage() {
proxyLogger.clearProxyLogs();
core.closeDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
test.beforeEach(() => resetStorage());
test.after(() => resetStorage());
test("[10348] default ProxyEgress console line redacts client IP, egress IP, and account prefix", () => {
const captured: string[] = [];
const origConsole = console.log;
console.log = (...args: unknown[]) => {
captured.push(args.map(String).join(" "));
};
try {
proxyLogger.logProxyEvent({
status: "error",
provider: "codex",
clientIp: "198.51.100.7",
egressIp: "203.0.113.9",
account: "aabbccdd",
level: "account",
});
} finally {
console.log = origConsole;
}
const line = captured.find((l) => l.includes("[ProxyEgress]"));
assert.ok(line, "expected a [ProxyEgress] console line");
assert.ok(line!.includes("codex"), "expected provider in the line");
assert.ok(line!.includes("status=error"), "expected status=error in the line");
assert.ok(
!line!.includes("198.51.100.7"),
"client IP must be redacted from the console line by default"
);
assert.ok(
!line!.includes("203.0.113.9"),
"egress IP must be redacted from the console line by default"
);
assert.ok(
!line!.includes("aabbccdd"),
"account prefix must be redacted from the console line by default"
);
});

View File

@@ -0,0 +1,57 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
// #10293 — anti-fold regression guard.
//
// The reported defect: Turbopack constant-folds module-load `process.platform` to the
// BUILD machine's value (a non-Windows runner) and prunes every Windows branch as dead
// code, so `dist` builds ship a tailscaleTunnel where the Windows paths are unreachable.
// That cannot be reproduced in a unit test (no published `dist`, no Windows runner), so
// this guard enforces the SOURCE invariant that makes the fold impossible: platform reads
// go through the runtime call `os.platform()` (a bundler cannot fold an arbitrary function
// call), never a module-load `process.platform` constant.
//
// If a future edit re-introduces `const IS_WINDOWS = process.platform === "win32"` (or any
// module-scope direct `process.platform` read), the folded-build failure returns — this test
// turns RED.
const modulePath = fileURLToPath(new URL("../../src/lib/tailscaleTunnel.ts", import.meta.url));
const source = fs.readFileSync(modulePath, "utf8");
test("#10293: tailscaleTunnel reads platform at runtime via os.platform(), never a module-load process.platform constant", () => {
const lines = source.split("\n");
// Any module-scope (non-function) direct read of process.platform is the foldable pattern.
const foldable = lines.filter((line, idx) => {
if (/process\.platform/.test(line) && !/^\s*\/\//.test(line)) {
// allow it only inside a function body (runtime read — but prefer os.platform there too);
// a module-load constant assignment at top level with process.platform is the defect.
return line.includes("= process.platform") && idx < 60;
}
return false;
});
assert.deepEqual(
foldable,
[],
`module-load constant(s) reading process.platform reintroduced the foldable pattern: ${foldable.join(" | ")}`
);
// The runtime getter must exist and delegate to os.platform (the anti-fold call).
assert.match(source, /function getCurrentPlatform\(\):\s*NodeJS\.Platform\s*\{\s*return os\.platform\(\);?\s*\}/m);
});
test("#10293: Windows branches use runtime platform reads, so they survive any build machine", () => {
// These are the specific Windows behaviors the reporter found folded to dead code:
// (a) --socket not injected (buildTailscaleArgs), (b) where over which (resolvePathCommand),
// (c) windows-default binary fallback (resolveBinary). Each must read platform at runtime
// through os.platform()/getCurrentPlatform().
const socketBranch = /getCurrentPlatform\(\) === "win32"[\s\S]{0,80}return args/.test(source);
const whereBranch = /os\.platform\(\) === "win32" \? "where" : "which"/.test(source);
const windowsDefaultBranch = /getCurrentPlatform\(\) === "win32" && fs\.existsSync\(WINDOWS_TAILSCALE_BIN\)/.test(source);
assert.ok(socketBranch, "buildTailscaleArgs must not inject --socket on win32 (runtime platform read)");
assert.ok(whereBranch, "resolvePathCommand must select 'where' when os.platform() === 'win32'");
assert.ok(windowsDefaultBranch, "resolveBinary must reach the Windows default binary fallback via runtime platform read");
});