From bafa6de76d7f13591cc2014730ba02b0ab2ce9e8 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 29 May 2026 18:23:48 -0400 Subject: [PATCH] fix(proxy): cap connect tunnel timeouts --- src/infra/net/http-connect-tunnel.test.ts | 24 +++++++++++++++++++++++ src/infra/net/http-connect-tunnel.ts | 13 ++++++++---- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/infra/net/http-connect-tunnel.test.ts b/src/infra/net/http-connect-tunnel.test.ts index c4b94abad83..ae263da4178 100644 --- a/src/infra/net/http-connect-tunnel.test.ts +++ b/src/infra/net/http-connect-tunnel.test.ts @@ -1,5 +1,6 @@ import { EventEmitter } from "node:events"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { MAX_TIMER_TIMEOUT_MS } from "../../shared/number-coercion.js"; class FakeSocket extends EventEmitter { public readonly writes: string[] = []; @@ -349,4 +350,27 @@ describe("openHttpConnectTunnel", () => { await rejected; expect(proxySocket.destroyed).toBe(true); }); + + it("caps oversized CONNECT timeouts before arming the watchdog", async () => { + vi.useFakeTimers(); + const proxySocket = new FakeSocket(); + setNextNetSocket(proxySocket); + const { openHttpConnectTunnel } = await import("./http-connect-tunnel.js"); + + const tunnel = openHttpConnectTunnel({ + proxyUrl: new URL("http://proxy.example:8080"), + targetHost: "api.push.apple.com", + targetPort: 443, + timeoutMs: Number.MAX_SAFE_INTEGER, + }); + + await vi.advanceTimersByTimeAsync(1); + expect(proxySocket.destroyed).toBe(false); + + await vi.advanceTimersByTimeAsync(MAX_TIMER_TIMEOUT_MS - 1); + await expect(tunnel).rejects.toThrow( + `Proxy CONNECT failed via http://proxy.example:8080: Proxy CONNECT timed out after ${MAX_TIMER_TIMEOUT_MS}ms`, + ); + expect(proxySocket.destroyed).toBe(true); + }); }); diff --git a/src/infra/net/http-connect-tunnel.ts b/src/infra/net/http-connect-tunnel.ts index 3cbe03aab9e..0a4ce846d1a 100644 --- a/src/infra/net/http-connect-tunnel.ts +++ b/src/infra/net/http-connect-tunnel.ts @@ -1,5 +1,6 @@ import * as net from "node:net"; import * as tls from "node:tls"; +import { resolveTimerTimeoutMs } from "../../shared/number-coercion.js"; import type { ManagedProxyTlsOptions } from "./proxy/proxy-tls.js"; export type HttpConnectTunnelParams = { @@ -11,6 +12,7 @@ export type HttpConnectTunnelParams = { }; const MAX_CONNECT_RESPONSE_HEADER_BYTES = 16 * 1024; +const MIN_CONNECT_TIMEOUT_MS = 1; type ProxySocket = net.Socket | tls.TLSSocket; type ConnectResponseBuffer = Buffer; @@ -159,11 +161,14 @@ class HttpConnectTunnelAttempt { } private startTimeout(): void { - const timeoutMs = this.params.timeoutMs; - if (timeoutMs && Number.isFinite(timeoutMs) && timeoutMs > 0) { + const timeoutMs = + this.params.timeoutMs === undefined || this.params.timeoutMs <= 0 + ? undefined + : resolveTimerTimeoutMs(this.params.timeoutMs, MIN_CONNECT_TIMEOUT_MS); + if (timeoutMs !== undefined) { this.timeout = setTimeout(() => { - this.fail(new Error(`Proxy CONNECT timed out after ${Math.trunc(timeoutMs)}ms`)); - }, Math.trunc(timeoutMs)); + this.fail(new Error(`Proxy CONNECT timed out after ${timeoutMs}ms`)); + }, timeoutMs); } }