Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .projen/deps.json

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

2 changes: 1 addition & 1 deletion .projenrc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ArrowParens, TrailingComma } from 'projen/lib/javascript';
const project = new awscdk.AwsCdkConstructLibrary({
author: 'rehanvdm',
authorAddress: 'rehan.vdm+github-tailscale-lambda-proxy@gmail.com',
cdkVersion: '2.150.0',
cdkVersion: '2.176.0',
defaultReleaseBranch: 'main',
jsiiVersion: '~5.7.0',
name: 'tailscale-lambda-proxy',
Expand Down
11 changes: 11 additions & 0 deletions API.md

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

3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ export class MyStack extends cdk.Stack {
// lambda: {
// functionName: "tailscale-proxy",
// }
// }
// },
// debug: true, // Enable debug logging, show request + response, search for lines starting with "[tailscale-"
});

const caller = new NodejsFunction(this, "tailscale-caller", {
Expand Down
78 changes: 55 additions & 23 deletions package-lock.json

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

4 changes: 2 additions & 2 deletions package.json

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

3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export interface TailscaleLambdaProxyProps {
readonly tsHostname: string;

readonly options?: TailscaleLambdaProxyPropsOptions;

readonly debug?: boolean;
}

export class TailscaleLambdaProxy extends Construct {
Expand All @@ -52,6 +54,7 @@ export class TailscaleLambdaProxy extends Construct {
environment: {
TS_SECRET_API_KEY: props.tsSecretApiKey.secretArn,
TS_HOSTNAME: props.tsHostname,
...(props?.debug) ? { DEBUG: 'true' } : { },
...(props.options?.lambda?.nodeTlsRejectUnauthorized === false) ? { NODE_TLS_REJECT_UNAUTHORIZED: '0' } : { },
},
timeout: cdk.Duration.minutes(15),
Expand Down
20 changes: 19 additions & 1 deletion src/lambda/tailscale-proxy/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,20 @@ const AWS_SPECIFIC_HEADERS = [
'x-amz-content-sha256',
];

export function logError(...args: any[]) {
console.error('[tailscale-proxy] ERROR', ...args);
}
export function logInfo(...args: any[]) {
console.log('[tailscale-proxy] INFO', ...args);
}
export function logDebug(...args: any[]) {
if (process.env.DEBUG) {
console.log('[tailscale-proxy] DEBUG', ...args);
}
}

export async function handler(event: APIGatewayProxyEventV2): Promise<APIGatewayProxyResultV2> {
logDebug('Event:', JSON.stringify(event, null, 2));
let metrics: Metrics | undefined;
try {
let isHttps = undefined; // Auto-detect, will be set for port 443
Expand Down Expand Up @@ -65,6 +78,9 @@ export async function handler(event: APIGatewayProxyEventV2): Promise<APIGateway
targetHeaders['x-amz-content-sha256'] = event.headers['ts-x-amz-content-sha256'] as string;
}

const body = event.body
? (event.isBase64Encoded ? Buffer.from(event.body, 'base64').toString('utf-8') : event.body)
: undefined;
const response = await proxyHttpRequest({
hostname: event.headers['ts-target-ip'],
port: event.headers['ts-target-port'],
Expand All @@ -73,10 +89,12 @@ export async function handler(event: APIGatewayProxyEventV2): Promise<APIGateway
path: event.requestContext.http.path,
headers: targetHeaders,
method: event.requestContext.http.method,
body: event.body,
body: body,
});

metrics?.addMetric('success', MetricUnit.Count, 1);

logDebug('Response', JSON.stringify(response, null, 2));
return response;
} catch (_err) {
metrics?.addMetric('error', MetricUnit.Count, 1);
Expand Down
13 changes: 8 additions & 5 deletions src/lambda/tailscale-proxy/proxy-http-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as http from 'http';
import * as https from 'https';
import { APIGatewayProxyResultV2 } from 'aws-lambda';
import { SocksProxyAgent } from 'socks-proxy-agent';
import { logDebug, logInfo, logError } from './index';

export async function proxyHttpRequest(
target: Pick<http.RequestOptions, 'hostname' | 'port'>,
Expand All @@ -14,6 +15,7 @@ export async function proxyHttpRequest(
},
): Promise<APIGatewayProxyResultV2> {
async function requestPromise(): Promise<APIGatewayProxyResultV2> {
logDebug('proxyHttpRequest', JSON.stringify({ target, isHttps, request }, null, 2));
const socksProxyAgent = new SocksProxyAgent('socks://localhost:1055');
return new Promise((resolve, reject) => {
const chunks: Buffer[] = [];
Expand All @@ -32,6 +34,7 @@ export async function proxyHttpRequest(
});
res.on('end', () => {
const responseBody = Buffer.concat(chunks);
logDebug('requestPromise.end responseBody', responseBody);
resolve({
statusCode: res.statusCode || 500,
headers: res.headers as Record<string, string>,
Expand All @@ -40,13 +43,13 @@ export async function proxyHttpRequest(
});
});
res.on('error', (error: Error): void => {
console.error('Error receiving response:', error);
logError('Error receiving response:', error);
reject(error);
});
});

apiRequest.on('error', (error: Error): void => {
console.error('Error sending request:', error);
logError('Error sending request:', error);
reject(error);
});

Expand All @@ -68,8 +71,8 @@ export async function proxyHttpRequest(
success = true;
} catch (error) {
if (error == 'Error: Socks5 proxy rejected connection - Failure' && attempt < connectionRetryDelays.length) {
console.error('Error: Socks5 proxy rejected connection - Failure');
console.log('Retrying in', connectionRetryDelays[attempt], 'ms');
logError('Error: Socks5 proxy rejected connection - Failure');
logInfo('Retrying in', connectionRetryDelays[attempt], 'ms');
await new Promise((resolve) => setTimeout(resolve, connectionRetryDelays[attempt]));
attempt++;
} else {
Expand All @@ -79,7 +82,7 @@ export async function proxyHttpRequest(
} while (!success && attempt < connectionRetryDelays.length);

if (attempt > 0) {
console.log('Error: Socks5 proxy rejected connection - Failure - RESOLVED - attempt:', attempt, 'total delay time:', connectionRetryDelays.slice(0, attempt).reduce((a, b) => a + b, 0));
logInfo('Error: Socks5 proxy rejected connection - Failure - RESOLVED - attempt:', attempt, 'total delay time:', connectionRetryDelays.slice(0, attempt).reduce((a, b) => a + b, 0));
}

return response!;
Expand Down