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 # 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) ## 2025-02-25 - 3.13.0 - feat(core)
Add support for tagging iptables rules with comments and cleaning them up on process exit Add support for tagging iptables rules with comments and cleaning them up on process exit

View File

@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@push.rocks/smartproxy', 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.' 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; sniEnabled?: boolean;
defaultAllowedIPs?: string[]; defaultAllowedIPs?: string[];
preserveSourceIP?: boolean; preserveSourceIP?: boolean;
maxConnectionLifetime?: number; // New option (in milliseconds) to force cleanup of long-lived connections
} }
/** /**
@ -85,6 +86,7 @@ interface IConnectionRecord {
incomingStartTime: number; incomingStartTime: number;
outgoingStartTime?: number; outgoingStartTime?: number;
connectionClosed: boolean; connectionClosed: boolean;
cleanupTimer?: NodeJS.Timeout; // Timer to force cleanup after max lifetime/inactivity
} }
export class PortProxy { export class PortProxy {
@ -102,10 +104,11 @@ export class PortProxy {
outgoing: {}, outgoing: {},
}; };
constructor(settings: IPortProxySettings) { constructor(settingsArg: IPortProxySettings) {
this.settings = { this.settings = {
...settings, ...settingsArg,
toHost: settings.toHost || 'localhost', toHost: settingsArg.toHost || 'localhost',
maxConnectionLifetime: settingsArg.maxConnectionLifetime || 10000,
}; };
} }
@ -164,6 +167,9 @@ export class PortProxy {
const cleanupOnce = () => { const cleanupOnce = () => {
if (!connectionRecord.connectionClosed) { if (!connectionRecord.connectionClosed) {
connectionRecord.connectionClosed = true; connectionRecord.connectionClosed = true;
if (connectionRecord.cleanupTimer) {
clearTimeout(connectionRecord.cleanupTimer);
}
cleanUpSockets(connectionRecord.incoming, connectionRecord.outgoing || undefined); cleanUpSockets(connectionRecord.incoming, connectionRecord.outgoing || undefined);
this.connectionRecords.delete(connectionRecord); this.connectionRecords.delete(connectionRecord);
console.log(`Connection from ${remoteIP} terminated. Active connections: ${this.connectionRecords.size}`); console.log(`Connection from ${remoteIP} terminated. Active connections: ${this.connectionRecords.size}`);
@ -262,6 +268,7 @@ export class PortProxy {
socket.pipe(targetSocket); socket.pipe(targetSocket);
targetSocket.pipe(socket); targetSocket.pipe(socket);
// Attach error and close handlers.
socket.on('error', handleError('incoming')); socket.on('error', handleError('incoming'));
targetSocket.on('error', handleError('outgoing')); targetSocket.on('error', handleError('outgoing'));
socket.on('close', handleClose('incoming')); socket.on('close', handleClose('incoming'));
@ -284,6 +291,40 @@ export class PortProxy {
}); });
socket.on('end', handleClose('incoming')); socket.on('end', handleClose('incoming'));
targetSocket.on('end', handleClose('outgoing')); 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) { if (this.settings.sniEnabled) {