File tree Expand file tree Collapse file tree
Expand file tree Collapse file tree Original file line number Diff line number Diff line change @@ -17,6 +17,7 @@ import { dangerouslyBypassManagedProxyForGatewayLoopbackControlPlane } from "../
1717import { normalizeFingerprint } from "../infra/tls/fingerprint.js" ;
1818import { rawDataToString } from "../infra/ws.js" ;
1919import { logDebug , logError } from "../logger.js" ;
20+ import { isLoopbackIpAddress } from "../shared/net/ip.js" ;
2021import {
2122 normalizeLowercaseStringOrEmpty ,
2223 normalizeOptionalString ,
@@ -99,7 +100,7 @@ function createDirectGatewayAgent(url: string): http.Agent | https.Agent | undef
99100 } catch {
100101 return undefined ;
101102 }
102- if ( ! isLoopbackHost ( hostname ) ) {
103+ if ( ! isLoopbackIpAddress ( hostname ) ) {
103104 return undefined ;
104105 }
105106 return url . startsWith ( "wss://" ) ? new https . Agent ( ) : new http . Agent ( ) ;
Original file line number Diff line number Diff line change @@ -97,6 +97,22 @@ describe("GatewayClient", () => {
9797 ) ;
9898 } ) ;
9999
100+ test ( "uses an explicit direct agent for IPv6 loopback control-plane WebSocket connections" , ( ) => {
101+ const client = new GatewayClient ( { url : "ws://[::1]:1" } ) ;
102+ client . start ( ) ;
103+ const last = wsMockState . last as { opts : { agent ?: unknown } } | null ;
104+
105+ expect ( last ?. opts . agent ) . toBeDefined ( ) ;
106+ } ) ;
107+
108+ test ( "does not use the direct control-plane bypass for localhost hostnames" , ( ) => {
109+ const client = new GatewayClient ( { url : "ws://localhost:1" } ) ;
110+ client . start ( ) ;
111+ const last = wsMockState . last as { opts : { agent ?: unknown } } | null ;
112+
113+ expect ( last ?. opts . agent ) . toBeUndefined ( ) ;
114+ } ) ;
115+
100116 test ( "does not force a direct agent for remote Gateway WebSocket connections" , ( ) => {
101117 const client = new GatewayClient ( {
102118 url : "wss://gateway.example.com" ,
Original file line number Diff line number Diff line change @@ -332,6 +332,24 @@ describe("startProxy", () => {
332332 await stopProxy ( handle ) ;
333333 } ) ;
334334
335+ it ( "allows the Gateway control-plane bypass for literal loopback IPs only" , ( ) => {
336+ expect (
337+ dangerouslyBypassManagedProxyForGatewayLoopbackControlPlane (
338+ "ws://127.0.0.1:18789" ,
339+ ( ) => "ok" ,
340+ ) ,
341+ ) . toBe ( "ok" ) ;
342+ expect (
343+ dangerouslyBypassManagedProxyForGatewayLoopbackControlPlane ( "ws://[::1]:18789" , ( ) => "ok" ) ,
344+ ) . toBe ( "ok" ) ;
345+ expect ( ( ) =>
346+ dangerouslyBypassManagedProxyForGatewayLoopbackControlPlane (
347+ "ws://localhost:18789" ,
348+ ( ) => undefined ,
349+ ) ,
350+ ) . toThrow ( "loopback-only" ) ;
351+ } ) ;
352+
335353 it ( "rejects dangerous Gateway control-plane bypass for non-loopback URLs" , ( ) => {
336354 expect ( ( ) =>
337355 dangerouslyBypassManagedProxyForGatewayLoopbackControlPlane (
Original file line number Diff line number Diff line change @@ -12,6 +12,7 @@ import https from "node:https";
1212import { bootstrap as bootstrapGlobalAgent } from "global-agent" ;
1313import type { ProxyConfig } from "../../../config/zod-schema.proxy.js" ;
1414import { logInfo , logWarn } from "../../../logger.js" ;
15+ import { isLoopbackIpAddress } from "../../../shared/net/ip.js" ;
1516import { forceResetGlobalDispatcher } from "../undici-global-dispatcher.js" ;
1617
1718export type ProxyHandle = {
@@ -287,13 +288,7 @@ function isGatewayLoopbackControlPlaneUrl(value: string): boolean {
287288 ) {
288289 return false ;
289290 }
290- const hostname = url . hostname . toLowerCase ( ) ;
291- return (
292- hostname === "localhost" ||
293- hostname === "127.0.0.1" ||
294- hostname === "::1" ||
295- hostname === "[::1]"
296- ) ;
291+ return isLoopbackIpAddress ( url . hostname ) ;
297292}
298293
299294export function dangerouslyBypassManagedProxyForGatewayLoopbackControlPlane < T > (
You can’t perform that action at this time.
0 commit comments