341 lines
9.2 KiB
Markdown
341 lines
9.2 KiB
Markdown
|
# Connection Cleanup Code Patterns
|
||
|
|
||
|
## Pattern 1: Safe Connection Cleanup
|
||
|
|
||
|
```typescript
|
||
|
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
|
||
|
|
||
|
```typescript
|
||
|
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
|
||
|
|
||
|
```typescript
|
||
|
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
|
||
|
|
||
|
```typescript
|
||
|
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
|
||
|
|
||
|
```typescript
|
||
|
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
|
||
|
|
||
|
```typescript
|
||
|
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
|
||
|
|
||
|
```typescript
|
||
|
// 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
|
||
|
|
||
|
```typescript
|
||
|
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
|
||
|
|
||
|
1. **Idempotency**: Cleanup operations should be safe to call multiple times
|
||
|
2. **State Tracking**: Always track connection and cleanup state
|
||
|
3. **Error Resilience**: Handle errors during cleanup gracefully
|
||
|
4. **Resource Release**: Clear all references (timers, handlers, buffers)
|
||
|
5. **Graceful First**: Try graceful shutdown before forced destroy
|
||
|
6. **Comprehensive Coverage**: Handle all possible termination scenarios
|
||
|
7. **Logging**: Track termination reasons for debugging
|
||
|
8. **Memory Safety**: Clear data structures to prevent leaks
|