From 615feacaff2a7bc0f875c4cdeccdc26d04f71ae4 Mon Sep 17 00:00:00 2001 From: Alexandre Pereira Date: Wed, 23 Sep 2026 17:51:11 +0100 Subject: [PATCH] fix(install): answer the direct lookup in the shape Node asks for (#2840) The direct install-source dispatcher pins the connection to the approved address through a custom lookup that always answered with a single address. Node 20+ enables network family autoselection by default and calls lookup with `all: true`, expecting an array, so every direct download failed with ERR_INVALID_IP_ADDRESS before a socket opened. Proxied downloads were unaffected. The lookup now returns `[{ address, family }]` when asked for all addresses and the single-address form otherwise, matching the maestro runScript dispatcher. The approved address is still the only one it ever returns. Closes #2840 Co-Authored-By: Claude Opus 5.5 (1M context) --- .../install-source-network-transport.test.ts | 36 +++++++++++++++++-- .../src/install-source-network-transport.ts | 10 +++++- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/packages/provision-kit/src/install-source-network-transport.test.ts b/packages/provision-kit/src/install-source-network-transport.test.ts index 6c8c78a506..3b219119d8 100644 --- a/packages/provision-kit/src/install-source-network-transport.test.ts +++ b/packages/provision-kit/src/install-source-network-transport.test.ts @@ -1,6 +1,13 @@ import assert from 'node:assert/strict'; -import { test } from 'vitest'; -import { matchesNoProxy, resolveProxyForUrl } from './install-source-network-transport.ts'; +import http from 'node:http'; +import type { AddressInfo } from 'node:net'; +import { text } from 'node:stream/consumers'; +import { test, vi } from 'vitest'; +import { + matchesNoProxy, + requestApprovedUrl, + resolveProxyForUrl, +} from './install-source-network-transport.ts'; test('lowercase proxy variables override uppercase even when empty', () => { assert.equal( @@ -37,3 +44,28 @@ test('NO_PROXY matches exact hosts, subdomains, ports, wildcards, and bracketed ); assert.equal(matchesNoProxy(new URL('https://elsewhere.example'), '*'), true); }); + +test('direct requests connect to the approved address through the real lookup', async () => { + vi.stubEnv('no_proxy', '*'); + const server = http.createServer((_request, response) => response.end('artifact')); + await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); + try { + const { port } = server.address() as AddressInfo; + const response = await requestApprovedUrl({ + url: new URL(`http://approved.invalid:${port}/app.zip`), + approvedAddress: '127.0.0.1', + family: 4, + headers: {}, + signal: AbortSignal.timeout(5_000), + }); + try { + assert.equal(response.statusCode, 200); + assert.equal(await text(response.body), 'artifact'); + } finally { + await response.close(); + } + } finally { + await new Promise((resolve) => server.close(() => resolve())); + vi.unstubAllEnvs(); + } +}); diff --git a/packages/provision-kit/src/install-source-network-transport.ts b/packages/provision-kit/src/install-source-network-transport.ts index a32efb9f27..39491de4b6 100644 --- a/packages/provision-kit/src/install-source-network-transport.ts +++ b/packages/provision-kit/src/install-source-network-transport.ts @@ -73,7 +73,15 @@ export function matchesNoProxy(url: URL, raw: string | undefined): boolean { function directDispatcher(address: string, family: 4 | 6): Agent { return new Agent({ - connect: { lookup: (_hostname, _options, callback) => callback(null, address, family) }, + connect: { + lookup: (_hostname, options, callback) => { + if (options.all) { + callback(null, [{ address, family }]); + return; + } + callback(null, address, family); + }, + }, }); }