diff --git a/package-lock.json b/package-lock.json index 946fe72e10..46468c8435 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,10 +21,11 @@ "node-fetch": "^2.7.0", "openid-client": "^6.1.3", "rfc4648": "^1.3.0", + "socks": "^2.8.4", "socks-proxy-agent": "^8.0.4", "stream-buffers": "^3.0.2", "tar-fs": "^3.0.9", - "undici": "^7.23.0", + "undici": "^6.24.1", "ws": "^8.18.2" }, "devDependencies": { @@ -3693,12 +3694,12 @@ "license": "MIT" }, "node_modules/undici": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.4.tgz", - "integrity": "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==", + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz", + "integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==", "license": "MIT", "engines": { - "node": ">=20.18.1" + "node": ">=18.17" } }, "node_modules/undici-types": { diff --git a/package.json b/package.json index 680c483e94..2e5e4a74fc 100644 --- a/package.json +++ b/package.json @@ -67,10 +67,11 @@ "node-fetch": "^2.7.0", "openid-client": "^6.1.3", "rfc4648": "^1.3.0", + "socks": "^2.8.4", "socks-proxy-agent": "^8.0.4", "stream-buffers": "^3.0.2", "tar-fs": "^3.0.9", - "undici": "^7.23.0", + "undici": "^6.24.1", "ws": "^8.18.2" }, "devDependencies": { diff --git a/src/config.ts b/src/config.ts index 8c669932c5..b68a492607 100644 --- a/src/config.ts +++ b/src/config.ts @@ -7,7 +7,7 @@ import net from 'node:net'; import path from 'node:path'; import { Headers, RequestInit } from 'node-fetch'; -import { Agent as UndiciAgent, ProxyAgent as UndiciProxyAgent, type Dispatcher } from 'undici'; +import { Agent as UndiciAgent, ProxyAgent as UndiciProxyAgent, buildConnector, type Dispatcher } from 'undici'; import { RequestContext } from './api.js'; import { Authenticator } from './auth.js'; import { AzureAuth } from './azure_auth.js'; @@ -37,6 +37,7 @@ import { OpenIDConnectAuth } from './oidc_auth.js'; import WebSocket from 'isomorphic-ws'; import child_process from 'node:child_process'; import { SocksProxyAgent } from 'socks-proxy-agent'; +import { SocksClient } from 'socks'; import { HttpProxyAgent, HttpProxyAgentOptions, HttpsProxyAgent, HttpsProxyAgentOptions } from 'hpagent'; import packagejson from '../package.json' with { type: 'json' }; import { setHeaderMiddleware } from './middleware.js'; @@ -47,6 +48,7 @@ import { setHeaderMiddleware } from './middleware.js'; // - ProxyAgent: ProxyAgent.Options (extends Agent.Options, adds uri, requestTls, proxyTls, etc.) export type DispatcherOptions = | { type: 'proxy'; uri: string; requestTls: tls.ConnectionOptions } + | { type: 'socks'; uri: string; requestTls: tls.ConnectionOptions } | { type: 'agent'; connect: tls.ConnectionOptions } | { type: 'none' }; @@ -72,6 +74,51 @@ function fileExists(filepath: string): boolean { } } +/** + * Creates an undici-compatible connector function that tunnels connections + * through a SOCKS proxy (v4/v4a/v5/v5h). + */ +function createSocksConnector(proxyUrl: string, tlsOptions: tls.ConnectionOptions): buildConnector.connector { + const parsedProxy = new URL(proxyUrl); + const proxyHost = parsedProxy.hostname; + const proxyPort = parseInt(parsedProxy.port, 10) || 1080; + let socksType: 4 | 5 = 5; + const proto = parsedProxy.protocol.replace(':', ''); + if (proto === 'socks4' || proto === 'socks4a') { + socksType = 4; + } + + return (options, callback) => { + const { hostname, port, protocol, servername } = options; + SocksClient.createConnection( + { + proxy: { host: proxyHost, port: proxyPort, type: socksType }, + command: 'connect', + destination: { host: hostname, port: parseInt(port, 10) }, + }, + (err, info) => { + if (err) { + callback(err, null); + return; + } + const socket = info!.socket; + if (protocol === 'https:') { + callback( + null, + tls.connect({ + ...tlsOptions, + socket, + servername: servername || hostname, + }), + ); + } else { + callback(null, socket); + } + }, + ); + }; +} + // TODO: the empty interface breaks the linter, but this type // will be needed later to get the object and cache features working again export interface ApiType {} @@ -607,6 +654,9 @@ export class KubeConfig implements SecurityAuthentication { tlsOptions.servername = (agentOptions as any).servername; if (cluster && cluster.proxyUrl) { + if (cluster.proxyUrl.startsWith('socks')) { + return { type: 'socks', uri: cluster.proxyUrl, requestTls: tlsOptions }; + } if (!cluster.server.startsWith('https') && !cluster.server.startsWith('http')) { throw new Error('Unsupported proxy type'); } @@ -628,6 +678,8 @@ export class KubeConfig implements SecurityAuthentication { switch (opts.type) { case 'proxy': return new UndiciProxyAgent({ uri: opts.uri, requestTls: opts.requestTls }); + case 'socks': + return new UndiciAgent({ connect: createSocksConnector(opts.uri, opts.requestTls) }); case 'agent': return new UndiciAgent({ connect: opts.connect }); case 'none': diff --git a/src/config_test.ts b/src/config_test.ts index 0723cb6641..2c8416e215 100644 --- a/src/config_test.ts +++ b/src/config_test.ts @@ -419,14 +419,14 @@ describe('KubeConfig', () => { await kc.applySecurityAuthentication(rc); - const dispatcher = rc.getDispatcher() as UndiciProxyAgent; - strictEqual(dispatcher instanceof UndiciProxyAgent, true); + const dispatcher = rc.getDispatcher() as UndiciAgent; + strictEqual(dispatcher instanceof UndiciAgent, true); const dispatcherOpts = kc.createDispatcherOptions(kc.getCurrentCluster(), { ca: Buffer.from('CADAT@', 'utf-8'), cert: Buffer.from('USER_CADATA', 'utf-8'), key: Buffer.from('USER_CKDATA', 'utf-8'), }); - strictEqual(dispatcherOpts.type, 'proxy'); + strictEqual(dispatcherOpts.type, 'socks'); strictEqual(dispatcherOpts.uri, 'socks5://example:1187'); }); it('should apply https proxy', async () => {