From 3d2497432a67f67a4b0a1e861a00cb2bc1862b8e Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 1 Nov 2026 01:12:43 +0000 Subject: [PATCH 1/3] test(relay): reproduce cloudflared aborting on inherited GODEBUG These tests reproduce pingdotgg/t3code#28453. Go >= 1.24 aborts at startup when a removed GODEBUG setting such as tlskyber=0 is set, and both cloudflared spawns hand it the server's environment unchanged: the install's version probe fails validation and the tunnel inherits GODEBUG. Co-Authored-By: Claude Opus 5.5 --- .../src/cloud/ManagedEndpointRuntime.test.ts | 36 +++++++++++++ packages/shared/src/relayClient.test.ts | 51 +++++++++++++++++++ 2 files changed, 87 insertions(+) diff --git a/apps/server/src/cloud/ManagedEndpointRuntime.test.ts b/apps/server/src/cloud/ManagedEndpointRuntime.test.ts index 409ed3ce2fb6..a906558d50f0 100644 --- a/apps/server/src/cloud/ManagedEndpointRuntime.test.ts +++ b/apps/server/src/cloud/ManagedEndpointRuntime.test.ts @@ -629,6 +629,42 @@ describe("CloudManagedEndpointRuntime", () => { }), ); + it.effect("starts the connector without the host's GODEBUG setting", () => + Effect.gen(function* () { + yield* Effect.acquireRelease( + Effect.sync(() => { + const previous = process.env.GODEBUG; + process.env.GODEBUG = "tlskyber=0"; + return previous; + }), + (previous) => + Effect.sync(() => { + if (previous === undefined) delete process.env.GODEBUG; + else process.env.GODEBUG = previous; + }), + ); + const spawned: Array = []; + const spawner = ChildProcessSpawner.make((command) => + Effect.gen(function* () { + if (!ChildProcess.isStandardCommand(command)) { + return yield* Effect.die("Expected standard command."); + } + spawned.push(command); + const handle = makeHandle({ pid: 700, onKill: () => {} }); + yield* Effect.addFinalizer(() => handle.kill().pipe(Effect.ignore)); + return handle; + }), + ); + const runtime = yield* buildCloudManagedEndpointRuntime(spawner); + yield* runtime.applyConfig(tunnelConfig); + + const { env, extendEnv } = spawned[0]!.options; + const childEnv = env === undefined || extendEnv ? { ...process.env, ...env } : env; + expect(childEnv.GODEBUG).toBeUndefined(); + expect(childEnv.TUNNEL_TOKEN).toBe(tunnelConfig.connectorToken); + }), + ); + it.effect("supervises the active connector and restarts it after process exit", () => Effect.gen(function* () { const spawned: Array = []; diff --git a/packages/shared/src/relayClient.test.ts b/packages/shared/src/relayClient.test.ts index d2719c9ac33f..516b536300a4 100644 --- a/packages/shared/src/relayClient.test.ts +++ b/packages/shared/src/relayClient.test.ts @@ -463,6 +463,57 @@ describe("RelayClient", () => { ), ); + it.effect.skipIf(windowsHost)( + "installs when the host environment sets a GODEBUG value Go has removed", + () => { + const hostEnv = { PATH: "", GODEBUG: "tlskyber=0" }; + const goRuntimeSpawner = ChildProcessSpawner.make((command) => + Effect.sync(() => { + if (!ChildProcess.isStandardCommand(command)) return makeHandle(); + // HostProcess.Environment stands in for the process.env a child inherits. + const { env, extendEnv } = command.options; + const childEnv = env === undefined || extendEnv ? { ...hostEnv, ...env } : env; + // Go >= 1.24 exits 2 before main when a removed GODEBUG setting has its old value. + if (childEnv.GODEBUG === "tlskyber=0") return makeHandle(2); + return command.args[0] === "version" + ? makeHandle( + 0, + `cloudflared version ${RelayClient.CLOUDFLARED_VERSION} (built 2026-01-01-00:00 UTC)\n`, + ) + : makeHandle(); + }), + ); + return Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const baseDir = yield* fileSystem.makeTempDirectoryScoped({ + prefix: "t3-cloudflared-test-", + }); + const manager = yield* RelayClient.makeCloudflaredRelayClient({ + baseDir, + releaseAsset: testReleaseAsset, + }); + + const installed = yield* manager.installWithProgress(() => Effect.void); + expect(installed).toEqual({ + status: "available", + executablePath: managedPathFor(baseDir, RelayClient.CLOUDFLARED_VERSION), + source: "managed", + version: RelayClient.CLOUDFLARED_VERSION, + }); + }).pipe( + Effect.scoped, + Effect.provide( + Layer.mergeAll( + NodeServices.layer, + layerHttpClient(testBinary), + Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, goRuntimeSpawner), + layerHostRuntime(hostEnv), + ), + ), + ); + }, + ); + it.effect("rejects downloads whose checksum does not match the pinned manifest", () => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; From 86b4d1fc712086c85cdc5854ac48710864ac86a4 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 1 Nov 2026 01:17:33 +0000 Subject: [PATCH 2/3] fix(relay): cloudflared no longer inherits the host's GODEBUG Go >= 1.24 aborts at startup when a removed GODEBUG setting is set to its old value. Hosts such as UGREEN UGOS export GODEBUG=tlskyber=0, so the relay client's version probe failed install validation and the tunnel would crash the same way. Both cloudflared spawns now drop GODEBUG and keep the rest of the server's environment. Fixes pingdotgg/t3code#28453 Co-Authored-By: Claude Opus 5.5 --- apps/server/src/cloud/ManagedEndpointRuntime.ts | 2 ++ packages/shared/src/relayClient.ts | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/server/src/cloud/ManagedEndpointRuntime.ts b/apps/server/src/cloud/ManagedEndpointRuntime.ts index 2528a08630a1..9d5119a61daf 100644 --- a/apps/server/src/cloud/ManagedEndpointRuntime.ts +++ b/apps/server/src/cloud/ManagedEndpointRuntime.ts @@ -629,6 +629,8 @@ export const make = Effect.gen(function* () { detached: false, env: { ...process.env, + // Go >= 1.24 aborts at startup when a removed GODEBUG setting (e.g. tlskyber=0) is inherited. + GODEBUG: undefined, TUNNEL_TOKEN: config.connectorToken, }, shell: false, diff --git a/packages/shared/src/relayClient.ts b/packages/shared/src/relayClient.ts index e56ef42ee1a4..a6f06ba7890c 100644 --- a/packages/shared/src/relayClient.ts +++ b/packages/shared/src/relayClient.ts @@ -281,7 +281,13 @@ export const makeCloudflaredRelayClient = Effect.fn("cloudflared.make")(function (key: VersionProbeKey) => spawner .string( - ChildProcess.make(key.executablePath, ["version"], { stdin: "ignore", stderr: "ignore" }), + ChildProcess.make(key.executablePath, ["version"], { + stdin: "ignore", + stderr: "ignore", + // Go >= 1.24 aborts at startup when a removed GODEBUG setting (e.g. tlskyber=0) is inherited. + env: { GODEBUG: undefined }, + extendEnv: true, + }), ) .pipe( Effect.map(parseCloudflaredVersionOutput), From 6e1a5101aedc657b8ed4a7c879e96b4b8952d1ee Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 10 Nov 2026 23:40:21 +0000 Subject: [PATCH 3/3] test(relay): inject GODEBUG through the host env after merging main Use the host Environment the connector now inherits from instead of mutating process.env, and provide NodeCrypto to the install test as main's tests do. AI-assisted; verified with the tests and tools listed above. --- .../src/cloud/ManagedEndpointRuntime.test.ts | 26 +++++++------------ packages/shared/src/relayClient.test.ts | 1 + 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/apps/server/src/cloud/ManagedEndpointRuntime.test.ts b/apps/server/src/cloud/ManagedEndpointRuntime.test.ts index 237e819f77ac..73bffa1f4f7b 100644 --- a/apps/server/src/cloud/ManagedEndpointRuntime.test.ts +++ b/apps/server/src/cloud/ManagedEndpointRuntime.test.ts @@ -914,18 +914,6 @@ describe("CloudManagedEndpointRuntime", () => { it.effect("starts the connector without the host's GODEBUG setting", () => Effect.gen(function* () { - yield* Effect.acquireRelease( - Effect.sync(() => { - const previous = process.env.GODEBUG; - process.env.GODEBUG = "tlskyber=0"; - return previous; - }), - (previous) => - Effect.sync(() => { - if (previous === undefined) delete process.env.GODEBUG; - else process.env.GODEBUG = previous; - }), - ); const spawned: Array = []; const spawner = ChildProcessSpawner.make((command) => Effect.gen(function* () { @@ -938,13 +926,19 @@ describe("CloudManagedEndpointRuntime", () => { return handle; }), ); - const runtime = yield* buildCloudManagedEndpointRuntime(spawner); + const runtime = yield* buildCloudManagedEndpointRuntime( + spawner, + layerRelayClientAvailable, + unusedHttpClient, + emptySecretStoreLayer, + { GODEBUG: "tlskyber=0" }, + ); yield* runtime.applyConfig(tunnelConfig); const { env, extendEnv } = spawned[0]!.options; - const childEnv = env === undefined || extendEnv ? { ...process.env, ...env } : env; - expect(childEnv.GODEBUG).toBeUndefined(); - expect(childEnv.TUNNEL_TOKEN).toBe(tunnelConfig.connectorToken); + expect(extendEnv).not.toBe(true); + expect(env?.GODEBUG).toBeUndefined(); + expect(env?.TUNNEL_TOKEN).toBe(tunnelConfig.connectorToken); }), ); diff --git a/packages/shared/src/relayClient.test.ts b/packages/shared/src/relayClient.test.ts index 010f6bfa9eb4..365501b9be53 100644 --- a/packages/shared/src/relayClient.test.ts +++ b/packages/shared/src/relayClient.test.ts @@ -513,6 +513,7 @@ describe("RelayClient", () => { Effect.provide( Layer.mergeAll( NodeServices.layer, + NodeCrypto.layer, layerHttpClient(testBinary), Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, goRuntimeSpawner), layerHostRuntime(hostEnv),