smartproxy/Connection-Cleanup-Patterns.md
2025-05-19 12:04:26 +00:00

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

  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