feat(PortProxy): Introduce max connection lifetime feature

This commit is contained in:
Philipp Kunz 2025-02-26 10:29:21 +00:00
parent be31a9b553
commit 23253a2731
3 changed files with 51 additions and 4 deletions

View File

@ -1,5 +1,11 @@
# Changelog
## 2025-02-26 - 3.14.0 - feat(PortProxy)
Introduce max connection lifetime feature
- Added an optional maxConnectionLifetime setting for PortProxy.
- Forces cleanup of long-lived connections based on inactivity or lifetime limit.
## 2025-02-25 - 3.13.0 - feat(core)
Add support for tagging iptables rules with comments and cleaning them up on process exit

View File

@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@push.rocks/smartproxy',
version: '3.13.0',
version: '3.14.0',
description: 'A robust and versatile proxy package designed to handle high workloads, offering features like SSL redirection, port proxying, WebSocket support, and customizable routing and authentication.'
}

View File

@ -14,6 +14,7 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions {
sniEnabled?: boolean;
defaultAllowedIPs?: string[];
preserveSourceIP?: boolean;
maxConnectionLifetime?: number; // New option (in milliseconds) to force cleanup of long-lived connections
}
/**
@ -85,6 +86,7 @@ interface IConnectionRecord {
incomingStartTime: number;
outgoingStartTime?: number;
connectionClosed: boolean;
cleanupTimer?: NodeJS.Timeout; // Timer to force cleanup after max lifetime/inactivity
}
export class PortProxy {
@ -102,10 +104,11 @@ export class PortProxy {
outgoing: {},
};
constructor(settings: IPortProxySettings) {
constructor(settingsArg: IPortProxySettings) {
this.settings = {
...settings,
toHost: settings.toHost || 'localhost',
...settingsArg,
toHost: settingsArg.toHost || 'localhost',
maxConnectionLifetime: settingsArg.maxConnectionLifetime || 10000,
};
}
@ -164,6 +167,9 @@ export class PortProxy {
const cleanupOnce = () => {
if (!connectionRecord.connectionClosed) {
connectionRecord.connectionClosed = true;
if (connectionRecord.cleanupTimer) {
clearTimeout(connectionRecord.cleanupTimer);
}
cleanUpSockets(connectionRecord.incoming, connectionRecord.outgoing || undefined);
this.connectionRecords.delete(connectionRecord);
console.log(`Connection from ${remoteIP} terminated. Active connections: ${this.connectionRecords.size}`);
@ -262,6 +268,7 @@ export class PortProxy {
socket.pipe(targetSocket);
targetSocket.pipe(socket);
// Attach error and close handlers.
socket.on('error', handleError('incoming'));
targetSocket.on('error', handleError('outgoing'));
socket.on('close', handleClose('incoming'));
@ -284,6 +291,40 @@ export class PortProxy {
});
socket.on('end', handleClose('incoming'));
targetSocket.on('end', handleClose('outgoing'));
// If maxConnectionLifetime is set, initialize a cleanup timer that will be reset on data flow.
if (this.settings.maxConnectionLifetime) {
let incomingActive = false;
let outgoingActive = false;
const resetCleanupTimer = () => {
if (this.settings.maxConnectionLifetime) {
if (connectionRecord.cleanupTimer) {
clearTimeout(connectionRecord.cleanupTimer);
}
connectionRecord.cleanupTimer = setTimeout(() => {
console.log(`Connection from ${remoteIP} exceeded max lifetime with inactivity (${this.settings.maxConnectionLifetime}ms), forcing cleanup.`);
cleanupOnce();
}, this.settings.maxConnectionLifetime);
}
};
// Start the cleanup timer.
resetCleanupTimer();
// Listen for data events on both sides and reset the timer when both are active.
socket.on('data', () => {
incomingActive = true;
if (incomingActive && outgoingActive) {
resetCleanupTimer();
}
});
targetSocket.on('data', () => {
outgoingActive = true;
if (incomingActive && outgoingActive) {
resetCleanupTimer();
}
});
}
};
if (this.settings.sniEnabled) {