fix(core): Propagate proxy environment to task runner processes

This commit is contained in:
Lorent Lempereur 2026-08-25 17:33:11 +02:00
parent c40ac64ea2
commit 05dfb59448
No known key found for this signature in database
10 changed files with 119 additions and 2 deletions

View File

@ -126,6 +126,9 @@ class TaskRunner:
self.websocket_url,
additional_headers=headers,
max_size=self.config.max_payload_size,
# The broker connection must stay direct, never routed
# through a proxy configured via environment variables.
proxy=None,
)
self.logger.info("Connected to broker")
await self._listen_for_messages()

View File

@ -94,6 +94,25 @@ class TestTaskRunnerConnectionRetry:
assert mock_connect.call_count == 1
@pytest.mark.asyncio
async def test_broker_connection_is_never_proxied(self, config):
runner = TaskRunner(config)
def connection_side_effect(*args, **kwargs):
runner.is_shutting_down = True
raise ConnectionRefusedError("Connection refused")
with (
patch("src.task_runner.websockets.connect") as mock_connect,
patch.object(runner, "logger"),
patch("src.task_runner.asyncio.sleep"),
):
mock_connect.side_effect = connection_side_effect
await runner.start()
assert mock_connect.call_args.kwargs["proxy"] is None
class TestTaskRunnerDrain:
@pytest.fixture

View File

