update
This commit is contained in:
parent
f9bcbf4bfc
commit
02e77655ad
341
Connection-Cleanup-Patterns.md
Normal file
341
Connection-Cleanup-Patterns.md
Normal file
@ -0,0 +1,341 @@
|
|||||||
|
# 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
|
248
Connection-Termination-Issues.md
Normal file
248
Connection-Termination-Issues.md
Normal file
@ -0,0 +1,248 @@
|
|||||||
|
# Connection Termination Issues and Solutions in SmartProxy/NetworkProxy
|
||||||
|
|
||||||
|
## Common Connection Termination Scenarios
|
||||||
|
|
||||||
|
### 1. Normal Connection Closure
|
||||||
|
|
||||||
|
**Flow**:
|
||||||
|
- Client or server initiates graceful close
|
||||||
|
- 'close' event triggers cleanup
|
||||||
|
- Connection removed from tracking
|
||||||
|
- Resources freed
|
||||||
|
|
||||||
|
**Code Path**:
|
||||||
|
```typescript
|
||||||
|
// In ConnectionManager
|
||||||
|
handleClose(side: 'incoming' | 'outgoing', record: IConnectionRecord) {
|
||||||
|
record.incomingTerminationReason = 'normal';
|
||||||
|
this.initiateCleanupOnce(record, 'closed_' + side);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Error-Based Termination
|
||||||
|
|
||||||
|
**Common Errors**:
|
||||||
|
- ECONNRESET: Connection reset by peer
|
||||||
|
- ETIMEDOUT: Connection timed out
|
||||||
|
- ECONNREFUSED: Connection refused
|
||||||
|
- EHOSTUNREACH: Host unreachable
|
||||||
|
|
||||||
|
**Handling**:
|
||||||
|
```typescript
|
||||||
|
handleError(side: 'incoming' | 'outgoing', record: IConnectionRecord) {
|
||||||
|
return (err: Error) => {
|
||||||
|
const code = (err as any).code;
|
||||||
|
let reason = 'error';
|
||||||
|
|
||||||
|
if (code === 'ECONNRESET') {
|
||||||
|
reason = 'econnreset';
|
||||||
|
} else if (code === 'ETIMEDOUT') {
|
||||||
|
reason = 'etimedout';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.initiateCleanupOnce(record, reason);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Inactivity Timeout
|
||||||
|
|
||||||
|
**Detection**:
|
||||||
|
```typescript
|
||||||
|
performInactivityCheck(): void {
|
||||||
|
const now = Date.now();
|
||||||
|
for (const record of this.connectionRecords.values()) {
|
||||||
|
const inactivityTime = now - record.lastActivity;
|
||||||
|
if (inactivityTime > effectiveTimeout) {
|
||||||
|
this.cleanupConnection(record, 'inactivity');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Special Cases**:
|
||||||
|
- Keep-alive connections get extended timeouts
|
||||||
|
- "Immortal" connections bypass inactivity checks
|
||||||
|
- Warning issued before closure for keep-alive connections
|
||||||
|
|
||||||
|
### 4. NFTables-Handled Connections
|
||||||
|
|
||||||
|
**Special Handling**:
|
||||||
|
```typescript
|
||||||
|
if (route.action.forwardingEngine === 'nftables') {
|
||||||
|
socket.end();
|
||||||
|
record.nftablesHandled = true;
|
||||||
|
this.connectionManager.initiateCleanupOnce(record, 'nftables_handled');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
These connections are:
|
||||||
|
- Handled at kernel level
|
||||||
|
- Closed immediately at application level
|
||||||
|
- Tracked for metrics only
|
||||||
|
|
||||||
|
### 5. NetworkProxy Bridge Termination
|
||||||
|
|
||||||
|
**Bridge Cleanup**:
|
||||||
|
```typescript
|
||||||
|
const cleanup = (reason: string) => {
|
||||||
|
socket.unpipe(proxySocket);
|
||||||
|
proxySocket.unpipe(socket);
|
||||||
|
proxySocket.destroy();
|
||||||
|
cleanupCallback(reason);
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.on('end', () => cleanup('socket_end'));
|
||||||
|
socket.on('error', () => cleanup('socket_error'));
|
||||||
|
proxySocket.on('end', () => cleanup('proxy_end'));
|
||||||
|
proxySocket.on('error', () => cleanup('proxy_error'));
|
||||||
|
```
|
||||||
|
|
||||||
|
## Preventing Connection Leaks
|
||||||
|
|
||||||
|
### 1. Always Remove Event Listeners
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
cleanupConnection(record: IConnectionRecord, reason: string): void {
|
||||||
|
if (record.incoming) {
|
||||||
|
record.incoming.removeAllListeners('data');
|
||||||
|
record.renegotiationHandler = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Clear Timers
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
if (record.cleanupTimer) {
|
||||||
|
clearTimeout(record.cleanupTimer);
|
||||||
|
record.cleanupTimer = undefined;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Proper Socket Cleanup
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
private cleanupSocket(record: IConnectionRecord, side: string, socket: net.Socket): void {
|
||||||
|
try {
|
||||||
|
if (!socket.destroyed) {
|
||||||
|
socket.end(); // Graceful
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!socket.destroyed) {
|
||||||
|
socket.destroy(); // Forced
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`Error closing ${side} socket: ${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Connection Record Cleanup
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Clear pending data to prevent memory leaks
|
||||||
|
record.pendingData = [];
|
||||||
|
record.pendingDataSize = 0;
|
||||||
|
|
||||||
|
// Remove from tracking map
|
||||||
|
this.connectionRecords.delete(record.id);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Monitoring and Debugging
|
||||||
|
|
||||||
|
### 1. Termination Statistics
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
private terminationStats: {
|
||||||
|
incoming: Record<string, number>;
|
||||||
|
outgoing: Record<string, number>;
|
||||||
|
} = { incoming: {}, outgoing: {} };
|
||||||
|
|
||||||
|
incrementTerminationStat(side: 'incoming' | 'outgoing', reason: string): void {
|
||||||
|
this.terminationStats[side][reason] = (this.terminationStats[side][reason] || 0) + 1;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Connection Logging
|
||||||
|
|
||||||
|
**Detailed Logging**:
|
||||||
|
```typescript
|
||||||
|
console.log(
|
||||||
|
`[${record.id}] Connection from ${record.remoteIP} terminated (${reason}).` +
|
||||||
|
` Duration: ${prettyMs(duration)}, Bytes IN: ${bytesReceived}, OUT: ${bytesSent}`
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Active Connection Tracking
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
getConnectionCount(): number {
|
||||||
|
return this.connectionRecords.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
// In NetworkProxy
|
||||||
|
metrics = {
|
||||||
|
activeConnections: this.connectedClients,
|
||||||
|
portProxyConnections: this.portProxyConnections,
|
||||||
|
tlsTerminatedConnections: this.tlsTerminatedConnections
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices for Connection Termination
|
||||||
|
|
||||||
|
1. **Always Use initiateCleanupOnce()**:
|
||||||
|
- Prevents duplicate cleanup operations
|
||||||
|
- Ensures proper termination reason tracking
|
||||||
|
|
||||||
|
2. **Handle All Socket Events**:
|
||||||
|
- 'error', 'close', 'end' events
|
||||||
|
- Both incoming and outgoing sockets
|
||||||
|
|
||||||
|
3. **Implement Proper Timeouts**:
|
||||||
|
- Initial data timeout
|
||||||
|
- Inactivity timeout
|
||||||
|
- Maximum connection lifetime
|
||||||
|
|
||||||
|
4. **Track Resources**:
|
||||||
|
- Connection records
|
||||||
|
- Socket maps
|
||||||
|
- Timer references
|
||||||
|
|
||||||
|
5. **Log Termination Reasons**:
|
||||||
|
- Helps debug connection issues
|
||||||
|
- Provides metrics for monitoring
|
||||||
|
|
||||||
|
6. **Graceful Shutdown**:
|
||||||
|
- Try socket.end() before socket.destroy()
|
||||||
|
- Allow time for graceful closure
|
||||||
|
|
||||||
|
7. **Memory Management**:
|
||||||
|
- Clear pending data buffers
|
||||||
|
- Remove event listeners
|
||||||
|
- Delete connection records
|
||||||
|
|
||||||
|
## Common Issues and Solutions
|
||||||
|
|
||||||
|
### Issue: Memory Leaks from Event Listeners
|
||||||
|
**Solution**: Always call removeAllListeners() during cleanup
|
||||||
|
|
||||||
|
### Issue: Orphaned Connections
|
||||||
|
**Solution**: Implement multiple cleanup triggers (timeout, error, close)
|
||||||
|
|
||||||
|
### Issue: Duplicate Cleanup Operations
|
||||||
|
**Solution**: Use connectionClosed flag and initiateCleanupOnce()
|
||||||
|
|
||||||
|
### Issue: Hanging Connections
|
||||||
|
**Solution**: Implement inactivity checks and maximum lifetime limits
|
||||||
|
|
||||||
|
### Issue: Resource Exhaustion
|
||||||
|
**Solution**: Track connection counts and implement limits
|
||||||
|
|
||||||
|
### Issue: Lost Data During Cleanup
|
||||||
|
**Solution**: Use proper unpipe operations and graceful shutdown
|
||||||
|
|
||||||
|
### Issue: Debugging Connection Issues
|
||||||
|
**Solution**: Track termination reasons and maintain detailed logs
|
153
NetworkProxy-SmartProxy-Connection-Management.md
Normal file
153
NetworkProxy-SmartProxy-Connection-Management.md
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
# NetworkProxy Connection Termination and SmartProxy Connection Handling
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The connection management between NetworkProxy and SmartProxy involves complex coordination to handle TLS termination, connection forwarding, and proper cleanup. This document outlines how these systems work together.
|
||||||
|
|
||||||
|
## SmartProxy Connection Management
|
||||||
|
|
||||||
|
### Connection Tracking (ConnectionManager)
|
||||||
|
|
||||||
|
1. **Connection Lifecycle**:
|
||||||
|
- New connections are registered in `ConnectionManager.createConnection()`
|
||||||
|
- Each connection gets a unique ID and tracking record
|
||||||
|
- Connection records track both incoming (client) and outgoing (target) sockets
|
||||||
|
- Connections are removed from tracking upon cleanup
|
||||||
|
|
||||||
|
2. **Connection Cleanup Flow**:
|
||||||
|
```
|
||||||
|
initiateCleanupOnce() -> cleanupConnection() -> cleanupSocket()
|
||||||
|
```
|
||||||
|
- `initiateCleanupOnce()`: Prevents duplicate cleanup operations
|
||||||
|
- `cleanupConnection()`: Main cleanup logic, removes connections from tracking
|
||||||
|
- `cleanupSocket()`: Handles socket termination (graceful end, then forced destroy)
|
||||||
|
|
||||||
|
3. **Cleanup Triggers**:
|
||||||
|
- Socket errors (ECONNRESET, ETIMEDOUT, etc.)
|
||||||
|
- Socket close events
|
||||||
|
- Inactivity timeouts
|
||||||
|
- Connection lifetime limits
|
||||||
|
- Manual cleanup (e.g., NFTables-handled connections)
|
||||||
|
|
||||||
|
## NetworkProxy Integration
|
||||||
|
|
||||||
|
### NetworkProxyBridge
|
||||||
|
|
||||||
|
The `NetworkProxyBridge` class manages the connection between SmartProxy and NetworkProxy:
|
||||||
|
|
||||||
|
1. **Connection Forwarding**:
|
||||||
|
```typescript
|
||||||
|
forwardToNetworkProxy(
|
||||||
|
connectionId: string,
|
||||||
|
socket: net.Socket,
|
||||||
|
record: IConnectionRecord,
|
||||||
|
initialChunk: Buffer,
|
||||||
|
networkProxyPort: number,
|
||||||
|
cleanupCallback: (reason: string) => void
|
||||||
|
)
|
||||||
|
```
|
||||||
|
- Creates a new socket connection to NetworkProxy
|
||||||
|
- Pipes data between client and NetworkProxy sockets
|
||||||
|
- Sets up cleanup handlers for both sockets
|
||||||
|
|
||||||
|
2. **Cleanup Coordination**:
|
||||||
|
- When either socket ends or errors, both are cleaned up
|
||||||
|
- Cleanup callback notifies SmartProxy's ConnectionManager
|
||||||
|
- Proper unpipe operations prevent memory leaks
|
||||||
|
|
||||||
|
## NetworkProxy Connection Tracking
|
||||||
|
|
||||||
|
### Connection Tracking in NetworkProxy
|
||||||
|
|
||||||
|
1. **Raw TCP Connection Tracking**:
|
||||||
|
```typescript
|
||||||
|
setupConnectionTracking(): void {
|
||||||
|
this.httpsServer.on('connection', (connection: net.Socket) => {
|
||||||
|
// Track connections in socketMap
|
||||||
|
this.socketMap.add(connection);
|
||||||
|
|
||||||
|
// Setup cleanup handlers
|
||||||
|
connection.on('close', cleanupConnection);
|
||||||
|
connection.on('error', cleanupConnection);
|
||||||
|
connection.on('end', cleanupConnection);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **SmartProxy Connection Detection**:
|
||||||
|
- Connections from localhost (127.0.0.1) are identified as SmartProxy connections
|
||||||
|
- Special counter tracks `portProxyConnections`
|
||||||
|
- Connection counts are updated when connections close
|
||||||
|
|
||||||
|
3. **Metrics and Monitoring**:
|
||||||
|
- Active connections tracked in `connectedClients`
|
||||||
|
- TLS handshake completions tracked in `tlsTerminatedConnections`
|
||||||
|
- Connection pool status monitored periodically
|
||||||
|
|
||||||
|
## Connection Termination Flow
|
||||||
|
|
||||||
|
### Typical TLS Termination Flow:
|
||||||
|
|
||||||
|
1. Client connects to SmartProxy
|
||||||
|
2. SmartProxy creates connection record and tracks socket
|
||||||
|
3. SmartProxy determines route requires TLS termination
|
||||||
|
4. NetworkProxyBridge forwards connection to NetworkProxy
|
||||||
|
5. NetworkProxy performs TLS termination
|
||||||
|
6. Data flows through piped sockets
|
||||||
|
7. When connection ends:
|
||||||
|
- NetworkProxy cleans up its socket tracking
|
||||||
|
- NetworkProxyBridge handles cleanup coordination
|
||||||
|
- SmartProxy's ConnectionManager removes connection record
|
||||||
|
- All resources are properly released
|
||||||
|
|
||||||
|
### Cleanup Coordination Points:
|
||||||
|
|
||||||
|
1. **SmartProxy Cleanup**:
|
||||||
|
- ConnectionManager tracks all cleanup reasons
|
||||||
|
- Socket handlers removed to prevent memory leaks
|
||||||
|
- Timeout timers cleared
|
||||||
|
- Connection records removed from maps
|
||||||
|
- Security manager notified of connection removal
|
||||||
|
|
||||||
|
2. **NetworkProxy Cleanup**:
|
||||||
|
- Sockets removed from tracking map
|
||||||
|
- Connection counters updated
|
||||||
|
- Metrics updated for monitoring
|
||||||
|
- Connection pool resources freed
|
||||||
|
|
||||||
|
3. **Bridge Cleanup**:
|
||||||
|
- Unpipe operations prevent data loss
|
||||||
|
- Both sockets properly destroyed
|
||||||
|
- Cleanup callback ensures SmartProxy is notified
|
||||||
|
|
||||||
|
## Important Considerations
|
||||||
|
|
||||||
|
1. **Memory Management**:
|
||||||
|
- All event listeners must be removed during cleanup
|
||||||
|
- Proper unpipe operations prevent memory leaks
|
||||||
|
- Connection records cleared from all tracking maps
|
||||||
|
|
||||||
|
2. **Error Handling**:
|
||||||
|
- Multiple cleanup mechanisms prevent orphaned connections
|
||||||
|
- Graceful shutdown attempted before forced destruction
|
||||||
|
- Timeout mechanisms ensure cleanup even in edge cases
|
||||||
|
|
||||||
|
3. **State Consistency**:
|
||||||
|
- Connection closed flags prevent duplicate cleanup
|
||||||
|
- Termination reasons tracked for debugging
|
||||||
|
- Activity timestamps updated for accurate timeout handling
|
||||||
|
|
||||||
|
4. **Performance**:
|
||||||
|
- Connection pools minimize TCP handshake overhead
|
||||||
|
- Efficient socket tracking using Maps
|
||||||
|
- Periodic cleanup prevents resource accumulation
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. Always use `initiateCleanupOnce()` to prevent duplicate cleanup operations
|
||||||
|
2. Track termination reasons for debugging and monitoring
|
||||||
|
3. Ensure all event listeners are removed during cleanup
|
||||||
|
4. Use proper unpipe operations when breaking socket connections
|
||||||
|
5. Monitor connection counts and cleanup statistics
|
||||||
|
6. Implement proper timeout handling for all connection types
|
||||||
|
7. Keep socket tracking maps synchronized with actual socket state
|
@ -1,86 +0,0 @@
|
|||||||
# ACME/Certificate Simplification Summary
|
|
||||||
|
|
||||||
## What Was Done
|
|
||||||
|
|
||||||
We successfully implemented the ACME/Certificate simplification plan for SmartProxy:
|
|
||||||
|
|
||||||
### 1. Created New Certificate Management System
|
|
||||||
|
|
||||||
- **SmartCertManager** (`ts/proxies/smart-proxy/certificate-manager.ts`): A unified certificate manager that handles both ACME and static certificates
|
|
||||||
- **CertStore** (`ts/proxies/smart-proxy/cert-store.ts`): File-based certificate storage system
|
|
||||||
|
|
||||||
### 2. Updated Route Types
|
|
||||||
|
|
||||||
- Added `IRouteAcme` interface for ACME configuration
|
|
||||||
- Added `IStaticResponse` interface for static route responses
|
|
||||||
- Extended `IRouteTls` with comprehensive certificate options
|
|
||||||
- Added `handler` property to `IRouteAction` for static routes
|
|
||||||
|
|
||||||
### 3. Implemented Static Route Handler
|
|
||||||
|
|
||||||
- Added `handleStaticAction` method to route-connection-handler.ts
|
|
||||||
- Added support for 'static' route type in the action switch statement
|
|
||||||
- Implemented proper HTTP response formatting
|
|
||||||
|
|
||||||
### 4. Updated SmartProxy Integration
|
|
||||||
|
|
||||||
- Removed old CertProvisioner and Port80Handler dependencies
|
|
||||||
- Added `initializeCertificateManager` method
|
|
||||||
- Updated `start` and `stop` methods to use new certificate manager
|
|
||||||
- Added `provisionCertificate`, `renewCertificate`, and `getCertificateStatus` methods
|
|
||||||
|
|
||||||
### 5. Simplified NetworkProxyBridge
|
|
||||||
|
|
||||||
- Removed all certificate-related logic
|
|
||||||
- Simplified to only handle network proxy forwarding
|
|
||||||
- Updated to use port-based matching for network proxy routes
|
|
||||||
|
|
||||||
### 6. Cleaned Up HTTP Module
|
|
||||||
|
|
||||||
- Removed exports for port80 subdirectory
|
|
||||||
- Kept only router and redirect functionality
|
|
||||||
|
|
||||||
### 7. Created Tests
|
|
||||||
|
|
||||||
- Created simplified test for certificate functionality
|
|
||||||
- Test demonstrates static route handling and basic certificate configuration
|
|
||||||
|
|
||||||
## Key Improvements
|
|
||||||
|
|
||||||
1. **No Backward Compatibility**: Clean break from legacy implementations
|
|
||||||
2. **Direct SmartAcme Integration**: Uses @push.rocks/smartacme directly without custom wrappers
|
|
||||||
3. **Route-Based ACME Challenges**: No separate HTTP server needed
|
|
||||||
4. **Simplified Architecture**: Removed unnecessary abstraction layers
|
|
||||||
5. **Unified Configuration**: Certificate configuration is part of route definitions
|
|
||||||
|
|
||||||
## Configuration Example
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const proxy = new SmartProxy({
|
|
||||||
routes: [{
|
|
||||||
name: 'secure-site',
|
|
||||||
match: { ports: 443, domains: 'example.com' },
|
|
||||||
action: {
|
|
||||||
type: 'forward',
|
|
||||||
target: { host: 'backend', port: 8080 },
|
|
||||||
tls: {
|
|
||||||
mode: 'terminate',
|
|
||||||
certificate: 'auto',
|
|
||||||
acme: {
|
|
||||||
email: 'admin@example.com',
|
|
||||||
useProduction: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
|
|
||||||
1. Remove old certificate module and port80 directory
|
|
||||||
2. Update documentation with new configuration format
|
|
||||||
3. Test with real ACME certificates in staging environment
|
|
||||||
4. Add more comprehensive tests for renewal and edge cases
|
|
||||||
|
|
||||||
The implementation is complete and builds successfully!
|
|
@ -1,34 +0,0 @@
|
|||||||
# NFTables Naming Consolidation Summary
|
|
||||||
|
|
||||||
This document summarizes the changes made to consolidate the naming convention for IP allow/block lists in the NFTables integration.
|
|
||||||
|
|
||||||
## Changes Made
|
|
||||||
|
|
||||||
1. **Updated NFTablesProxy interface** (`ts/proxies/nftables-proxy/models/interfaces.ts`):
|
|
||||||
- Changed `allowedSourceIPs` to `ipAllowList`
|
|
||||||
- Changed `bannedSourceIPs` to `ipBlockList`
|
|
||||||
|
|
||||||
2. **Updated NFTablesProxy implementation** (`ts/proxies/nftables-proxy/nftables-proxy.ts`):
|
|
||||||
- Updated all references from `allowedSourceIPs` to `ipAllowList`
|
|
||||||
- Updated all references from `bannedSourceIPs` to `ipBlockList`
|
|
||||||
|
|
||||||
3. **Updated NFTablesManager** (`ts/proxies/smart-proxy/nftables-manager.ts`):
|
|
||||||
- Changed mapping from `allowedSourceIPs` to `ipAllowList`
|
|
||||||
- Changed mapping from `bannedSourceIPs` to `ipBlockList`
|
|
||||||
|
|
||||||
## Files Already Using Consistent Naming
|
|
||||||
|
|
||||||
The following files already used the consistent naming convention `ipAllowList` and `ipBlockList`:
|
|
||||||
|
|
||||||
1. **Route helpers** (`ts/proxies/smart-proxy/utils/route-helpers.ts`)
|
|
||||||
2. **Integration test** (`test/test.nftables-integration.ts`)
|
|
||||||
3. **NFTables example** (`examples/nftables-integration.ts`)
|
|
||||||
4. **Route types** (`ts/proxies/smart-proxy/models/route-types.ts`)
|
|
||||||
|
|
||||||
## Result
|
|
||||||
|
|
||||||
The naming is now consistent throughout the codebase:
|
|
||||||
- `ipAllowList` is used for lists of allowed IP addresses
|
|
||||||
- `ipBlockList` is used for lists of blocked IP addresses
|
|
||||||
|
|
||||||
This matches the naming convention already established in SmartProxy's core routing system.
|
|
@ -1,4 +1,4 @@
|
|||||||
import { expect, tap } from '@push.rocks/tapbundle';
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
import {
|
import {
|
||||||
EventSystem,
|
EventSystem,
|
||||||
ProxyEvents,
|
ProxyEvents,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { expect, tap } from '@push.rocks/tapbundle';
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
import { IpUtils } from '../../../ts/core/utils/ip-utils.js';
|
import { IpUtils } from '../../../ts/core/utils/ip-utils.js';
|
||||||
|
|
||||||
tap.test('ip-utils - normalizeIP', async () => {
|
tap.test('ip-utils - normalizeIP', async () => {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { expect, tap } from '@push.rocks/tapbundle';
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
import * as routeUtils from '../../../ts/core/utils/route-utils.js';
|
import * as routeUtils from '../../../ts/core/utils/route-utils.js';
|
||||||
|
|
||||||
// Test domain matching
|
// Test domain matching
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { expect, tap } from '@push.rocks/tapbundle';
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
import { SharedSecurityManager } from '../../../ts/core/utils/shared-security-manager.js';
|
import { SharedSecurityManager } from '../../../ts/core/utils/shared-security-manager.js';
|
||||||
import type { IRouteConfig, IRouteContext } from '../../../ts/proxies/smart-proxy/models/route-types.js';
|
import type { IRouteConfig, IRouteContext } from '../../../ts/proxies/smart-proxy/models/route-types.js';
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { expect, tap } from '@push.rocks/tapbundle';
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
import { ValidationUtils } from '../../../ts/core/utils/validation-utils.js';
|
import { ValidationUtils } from '../../../ts/core/utils/validation-utils.js';
|
||||||
import type { IDomainOptions, IAcmeOptions } from '../../../ts/core/models/common-types.js';
|
import type { IDomainOptions, IAcmeOptions } from '../../../ts/core/models/common-types.js';
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { expect, tap } from '@push.rocks/tapbundle';
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
import { AcmeStateManager } from '../ts/proxies/smart-proxy/acme-state-manager.js';
|
import { AcmeStateManager } from '../ts/proxies/smart-proxy/acme-state-manager.js';
|
||||||
import type { IRouteConfig } from '../ts/proxies/smart-proxy/models/route-types.js';
|
import type { IRouteConfig } from '../ts/proxies/smart-proxy/models/route-types.js';
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { SmartProxy } from '../ts/proxies/smart-proxy/index.js';
|
import { SmartProxy } from '../ts/proxies/smart-proxy/index.js';
|
||||||
import { expect, tap } from '@push.rocks/tapbundle';
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
|
|
||||||
const testProxy = new SmartProxy({
|
const testProxy = new SmartProxy({
|
||||||
routes: [{
|
routes: [{
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { SmartProxy } from '../ts/proxies/smart-proxy/index.js';
|
import { SmartProxy } from '../ts/proxies/smart-proxy/index.js';
|
||||||
import { expect, tap } from '@push.rocks/tapbundle';
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
|
|
||||||
tap.test('should create SmartProxy with certificate routes', async () => {
|
tap.test('should create SmartProxy with certificate routes', async () => {
|
||||||
const proxy = new SmartProxy({
|
const proxy = new SmartProxy({
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { tap, expect } from '@push.rocks/tapbundle';
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||||
|
|
||||||
import { SmartProxy } from '../ts/proxies/smart-proxy/index.js';
|
import { SmartProxy } from '../ts/proxies/smart-proxy/index.js';
|
||||||
import {
|
import {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { tap, expect } from '@push.rocks/tapbundle';
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||||
import * as plugins from '../ts/plugins.js';
|
import * as plugins from '../ts/plugins.js';
|
||||||
import type { IForwardConfig, TForwardingType } from '../ts/forwarding/config/forwarding-types.js';
|
import type { IForwardConfig, TForwardingType } from '../ts/forwarding/config/forwarding-types.js';
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { tap, expect } from '@push.rocks/tapbundle';
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||||
import * as plugins from '../ts/plugins.js';
|
import * as plugins from '../ts/plugins.js';
|
||||||
|
|
||||||
// First, import the components directly to avoid issues with compiled modules
|
// First, import the components directly to avoid issues with compiled modules
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { expect, tap } from '@push.rocks/tapbundle';
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
import * as plugins from '../ts/plugins.js';
|
import * as plugins from '../ts/plugins.js';
|
||||||
import { NetworkProxy } from '../ts/proxies/network-proxy/index.js';
|
import { NetworkProxy } from '../ts/proxies/network-proxy/index.js';
|
||||||
import type { IRouteConfig } from '../ts/proxies/smart-proxy/models/route-types.js';
|
import type { IRouteConfig } from '../ts/proxies/smart-proxy/models/route-types.js';
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { expect, tap } from '@push.rocks/tapbundle';
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
import * as smartproxy from '../ts/index.js';
|
import * as smartproxy from '../ts/index.js';
|
||||||
import { loadTestCertificates } from './helpers/certificates.js';
|
import { loadTestCertificates } from './helpers/certificates.js';
|
||||||
import * as https from 'https';
|
import * as https from 'https';
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { SmartProxy } from '../ts/proxies/smart-proxy/index.js';
|
import { SmartProxy } from '../ts/proxies/smart-proxy/index.js';
|
||||||
import { createNfTablesRoute, createNfTablesTerminateRoute } from '../ts/proxies/smart-proxy/utils/route-helpers.js';
|
import { createNfTablesRoute, createNfTablesTerminateRoute } from '../ts/proxies/smart-proxy/utils/route-helpers.js';
|
||||||
import { expect, tap } from '@push.rocks/tapbundle';
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
import * as child_process from 'child_process';
|
import * as child_process from 'child_process';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { SmartProxy } from '../ts/proxies/smart-proxy/index.js';
|
import { SmartProxy } from '../ts/proxies/smart-proxy/index.js';
|
||||||
import { createNfTablesRoute, createNfTablesTerminateRoute } from '../ts/proxies/smart-proxy/utils/route-helpers.js';
|
import { createNfTablesRoute, createNfTablesTerminateRoute } from '../ts/proxies/smart-proxy/utils/route-helpers.js';
|
||||||
import { expect, tap } from '@push.rocks/tapbundle';
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
import * as net from 'net';
|
import * as net from 'net';
|
||||||
import * as http from 'http';
|
import * as http from 'http';
|
||||||
import * as https from 'https';
|
import * as https from 'https';
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { expect, tap } from '@push.rocks/tapbundle';
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
import { NFTablesManager } from '../ts/proxies/smart-proxy/nftables-manager.js';
|
import { NFTablesManager } from '../ts/proxies/smart-proxy/nftables-manager.js';
|
||||||
import type { IRouteConfig } from '../ts/proxies/smart-proxy/models/route-types.js';
|
import type { IRouteConfig } from '../ts/proxies/smart-proxy/models/route-types.js';
|
||||||
import type { ISmartProxyOptions } from '../ts/proxies/smart-proxy/models/interfaces.js';
|
import type { ISmartProxyOptions } from '../ts/proxies/smart-proxy/models/interfaces.js';
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { SmartProxy } from '../ts/proxies/smart-proxy/index.js';
|
import { SmartProxy } from '../ts/proxies/smart-proxy/index.js';
|
||||||
import { NFTablesManager } from '../ts/proxies/smart-proxy/nftables-manager.js';
|
import { NFTablesManager } from '../ts/proxies/smart-proxy/nftables-manager.js';
|
||||||
import { createNfTablesRoute } from '../ts/proxies/smart-proxy/utils/route-helpers.js';
|
import { createNfTablesRoute } from '../ts/proxies/smart-proxy/utils/route-helpers.js';
|
||||||
import { expect, tap } from '@push.rocks/tapbundle';
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
import * as child_process from 'child_process';
|
import * as child_process from 'child_process';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { expect, tap } from '@push.rocks/tapbundle';
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
import * as net from 'net';
|
import * as net from 'net';
|
||||||
import { SmartProxy } from '../ts/proxies/smart-proxy/index.js';
|
import { SmartProxy } from '../ts/proxies/smart-proxy/index.js';
|
||||||
import {
|
import {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { expect, tap } from '@push.rocks/tapbundle';
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
import { SmartProxy } from '../ts/index.js';
|
import { SmartProxy } from '../ts/index.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { expect, tap } from '@push.rocks/tapbundle';
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
import { SmartProxy } from '../ts/index.js';
|
import { SmartProxy } from '../ts/index.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Tests for the unified route-based configuration system
|
* Tests for the unified route-based configuration system
|
||||||
*/
|
*/
|
||||||
import { expect, tap } from '@push.rocks/tapbundle';
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
|
|
||||||
// Import from core modules
|
// Import from core modules
|
||||||
import { SmartProxy } from '../ts/proxies/smart-proxy/index.js';
|
import { SmartProxy } from '../ts/proxies/smart-proxy/index.js';
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import * as plugins from '../ts/plugins.js';
|
import * as plugins from '../ts/plugins.js';
|
||||||
import { SmartProxy } from '../ts/index.js';
|
import { SmartProxy } from '../ts/index.js';
|
||||||
import { tap, expect } from '@push.rocks/tapbundle';
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||||
|
|
||||||
let testProxy: SmartProxy;
|
let testProxy: SmartProxy;
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { tap, expect } from '@push.rocks/tapbundle';
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||||
import * as plugins from '../ts/plugins.js';
|
import * as plugins from '../ts/plugins.js';
|
||||||
|
|
||||||
// Import from individual modules to avoid naming conflicts
|
// Import from individual modules to avoid naming conflicts
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { expect, tap } from '@push.rocks/tapbundle';
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
import * as tsclass from '@tsclass/tsclass';
|
import * as tsclass from '@tsclass/tsclass';
|
||||||
import * as http from 'http';
|
import * as http from 'http';
|
||||||
import { ProxyRouter, type RouterResult } from '../ts/http/router/proxy-router.js';
|
import { ProxyRouter, type RouterResult } from '../ts/http/router/proxy-router.js';
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import * as plugins from '../ts/plugins.js';
|
import * as plugins from '../ts/plugins.js';
|
||||||
import { tap, expect } from '@push.rocks/tapbundle';
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||||
import { SmartCertManager } from '../ts/proxies/smart-proxy/certificate-manager.js';
|
import { SmartCertManager } from '../ts/proxies/smart-proxy/certificate-manager.js';
|
||||||
import type { IRouteConfig } from '../ts/proxies/smart-proxy/models/route-types.js';
|
import type { IRouteConfig } from '../ts/proxies/smart-proxy/models/route-types.js';
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { expect, tap } from '@push.rocks/tapbundle';
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
import * as net from 'net';
|
import * as net from 'net';
|
||||||
import { SmartProxy } from '../ts/proxies/smart-proxy/index.js';
|
import { SmartProxy } from '../ts/proxies/smart-proxy/index.js';
|
||||||
|
|
||||||
|
@ -219,21 +219,12 @@ export class NetworkProxy implements IMetricsTracker {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated Use SmartCertManager instead
|
|
||||||
*/
|
|
||||||
public setExternalPort80Handler(handler: any): void {
|
|
||||||
this.logger.warn('Port80Handler is deprecated - use SmartCertManager instead');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts the proxy server
|
* Starts the proxy server
|
||||||
*/
|
*/
|
||||||
public async start(): Promise<void> {
|
public async start(): Promise<void> {
|
||||||
this.startTime = Date.now();
|
this.startTime = Date.now();
|
||||||
|
|
||||||
// Certificate management is now handled by SmartCertManager
|
|
||||||
|
|
||||||
// Create HTTP/2 server with HTTP/1 fallback
|
// Create HTTP/2 server with HTTP/1 fallback
|
||||||
this.httpsServer = plugins.http2.createSecureServer(
|
this.httpsServer = plugins.http2.createSecureServer(
|
||||||
{
|
{
|
||||||
|
@ -47,6 +47,7 @@ export interface IRouteContext {
|
|||||||
path?: string; // URL path (for HTTP connections)
|
path?: string; // URL path (for HTTP connections)
|
||||||
query?: string; // Query string (for HTTP connections)
|
query?: string; // Query string (for HTTP connections)
|
||||||
headers?: Record<string, string>; // HTTP headers (for HTTP connections)
|
headers?: Record<string, string>; // HTTP headers (for HTTP connections)
|
||||||
|
method?: string; // HTTP method (for HTTP connections)
|
||||||
|
|
||||||
// TLS information
|
// TLS information
|
||||||
isTls: boolean; // Whether the connection is TLS
|
isTls: boolean; // Whether the connection is TLS
|
||||||
|
@ -728,45 +728,139 @@ export class RouteConnectionHandler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
let buffer = Buffer.alloc(0);
|
||||||
// Build route context
|
|
||||||
const context: IRouteContext = {
|
|
||||||
port: record.localPort,
|
|
||||||
domain: record.lockedDomain,
|
|
||||||
clientIp: record.remoteIP,
|
|
||||||
serverIp: socket.localAddress!,
|
|
||||||
path: undefined, // Will need to be extracted from HTTP request
|
|
||||||
isTls: record.isTLS,
|
|
||||||
tlsVersion: record.tlsVersion,
|
|
||||||
routeName: route.name,
|
|
||||||
routeId: route.name,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
connectionId
|
|
||||||
};
|
|
||||||
|
|
||||||
// Call the handler
|
const handleHttpData = async (chunk: Buffer) => {
|
||||||
const response = await route.action.handler(context);
|
buffer = Buffer.concat([buffer, chunk]);
|
||||||
|
|
||||||
// Send HTTP response
|
// Look for end of HTTP headers
|
||||||
const headers = response.headers || {};
|
const headerEndIndex = buffer.indexOf('\r\n\r\n');
|
||||||
headers['Content-Length'] = Buffer.byteLength(response.body).toString();
|
if (headerEndIndex === -1) {
|
||||||
|
// Need more data
|
||||||
let httpResponse = `HTTP/1.1 ${response.status} ${getStatusText(response.status)}\r\n`;
|
if (buffer.length > 8192) { // Prevent excessive buffering
|
||||||
for (const [key, value] of Object.entries(headers)) {
|
console.error(`[${connectionId}] HTTP headers too large`);
|
||||||
httpResponse += `${key}: ${value}\r\n`;
|
socket.end();
|
||||||
|
this.connectionManager.cleanupConnection(record, 'headers_too_large');
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
httpResponse += '\r\n';
|
|
||||||
|
|
||||||
socket.write(httpResponse);
|
// Parse the HTTP request
|
||||||
socket.write(response.body);
|
const headerBuffer = buffer.slice(0, headerEndIndex);
|
||||||
socket.end();
|
const headers = headerBuffer.toString();
|
||||||
|
const lines = headers.split('\r\n');
|
||||||
|
|
||||||
this.connectionManager.cleanupConnection(record, 'completed');
|
if (lines.length === 0) {
|
||||||
} catch (error) {
|
console.error(`[${connectionId}] Invalid HTTP request`);
|
||||||
console.error(`[${connectionId}] Error in static handler: ${error}`);
|
socket.end();
|
||||||
socket.end();
|
this.connectionManager.cleanupConnection(record, 'invalid_request');
|
||||||
this.connectionManager.cleanupConnection(record, 'handler_error');
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse request line
|
||||||
|
const requestLine = lines[0];
|
||||||
|
const requestParts = requestLine.split(' ');
|
||||||
|
if (requestParts.length < 3) {
|
||||||
|
console.error(`[${connectionId}] Invalid HTTP request line`);
|
||||||
|
socket.end();
|
||||||
|
this.connectionManager.cleanupConnection(record, 'invalid_request_line');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [method, path, httpVersion] = requestParts;
|
||||||
|
|
||||||
|
// Parse headers
|
||||||
|
const headersMap: Record<string, string> = {};
|
||||||
|
for (let i = 1; i < lines.length; i++) {
|
||||||
|
const colonIndex = lines[i].indexOf(':');
|
||||||
|
if (colonIndex > 0) {
|
||||||
|
const key = lines[i].slice(0, colonIndex).trim().toLowerCase();
|
||||||
|
const value = lines[i].slice(colonIndex + 1).trim();
|
||||||
|
headersMap[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract query string if present
|
||||||
|
let pathname = path;
|
||||||
|
let query: string | undefined;
|
||||||
|
const queryIndex = path.indexOf('?');
|
||||||
|
if (queryIndex !== -1) {
|
||||||
|
pathname = path.slice(0, queryIndex);
|
||||||
|
query = path.slice(queryIndex + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Build route context with parsed HTTP information
|
||||||
|
const context: IRouteContext = {
|
||||||
|
port: record.localPort,
|
||||||
|
domain: record.lockedDomain || headersMap['host']?.split(':')[0],
|
||||||
|
clientIp: record.remoteIP,
|
||||||
|
serverIp: socket.localAddress!,
|
||||||
|
path: pathname,
|
||||||
|
query: query,
|
||||||
|
headers: headersMap,
|
||||||
|
method: method,
|
||||||
|
isTls: record.isTLS,
|
||||||
|
tlsVersion: record.tlsVersion,
|
||||||
|
routeName: route.name,
|
||||||
|
routeId: route.name,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
connectionId
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove the data listener since we're handling the request
|
||||||
|
socket.removeListener('data', handleHttpData);
|
||||||
|
|
||||||
|
// Call the handler with the properly parsed context
|
||||||
|
const response = await route.action.handler(context);
|
||||||
|
|
||||||
|
// Prepare the HTTP response
|
||||||
|
const responseHeaders = response.headers || {};
|
||||||
|
const contentLength = Buffer.byteLength(response.body || '');
|
||||||
|
responseHeaders['Content-Length'] = contentLength.toString();
|
||||||
|
|
||||||
|
if (!responseHeaders['Content-Type']) {
|
||||||
|
responseHeaders['Content-Type'] = 'text/plain';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the response
|
||||||
|
let httpResponse = `HTTP/1.1 ${response.status} ${getStatusText(response.status)}\r\n`;
|
||||||
|
for (const [key, value] of Object.entries(responseHeaders)) {
|
||||||
|
httpResponse += `${key}: ${value}\r\n`;
|
||||||
|
}
|
||||||
|
httpResponse += '\r\n';
|
||||||
|
|
||||||
|
// Send response
|
||||||
|
socket.write(httpResponse);
|
||||||
|
if (response.body) {
|
||||||
|
socket.write(response.body);
|
||||||
|
}
|
||||||
|
socket.end();
|
||||||
|
|
||||||
|
this.connectionManager.cleanupConnection(record, 'completed');
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[${connectionId}] Error in static handler: ${error}`);
|
||||||
|
|
||||||
|
// Send error response
|
||||||
|
const errorResponse = 'HTTP/1.1 500 Internal Server Error\r\n' +
|
||||||
|
'Content-Type: text/plain\r\n' +
|
||||||
|
'Content-Length: 21\r\n' +
|
||||||
|
'\r\n' +
|
||||||
|
'Internal Server Error';
|
||||||
|
socket.write(errorResponse);
|
||||||
|
socket.end();
|
||||||
|
|
||||||
|
this.connectionManager.cleanupConnection(record, 'handler_error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Listen for data
|
||||||
|
socket.on('data', handleHttpData);
|
||||||
|
|
||||||
|
// Ensure cleanup on socket close
|
||||||
|
socket.once('close', () => {
|
||||||
|
socket.removeListener('data', handleHttpData);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
x
Reference in New Issue
Block a user