Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add proxy support #2111

Merged
138 changes: 138 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@
"tar": "^7.0.0",
"tmp-promise": "^3.0.2",
"tslib": "^2.5.0",
"ws": "^8.18.0"
"ws": "^8.18.0",
"socks-proxy-agent": "^8.0.4",
"hpagent": "^1.2.0"
krmodelski marked this conversation as resolved.
Show resolved Hide resolved
},
"devDependencies": {
"@types/chai": "^5.0.0",
Expand Down
25 changes: 24 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import {
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 { HttpProxyAgent, HttpProxyAgentOptions, HttpsProxyAgent, HttpsProxyAgentOptions } from 'hpagent';

const SERVICEACCOUNT_ROOT: string = '/var/run/secrets/kubernetes.io/serviceaccount';
const SERVICEACCOUNT_CA_PATH: string = SERVICEACCOUNT_ROOT + '/ca.crt';
Expand Down Expand Up @@ -248,7 +250,28 @@ export class KubeConfig implements SecurityAuthentication {
agentOptions.passphrase = httpsOptions.passphrase;
agentOptions.rejectUnauthorized = httpsOptions.rejectUnauthorized;

context.setAgent(new https.Agent(agentOptions));
let agent: https.Agent | SocksProxyAgent | HttpProxyAgent | HttpsProxyAgent;

if (cluster && cluster.proxyUrl) {
if (cluster.proxyUrl.startsWith('socks')) {
agentOptions.rejectUnauthorized = false;
krmodelski marked this conversation as resolved.
Show resolved Hide resolved
agent = new SocksProxyAgent(cluster.proxyUrl, agentOptions);
} else if (cluster.server.startsWith('https')) {
const httpsProxyAgentOptions: HttpsProxyAgentOptions = agentOptions as HttpsProxyAgentOptions;
httpsProxyAgentOptions.proxy = cluster.proxyUrl;
agent = new HttpsProxyAgent(httpsProxyAgentOptions);
} else if (cluster.server.startsWith('http')) {
const httpProxyAgentOptions: HttpProxyAgentOptions = agentOptions as HttpProxyAgentOptions;
httpProxyAgentOptions.proxy = cluster.proxyUrl;
agent = new HttpProxyAgent(httpProxyAgentOptions);
} else {
throw new Error('Unsupported proxy type');
}
} else {
agent = new https.Agent(agentOptions);
}

context.setAgent(agent);
}

/**
Expand Down
67 changes: 67 additions & 0 deletions src/config_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,15 @@ import { CoreV1Api, RequestContext } from './api.js';
import { bufferFromFileOrString, findHomeDir, findObject, KubeConfig, makeAbsolutePath } from './config.js';
import { ActionOnInvalid, Cluster, newClusters, newContexts, newUsers, User } from './config_types.js';
import { ExecAuth } from './exec_auth.js';
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
import { SocksProxyAgent } from 'socks-proxy-agent';

const kcFileName = 'testdata/kubeconfig.yaml';
const kc2FileName = 'testdata/kubeconfig-2.yaml';
const kcDupeCluster = 'testdata/kubeconfig-dupe-cluster.yaml';
const kcDupeContext = 'testdata/kubeconfig-dupe-context.yaml';
const kcDupeUser = 'testdata/kubeconfig-dupe-user.yaml';
const kcProxyUrl = 'testdata/kubeconfig-proxy-url.yaml';

const kcNoUserFileName = 'testdata/empty-user-kubeconfig.yaml';
const kcInvalidContextFileName = 'testdata/empty-context-kubeconfig.yaml';
Expand All @@ -43,6 +46,7 @@ function validateFileLoad(kc: KubeConfig) {
expect(cluster1.name).to.equal('cluster1');
expect(cluster1.caData).to.equal('Q0FEQVRB');
expect(cluster1.server).to.equal('http://example.com');
expect(cluster1.proxyUrl).to.equal('socks5://localhost:1181');
expect(cluster2.name).to.equal('cluster2');
expect(cluster2.caData).to.equal('Q0FEQVRBMg==');
expect(cluster2.server).to.equal('http://example2.com');
Expand Down Expand Up @@ -358,6 +362,69 @@ describe('KubeConfig', () => {

assertRequestOptionsEqual(opts, expectedOptions);
});
it('should apply socks proxy', async () => {
const kc = new KubeConfig();
kc.loadFromFile(kcProxyUrl);
kc.setCurrentContext('contextA');

const testServerName = 'https://example.com';
const rc = new RequestContext(testServerName, HttpMethod.GET);

await kc.applySecurityAuthentication(rc);
const expectedCA = Buffer.from('CADAT@', 'utf-8');
const expectedProxyHost = 'example';
const expectedProxyPort = 1187;

expect(rc.getAgent()).to.be.instanceOf(SocksProxyAgent);
const agent = rc.getAgent() as SocksProxyAgent;
expect(agent.options.ca?.toString()).to.equal(expectedCA.toString());
expect(agent.proxy.host).to.equal(expectedProxyHost);
expect(agent.proxy.port).to.equal(expectedProxyPort);
});
it('should apply https proxy', async () => {
const kc = new KubeConfig();
kc.loadFromFile(kcProxyUrl);
kc.setCurrentContext('contextB');

const testServerName = 'https://example.com';
const rc = new RequestContext(testServerName, HttpMethod.GET);

await kc.applySecurityAuthentication(rc);
const expectedCA = Buffer.from('CADAT@', 'utf-8');
const expectedProxyHref = 'http://example:9443/';

expect(rc.getAgent()).to.be.instanceOf(HttpsProxyAgent);
const agent = rc.getAgent() as HttpsProxyAgent;
expect(agent.options.ca?.toString()).to.equal(expectedCA.toString());
expect(agent.proxy.href).to.equal(expectedProxyHref);
});
it('should apply http proxy', async () => {
const kc = new KubeConfig();
kc.loadFromFile(kcProxyUrl);
kc.setCurrentContext('contextC');

const testServerName = 'https://example.com';
const rc = new RequestContext(testServerName, HttpMethod.GET);

await kc.applySecurityAuthentication(rc);
const expectedCA = Buffer.from('CADAT@', 'utf-8');
const expectedProxyHref = 'http://example:8080/';

expect(rc.getAgent()).to.be.instanceOf(HttpProxyAgent);
const agent = rc.getAgent() as HttpProxyAgent;
expect(agent.options.ca?.toString()).to.equal(expectedCA.toString());
expect(agent.proxy.href).to.equal(expectedProxyHref);
});
it('should throw an error if proxy-url is provided but the server protocol is not http or https', async () => {
const kc = new KubeConfig();
kc.loadFromFile(kcProxyUrl);
kc.setCurrentContext('contextD');

const testServerName = 'https://example.com';
const rc = new RequestContext(testServerName, HttpMethod.GET);

return expect(kc.applySecurityAuthentication(rc)).to.be.rejectedWith('Unsupported proxy type');
});
});

describe('loadClusterConfigObjects', () => {
Expand Down
3 changes: 3 additions & 0 deletions src/config_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface Cluster {
readonly server: string;
readonly tlsServerName?: string;
readonly skipTLSVerify: boolean;
readonly proxyUrl?: string;
}

export function newClusters(a: any, opts?: Partial<ConfigOptions>): Cluster[] {
Expand All @@ -43,6 +44,7 @@ export function exportCluster(cluster: Cluster): any {
'certificate-authority': cluster.caFile,
'insecure-skip-tls-verify': cluster.skipTLSVerify,
'tls-server-name': cluster.tlsServerName,
'proxy-url': cluster.proxyUrl,
},
};
}
Expand All @@ -68,6 +70,7 @@ function clusterIterator(
server: elt.cluster.server.replace(/\/$/, ''),
skipTLSVerify: elt.cluster['insecure-skip-tls-verify'] === true,
tlsServerName: elt.cluster['tls-server-name'],
proxyUrl: elt.cluster['proxy-url'],
};
} catch (err) {
switch (onInvalidEntry) {
Expand Down
Loading