9.2 KiB
9.2 KiB
Connection Cleanup Code Patterns
Pattern 1: Safe Connection Cleanup
public initiateCleanupOnce(record: IConnectionRecord, reason: string = 'normal'): void {
// Prevent duplicate cleanup
if (record.incomingTerminationReason === null ||
record.incomingTerminationReason === undefined) {
record.incomingTerminationReason = reason;
this.incrementTerminationStat('incoming', reason);
}
this.cleanupConnection(record, reason);
}
public cleanupConnection(record: IConnectionRecord, reason: string = 'normal'): void {
if (!record.connectionClosed) {
record.connectionClosed = true;
// Remove from tracking immediately
this.connectionRecords.delete(record.id);
this.securityManager.removeConnectionByIP(record.remoteIP, record.id);
// Clear timers
if (record.cleanupTimer) {
clearTimeout(record.cleanupTimer);
record.cleanupTimer = undefined;
}
// Clean up sockets
this.cleanupSocket(record, 'incoming', record.incoming);
if (record.outgoing) {
this.cleanupSocket(record, 'outgoing', record.outgoing);
}
// Clear memory
record.pendingData = [];
record.pendingDataSize = 0;
}
}
Pattern 2: Socket Cleanup with Retry
private cleanupSocket(
record: IConnectionRecord,
side: 'incoming' | 'outgoing',
socket: plugins.net.Socket
): void {
try {
if (!socket.destroyed) {
// Graceful shutdown first
socket.end();
// Force destroy after timeout
const socketTimeout = setTimeout(() => {
try {
if (!socket.destroyed) {
socket.destroy();
}
} catch (err) {
console.log(`[${record.id}] Error destroying ${side} socket: ${err}`);
}
}, 1000);
// Don't block process exit
if (socketTimeout.unref) {
socketTimeout.unref();
}
}
} catch (err) {
console.log(`[${record.id}] Error closing ${side} socket: ${err}`);
// Fallback to destroy
try {
if (!socket.destroyed) {
socket.destroy();
}
} catch (destroyErr) {
console.log(`[${record.id}] Error destroying ${side} socket: ${destroyErr}`);
}
}
}
Pattern 3: NetworkProxy Bridge Cleanup
public async forwardToNetworkProxy(
connectionId: string,
socket: plugins.net.Socket,
record: IConnectionRecord,
initialChunk: Buffer,
networkProxyPort: number,
cleanupCallback: (reason: string) => void
): Promise<void> {
const proxySocket = new plugins.net.Socket();
// Connect to NetworkProxy
await new Promise<void>((resolve, reject) => {
proxySocket.connect(networkProxyPort, 'localhost', () => {
resolve();
});
proxySocket.on('error', reject);
});
// Send initial data
if (initialChunk) {
proxySocket.write(initialChunk);
}
// Setup bidirectional piping
socket.pipe(proxySocket);
proxySocket.pipe(socket);
// Comprehensive cleanup handler
const cleanup = (reason: string) => {
// Unpipe to prevent data loss
socket.unpipe(proxySocket);
proxySocket.unpipe(socket);
// Destroy proxy socket
proxySocket.destroy();
// Notify SmartProxy
cleanupCallback(reason);
};
// Setup all cleanup triggers
socket.on('end', () => cleanup('socket_end'));
socket.on('error', () => cleanup('socket_error'));
proxySocket.on('end', () => cleanup('proxy_end'));
proxySocket.on('error', () => cleanup('proxy_error'));
}
Pattern 4: Error Handler with Cleanup
public handleError(side: 'incoming' | 'outgoing', record: IConnectionRecord) {
return (err: Error) => {
const code = (err as any).code;
let reason = 'error';
// Map error codes to reasons
switch (code) {
case 'ECONNRESET':
reason = 'econnreset';
break;
case 'ETIMEDOUT':
reason = 'etimedout';
break;
case 'ECONNREFUSED':
reason = 'connection_refused';
break;
case 'EHOSTUNREACH':
reason = 'host_unreachable';
break;
}
// Log with context
const duration = Date.now() - record.incomingStartTime;
console.log(
`[${record.id}] ${code} on ${side} side from ${record.remoteIP}. ` +
`Duration: ${plugins.prettyMs(duration)}`
);
// Track termination reason
if (side === 'incoming' && record.incomingTerminationReason === null) {
record.incomingTerminationReason = reason;
this.incrementTerminationStat('incoming', reason);
}
// Initiate cleanup
this.initiateCleanupOnce(record, reason);
};
}
Pattern 5: Inactivity Check with Cleanup
public performInactivityCheck(): void {
const now = Date.now();
const connectionIds = [...this.connectionRecords.keys()];
for (const id of connectionIds) {
const record = this.connectionRecords.get(id);
if (!record) continue;
// Skip if disabled or immortal
if (this.settings.disableInactivityCheck ||
(record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal')) {
continue;
}
const inactivityTime = now - record.lastActivity;
let effectiveTimeout = this.settings.inactivityTimeout!;
// Extended timeout for keep-alive
if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'extended') {
effectiveTimeout *= (this.settings.keepAliveInactivityMultiplier || 6);
}
if (inactivityTime > effectiveTimeout && !record.connectionClosed) {
// Warn before closing keep-alive connections
if (record.hasKeepAlive && !record.inactivityWarningIssued) {
console.log(`[${id}] Warning: Keep-alive connection inactive`);
record.inactivityWarningIssued = true;
// Grace period
record.lastActivity = now - (effectiveTimeout - 600000);
} else {
// Close the connection
console.log(`[${id}] Closing due to inactivity`);
this.cleanupConnection(record, 'inactivity');
}
}
}
}
Pattern 6: Complete Shutdown
public clearConnections(): void {
const connectionIds = [...this.connectionRecords.keys()];
// Phase 1: Graceful end
for (const id of connectionIds) {
const record = this.connectionRecords.get(id);
if (record) {
try {
// Clear timers
if (record.cleanupTimer) {
clearTimeout(record.cleanupTimer);
record.cleanupTimer = undefined;
}
// Graceful socket end
if (record.incoming && !record.incoming.destroyed) {
record.incoming.end();
}
if (record.outgoing && !record.outgoing.destroyed) {
record.outgoing.end();
}
} catch (err) {
console.log(`Error during graceful end: ${err}`);
}
}
}
// Phase 2: Force destroy after delay
setTimeout(() => {
for (const id of connectionIds) {
const record = this.connectionRecords.get(id);
if (record) {
try {
// Remove all listeners
if (record.incoming) {
record.incoming.removeAllListeners();
if (!record.incoming.destroyed) {
record.incoming.destroy();
}
}
if (record.outgoing) {
record.outgoing.removeAllListeners();
if (!record.outgoing.destroyed) {
record.outgoing.destroy();
}
}
} catch (err) {
console.log(`Error during forced destruction: ${err}`);
}
}
}
// Clear all tracking
this.connectionRecords.clear();
this.terminationStats = { incoming: {}, outgoing: {} };
}, 100);
}
Pattern 7: Safe Event Handler Removal
// Store handlers for later removal
record.renegotiationHandler = this.tlsManager.createRenegotiationHandler(
connectionId,
serverName,
connInfo,
(connectionId, reason) => this.connectionManager.initiateCleanupOnce(record, reason)
);
// Add the handler
socket.on('data', record.renegotiationHandler);
// Remove during cleanup
if (record.incoming) {
try {
record.incoming.removeAllListeners('data');
record.renegotiationHandler = undefined;
} catch (err) {
console.log(`[${record.id}] Error removing data handlers: ${err}`);
}
}
Pattern 8: Connection State Tracking
interface IConnectionRecord {
id: string;
connectionClosed: boolean;
incomingTerminationReason: string | null;
outgoingTerminationReason: string | null;
cleanupTimer?: NodeJS.Timeout;
renegotiationHandler?: Function;
// ... other fields
}
// Check state before operations
if (!record.connectionClosed) {
// Safe to perform operations
}
// Track cleanup state
record.connectionClosed = true;
Key Principles
- Idempotency: Cleanup operations should be safe to call multiple times
- State Tracking: Always track connection and cleanup state
- Error Resilience: Handle errors during cleanup gracefully
- Resource Release: Clear all references (timers, handlers, buffers)
- Graceful First: Try graceful shutdown before forced destroy
- Comprehensive Coverage: Handle all possible termination scenarios
- Logging: Track termination reasons for debugging
- Memory Safety: Clear data structures to prevent leaks