@ -37,6 +37,7 @@
}
},
"dependencies": {
"@n8n/backend-network": "workspace:*",
"@n8n/config": "workspace:*",
"@n8n/di": "workspace:*",
"@n8n/errors": "workspace:*",

View File

@ -0,0 +1,46 @@
import { Container } from '@n8n/di';
import http from 'node:http';
import https from 'node:https';
import { mock } from 'vitest-mock-extended';
import { TaskRunnerSentry } from '../task-runner-sentry';
vi.mock('../js-task-runner/js-task-runner', () => ({
// eslint-disable-next-line @typescript-eslint/naming-convention
JsTaskRunner: class {
on() {}
async stop() {}
},
}));
describe('start', () => {
it('should install env-proxy global agents and exempt the broker host when proxy env vars are set', async () => {
const previousHttpAgent = http.globalAgent;
const previousHttpsAgent = https.globalAgent;
const previousNoProxy = process.env.NO_PROXY;
process.env.HTTPS_PROXY = 'http://proxy.host.invalid:3128';
// The NODE_PATH shim in start.ts relies on a CJS-only internal API.
vi.stubEnv('NODE_PATH', undefined);
Container.set(TaskRunnerSentry, mock<TaskRunnerSentry>());
try {
await import('../start.js');
expect(http.globalAgent.constructor.name).toBe('EnvProxyHttpAgent');
expect(https.globalAgent.constructor.name).toBe('EnvProxyHttpsAgent');
expect(process.env.NO_PROXY?.split(',')).toContain('127.0.0.1');
} finally {
delete process.env.HTTPS_PROXY;
if (previousNoProxy === undefined) {
delete process.env.NO_PROXY;
delete process.env.no_proxy;
} else {
process.env.NO_PROXY = previousNoProxy;
}
vi.unstubAllEnvs();
http.globalAgent = previousHttpAgent;
https.globalAgent = previousHttpsAgent;
}
});
});

View File

@ -1,3 +1,5 @@
import { installGlobalProxyAgent } from '@n8n/backend-network';
import { ensureHostsBypassProxy } from '@n8n/backend-network/proxy';
import { Container } from '@n8n/di';
import { ensureError } from '@n8n/utils/errors/ensure-error';
import { setGlobalState } from 'n8n-workflow';
@ -70,6 +72,12 @@ function createSignalHandler(
void (async function start() {
const config = Container.get(MainConfig);
// Env-proxy global agents must be installed before any outbound traffic
// (Sentry, user task code). The task broker connection must stay direct,
// so its host is exempted from proxying for the process lifetime.
ensureHostsBypassProxy([new URL(config.baseRunnerConfig.taskBrokerUri).hostname]);
installGlobalProxyAgent();
setGlobalState({
defaultTimezone: config.baseRunnerConfig.timezone,
});

View File

@ -50,6 +50,14 @@ describe('PyTaskRunnerProcess', () => {
'N8N_RUNNERS_ALLOW_TRANSITIVE_IMPORTS',
'N8N_RUNNERS_BUILTINS_DENY',
'N8N_BLOCK_RUNNER_ENV_ACCESS',
'HTTP_PROXY',
'http_proxy',
'HTTPS_PROXY',
'https_proxy',
'ALL_PROXY',
'all_proxy',
'NO_PROXY',
'no_proxy',
])('should propagate %s from env as is', async (envVar) => {
process.env[envVar] = 'custom value';

View File

@ -114,6 +114,14 @@ describe('TaskRunnerProcess', () => {
'NODE_PATH',
'GENERIC_TIMEZONE',
'N8N_RUNNERS_INSECURE_MODE',
'HTTP_PROXY',
'http_proxy',
'HTTPS_PROXY',
'https_proxy',
'ALL_PROXY',
'all_proxy',
'NO_PROXY',
'no_proxy',
])('should propagate %s from env as is', async (envVar) => {
authService.createGrantToken.mockResolvedValue('grantToken');
process.env[envVar] = 'custom value';

View File

@ -62,6 +62,16 @@ export class JsTaskRunnerProcess extends TaskRunnerProcessBase {
NODE_FUNCTION_ALLOW_BUILTIN: process.env.NODE_FUNCTION_ALLOW_BUILTIN,
NODE_FUNCTION_ALLOW_EXTERNAL: process.env.NODE_FUNCTION_ALLOW_EXTERNAL,
// proxy
HTTP_PROXY: process.env.HTTP_PROXY,
http_proxy: process.env.http_proxy,
HTTPS_PROXY: process.env.HTTPS_PROXY,
https_proxy: process.env.https_proxy,
ALL_PROXY: process.env.ALL_PROXY,
all_proxy: process.env.all_proxy,
NO_PROXY: process.env.NO_PROXY,
no_proxy: process.env.no_proxy,
// sentry
N8N_SENTRY_DSN: process.env.N8N_SENTRY_DSN,
N8N_VERSION: process.env.N8N_VERSION,

View File

@ -49,6 +49,16 @@ export class PyTaskRunnerProcess extends TaskRunnerProcessBase {
N8N_RUNNERS_TASK_TIMEOUT: this.runnerConfig.taskTimeout.toString(),
N8N_RUNNERS_HEARTBEAT_INTERVAL: this.runnerConfig.heartbeatInterval.toString(),
// proxy
HTTP_PROXY: process.env.HTTP_PROXY,
http_proxy: process.env.http_proxy,
HTTPS_PROXY: process.env.HTTPS_PROXY,
https_proxy: process.env.https_proxy,
ALL_PROXY: process.env.ALL_PROXY,
all_proxy: process.env.all_proxy,
NO_PROXY: process.env.NO_PROXY,
no_proxy: process.env.no_proxy,
// n8n
N8N_RUNNERS_STDLIB_ALLOW: process.env.N8N_RUNNERS_STDLIB_ALLOW,
N8N_RUNNERS_EXTERNAL_ALLOW: process.env.N8N_RUNNERS_EXTERNAL_ALLOW,

View File

@ -1128,7 +1128,7 @@ importers:
version: 1.0.27(@aws-sdk/credential-provider-node@3.972.80)(@langchain/core@1.2.8(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.221.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1))(openai@6.46.0(@aws-sdk/credential-provider-node@3.972.80)(@smithy/signature-v4@5.7.2)(ws@8.21.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.21.1(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.221.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1))(@smithy/signature-v4@5.7.2)(cheerio@1.1.0)(openai@6.46.0(@aws-sdk/credential-provider-node@3.972.80)(@smithy/signature-v4@5.7.2)(ws@8.21.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.21.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))
'@langchain/community':
specifier: 'catalog:'
version: 1.1.27(6cc31e45a82b311205f2bf99fec67d91)
version: 1.1.27(2af036a866615d639e1d6409476fa500)
'@langchain/core':
specifier: 'catalog:'
version: 1.2.8(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.221.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1))(openai@6.46.0(@aws-sdk/credential-provider-node@3.972.80)(@smithy/signature-v4@5.7.2)(ws@8.21.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.21.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))
@ -3718,6 +3718,9 @@ importers:
packages/@n8n/task-runner:
dependencies:
'@n8n/backend-network':
specifier: workspace:*
version: link:../backend-network
'@n8n/config':
specifier: workspace:*
version: link:../config
@ -14296,6 +14299,7 @@ packages:
'@xmldom/xmldom@0.8.14':
resolution: {integrity: sha512-T4EDRUBVZYRldYApjEJiU0e1stYWaRAX7CuSnKzrpwdZKo53zGV8/pqfzV6FfwNl9YThD2OumQYvqtvjvgG7aQ==}
engines: {node: '>=10.0.0'}
deprecated: this version has critical issues, please update to the latest version
'@yuku-codegen/binding-darwin-arm64@0.6.1':
resolution: {integrity: sha512-LDJtpOKtcv9f3V0eDUwFmmy47t2VC+DAuN+gq80R1IA+fa0d408i6sHsVtt6n+g5rf8f86ySoPSAe94lHt6Ixw==}
@ -28512,7 +28516,7 @@ snapshots:
- aws-crt
- encoding
'@langchain/community@1.1.27(6cc31e45a82b311205f2bf99fec67d91)':
'@langchain/community@1.1.27(2af036a866615d639e1d6409476fa500)':
dependencies:
'@browserbasehq/stagehand': empty-npm-package@1.0.0
'@ibm-cloud/watsonx-ai': 1.1.2(supports-color@8.1.1)