11import fs from 'node:fs' ;
22import https from 'node:https' ;
33import http from 'node:http' ;
4+ import tls from 'node:tls' ;
45import yaml from 'js-yaml' ;
56import net from 'node:net' ;
67import path from 'node:path' ;
78
89import { Headers , RequestInit } from 'node-fetch' ;
10+ import {
11+ Agent as UndiciAgent ,
12+ ProxyAgent as UndiciProxyAgent ,
13+ buildConnector ,
14+ type Dispatcher ,
15+ } from 'undici' ;
916import { RequestContext } from './api.js' ;
1017import { Authenticator } from './auth.js' ;
1118import { AzureAuth } from './azure_auth.js' ;
@@ -35,10 +42,21 @@ import { OpenIDConnectAuth } from './oidc_auth.js';
3542import WebSocket from 'isomorphic-ws' ;
3643import child_process from 'node:child_process' ;
3744import { SocksProxyAgent } from 'socks-proxy-agent' ;
45+ import { SocksClient } from 'socks' ;
3846import { HttpProxyAgent , HttpProxyAgentOptions , HttpsProxyAgent , HttpsProxyAgentOptions } from 'hpagent' ;
3947import packagejson from '../package.json' with { type : 'json' } ;
4048import { setHeaderMiddleware } from './middleware.js' ;
4149
50+ // Uses tls.ConnectionOptions for the TLS connect fields we populate (ca, cert, key, etc.).
51+ // For the full set of constructor options available when creating dispatchers, see:
52+ // - Agent: Agent.Options (extends Pool.Options -> Client.Options)
53+ // - ProxyAgent: ProxyAgent.Options (extends Agent.Options, adds uri, requestTls, proxyTls, etc.)
54+ export type DispatcherOptions =
55+ | { type : 'proxy' ; uri : string ; requestTls : tls . ConnectionOptions }
56+ | { type : 'socks' ; uri : string ; requestTls : tls . ConnectionOptions }
57+ | { type : 'agent' ; connect : tls . ConnectionOptions }
58+ | { type : 'none' } ;
59+
4260const SERVICEACCOUNT_ROOT : string = '/var/run/secrets/kubernetes.io/serviceaccount' ;
4361const SERVICEACCOUNT_CA_PATH : string = SERVICEACCOUNT_ROOT + '/ca.crt' ;
4462const SERVICEACCOUNT_TOKEN_PATH : string = SERVICEACCOUNT_ROOT + '/token' ;
@@ -61,6 +79,51 @@ function fileExists(filepath: string): boolean {
6179 }
6280}
6381
82+ /**
83+ * Creates an undici-compatible connector function that tunnels connections
84+ * through a SOCKS proxy (v4/v4a/v5/v5h).
85+ */
86+ function createSocksConnector ( proxyUrl : string , tlsOptions : tls . ConnectionOptions ) : buildConnector . connector {
87+ const parsedProxy = new URL ( proxyUrl ) ;
88+ const proxyHost = parsedProxy . hostname ;
89+ const proxyPort = parseInt ( parsedProxy . port , 10 ) || 1080 ;
90+ let socksType : 4 | 5 = 5 ;
91+ const proto = parsedProxy . protocol . replace ( ':' , '' ) ;
92+ if ( proto === 'socks4' || proto === 'socks4a' ) {
93+ socksType = 4 ;
94+ }
95+
96+ return ( options , callback ) => {
97+ const { hostname, port, protocol, servername } = options ;
98+ SocksClient . createConnection (
99+ {
100+ proxy : { host : proxyHost , port : proxyPort , type : socksType } ,
101+ command : 'connect' ,
102+ destination : { host : hostname , port : parseInt ( port , 10 ) } ,
103+ } ,
104+ ( err , info ) => {
105+ if ( err ) {
106+ callback ( err , null ) ;
107+ return ;
108+ }
109+ const socket = info ! . socket ;
110+ if ( protocol === 'https:' ) {
111+ callback (
112+ null ,
113+ tls . connect ( {
114+ ...tlsOptions ,
115+ socket,
116+ servername : servername || hostname ,
117+ } ) ,
118+ ) ;
119+ } else {
120+ callback ( null , socket ) ;
121+ }
122+ } ,
123+ ) ;
124+ } ;
125+ }
126+
64127// TODO: the empty interface breaks the linter, but this type
65128// will be needed later to get the object and cache features working again
66129export interface ApiType { }
@@ -275,7 +338,10 @@ export class KubeConfig implements SecurityAuthentication {
275338 agentOptions . rejectUnauthorized = httpsOptions . rejectUnauthorized ;
276339 }
277340
278- context . setAgent ( this . createAgent ( cluster , agentOptions ) ) ;
341+ const dispatcher = this . createDispatcher ( cluster , agentOptions ) ;
342+ if ( dispatcher !== undefined ) {
343+ context . setDispatcher ( dispatcher ) ;
344+ }
279345 }
280346
281347 /**
@@ -571,6 +637,61 @@ export class KubeConfig implements SecurityAuthentication {
571637 return agent ;
572638 }
573639
640+ /**
641+ * Build the dispatcher configuration (options + type) without constructing
642+ * the actual Dispatcher instance. Exposed as a separate method so that
643+ * tests can validate the option-mapping logic directly instead of reaching
644+ * into undici's Symbol-keyed private state.
645+ */
646+ public createDispatcherOptions (
647+ cluster : Cluster | null ,
648+ agentOptions : https . AgentOptions ,
649+ ) : DispatcherOptions {
650+ const tlsOptions : tls . ConnectionOptions = { } ;
651+ if ( agentOptions . ca !== undefined ) tlsOptions . ca = agentOptions . ca ;
652+ if ( agentOptions . cert !== undefined ) tlsOptions . cert = agentOptions . cert ;
653+ if ( agentOptions . key !== undefined ) tlsOptions . key = agentOptions . key ;
654+ if ( agentOptions . pfx !== undefined ) tlsOptions . pfx = agentOptions . pfx ;
655+ if ( agentOptions . passphrase !== undefined ) tlsOptions . passphrase = agentOptions . passphrase ;
656+ if ( agentOptions . rejectUnauthorized !== undefined )
657+ tlsOptions . rejectUnauthorized = agentOptions . rejectUnauthorized ;
658+ if ( ( agentOptions as any ) . servername !== undefined )
659+ tlsOptions . servername = ( agentOptions as any ) . servername ;
660+
661+ if ( cluster && cluster . proxyUrl ) {
662+ if ( cluster . proxyUrl . startsWith ( 'socks' ) ) {
663+ return { type : 'socks' , uri : cluster . proxyUrl , requestTls : tlsOptions } ;
664+ }
665+ if ( ! cluster . server . startsWith ( 'https' ) && ! cluster . server . startsWith ( 'http' ) ) {
666+ throw new Error ( 'Unsupported proxy type' ) ;
667+ }
668+ return { type : 'proxy' , uri : cluster . proxyUrl , requestTls : tlsOptions } ;
669+ } else if ( cluster ?. server ?. startsWith ( 'http:' ) && ! cluster . skipTLSVerify ) {
670+ throw new Error ( 'HTTP protocol is not allowed when skipTLSVerify is not set or false' ) ;
671+ }
672+ if ( Object . keys ( tlsOptions ) . length === 0 ) {
673+ return { type : 'none' } ;
674+ }
675+ return { type : 'agent' , connect : tlsOptions } ;
676+ }
677+
678+ private createDispatcher (
679+ cluster : Cluster | null ,
680+ agentOptions : https . AgentOptions ,
681+ ) : Dispatcher | undefined {
682+ const opts = this . createDispatcherOptions ( cluster , agentOptions ) ;
683+ switch ( opts . type ) {
684+ case 'proxy' :
685+ return new UndiciProxyAgent ( { uri : opts . uri , requestTls : opts . requestTls } ) ;
686+ case 'socks' :
687+ return new UndiciAgent ( { connect : createSocksConnector ( opts . uri , opts . requestTls ) } ) ;
688+ case 'agent' :
689+ return new UndiciAgent ( { connect : opts . connect } ) ;
690+ case 'none' :
691+ return undefined ;
692+ }
693+ }
694+
574695 private applyHTTPSOptions ( opts : https . RequestOptions | WebSocket . ClientOptions ) : void {
575696 const cluster = this . getCurrentCluster ( ) ;
576697 const user = this . getCurrentUser ( ) ;
0 commit comments