Isolate cloudflared runtime environment

This commit is contained in:
R.D.
2026-03-29 22:30:07 -04:00
parent 0a1b68639b
commit 179c03e79d
2 changed files with 88 additions and 5 deletions

View File

@@ -24,6 +24,18 @@ type AssetSpec = {
downloadUrl: string;
};
type CloudflaredRuntimeDirs = {
runtimeRoot: string;
homeDir: string;
configDir: string;
cacheDir: string;
dataDir: string;
tempDir: string;
userProfileDir: string;
appDataDir: string;
localAppDataDir: string;
};
type BinaryResolution = {
binaryPath: string | null;
source: CloudflaredInstallSource | null;
@@ -125,6 +137,24 @@ function getLogFilePath() {
return path.join(getTunnelDir(), "quick-tunnel.log");
}
export function getCloudflaredRuntimeDirs(): CloudflaredRuntimeDirs {
const runtimeRoot = path.join(getTunnelDir(), "runtime");
const homeDir = path.join(runtimeRoot, "home");
const userProfileDir = path.join(runtimeRoot, "userprofile");
return {
runtimeRoot,
homeDir,
configDir: path.join(runtimeRoot, "config"),
cacheDir: path.join(runtimeRoot, "cache"),
dataDir: path.join(runtimeRoot, "data"),
tempDir: path.join(runtimeRoot, "tmp"),
userProfileDir,
appDataDir: path.join(userProfileDir, "AppData", "Roaming"),
localAppDataDir: path.join(userProfileDir, "AppData", "Local"),
};
}
function getLocalTargetUrl() {
const { apiPort } = getRuntimePorts();
return `http://127.0.0.1:${apiPort}`;
@@ -138,6 +168,13 @@ async function ensureTunnelDir() {
await fs.mkdir(path.join(getTunnelDir(), "bin"), { recursive: true });
}
async function ensureTunnelRuntimeDirs() {
const runtimeDirs = getCloudflaredRuntimeDirs();
await Promise.all(
Object.values(runtimeDirs).map((dirPath) => fs.mkdir(dirPath, { recursive: true }))
);
}
async function readStateFile(): Promise<PersistedTunnelState> {
try {
const content = await fs.readFile(getStateFilePath(), "utf8");
@@ -202,7 +239,8 @@ export function extractTryCloudflareUrl(text: string) {
}
export function buildCloudflaredChildEnv(
sourceEnv: NodeJS.ProcessEnv = process.env
sourceEnv: NodeJS.ProcessEnv = process.env,
runtimeDirs: CloudflaredRuntimeDirs = getCloudflaredRuntimeDirs()
): NodeJS.ProcessEnv {
const childEnv: NodeJS.ProcessEnv = {};
@@ -213,9 +251,25 @@ export function buildCloudflaredChildEnv(
}
}
childEnv.HOME = runtimeDirs.homeDir;
childEnv.XDG_CONFIG_HOME = runtimeDirs.configDir;
childEnv.XDG_CACHE_HOME = runtimeDirs.cacheDir;
childEnv.XDG_DATA_HOME = runtimeDirs.dataDir;
childEnv.USERPROFILE = runtimeDirs.userProfileDir;
childEnv.APPDATA = runtimeDirs.appDataDir;
childEnv.LOCALAPPDATA = runtimeDirs.localAppDataDir;
if (!childEnv.TMPDIR) childEnv.TMPDIR = runtimeDirs.tempDir;
if (!childEnv.TMP) childEnv.TMP = runtimeDirs.tempDir;
if (!childEnv.TEMP) childEnv.TEMP = runtimeDirs.tempDir;
return childEnv;
}
export function getCloudflaredStartArgs(targetUrl: string) {
return ["tunnel", "--url", targetUrl, "--no-autoupdate"];
}
export function getCloudflaredAssetSpec(
platform = process.platform,
arch = process.arch
@@ -493,6 +547,7 @@ export async function startCloudflaredTunnel(): Promise<CloudflaredTunnelStatus>
await stopExistingTunnel();
await ensureTunnelDir();
await ensureTunnelRuntimeDirs();
await fs.writeFile(getLogFilePath(), "", "utf8");
await writeStateFile({
@@ -509,7 +564,7 @@ export async function startCloudflaredTunnel(): Promise<CloudflaredTunnelStatus>
const child = spawn(
binary.binaryPath as string,
["tunnel", "--url", targetUrl, "--no-autoupdate", "--protocol", "http2"],
getCloudflaredStartArgs(targetUrl),
{
stdio: ["ignore", "pipe", "pipe"],
env: buildCloudflaredChildEnv(),

View File

@@ -4,6 +4,7 @@ import assert from "node:assert/strict";
import {
buildCloudflaredChildEnv,
extractTryCloudflareUrl,
getCloudflaredStartArgs,
getCloudflaredAssetSpec,
} from "../../src/lib/cloudflaredTunnel.ts";
@@ -47,18 +48,45 @@ test("getCloudflaredAssetSpec returns null for unsupported platforms", () => {
assert.equal(getCloudflaredAssetSpec("freebsd", "x64"), null);
});
test("buildCloudflaredChildEnv keeps runtime essentials but drops secrets", () => {
test("buildCloudflaredChildEnv keeps runtime essentials, isolates runtime dirs, and drops secrets", () => {
const env = buildCloudflaredChildEnv({
PATH: "/usr/bin",
HOME: "/tmp/home",
HTTPS_PROXY: "http://proxy.internal:8080",
JWT_SECRET: "top-secret",
API_KEY_SECRET: "another-secret",
}, {
runtimeRoot: "/managed/runtime",
homeDir: "/managed/runtime/home",
configDir: "/managed/runtime/config",
cacheDir: "/managed/runtime/cache",
dataDir: "/managed/runtime/data",
tempDir: "/managed/runtime/tmp",
userProfileDir: "/managed/runtime/userprofile",
appDataDir: "/managed/runtime/userprofile/AppData/Roaming",
localAppDataDir: "/managed/runtime/userprofile/AppData/Local",
});
assert.deepEqual(env, {
PATH: "/usr/bin",
HOME: "/tmp/home",
HTTPS_PROXY: "http://proxy.internal:8080",
HOME: "/managed/runtime/home",
XDG_CONFIG_HOME: "/managed/runtime/config",
XDG_CACHE_HOME: "/managed/runtime/cache",
XDG_DATA_HOME: "/managed/runtime/data",
USERPROFILE: "/managed/runtime/userprofile",
APPDATA: "/managed/runtime/userprofile/AppData/Roaming",
LOCALAPPDATA: "/managed/runtime/userprofile/AppData/Local",
TMPDIR: "/managed/runtime/tmp",
TMP: "/managed/runtime/tmp",
TEMP: "/managed/runtime/tmp",
});
});
test("getCloudflaredStartArgs relies on cloudflared protocol auto-negotiation", () => {
assert.deepEqual(getCloudflaredStartArgs("http://127.0.0.1:20128"), [
"tunnel",
"--url",
"http://127.0.0.1:20128",
"--no-autoupdate",
]);
});