140 lines
4.0 KiB
Markdown
140 lines
4.0 KiB
Markdown
![]() |
# WebSocket Keep-Alive Configuration Guide
|
||
|
|
||
|
## Quick Fix for SNI Passthrough WebSocket Disconnections
|
||
|
|
||
|
If your WebSocket connections are disconnecting every 30 seconds in SNI passthrough mode, here's the immediate solution:
|
||
|
|
||
|
### Option 1: Extended Keep-Alive Treatment (Recommended)
|
||
|
|
||
|
```typescript
|
||
|
const proxy = new SmartProxy({
|
||
|
// Extend timeout for keep-alive connections
|
||
|
keepAliveTreatment: 'extended',
|
||
|
keepAliveInactivityMultiplier: 10, // 10x the base timeout
|
||
|
inactivityTimeout: 14400000, // 4 hours base (40 hours with multiplier)
|
||
|
|
||
|
routes: [
|
||
|
{
|
||
|
name: 'websocket-passthrough',
|
||
|
match: {
|
||
|
ports: 443,
|
||
|
domains: ['ws.example.com', 'wss.example.com']
|
||
|
},
|
||
|
action: {
|
||
|
type: 'forward',
|
||
|
target: { host: 'backend', port: 443 },
|
||
|
tls: { mode: 'passthrough' }
|
||
|
}
|
||
|
}
|
||
|
]
|
||
|
});
|
||
|
```
|
||
|
|
||
|
### Option 2: Immortal Connections (Never Timeout)
|
||
|
|
||
|
```typescript
|
||
|
const proxy = new SmartProxy({
|
||
|
// Never timeout keep-alive connections
|
||
|
keepAliveTreatment: 'immortal',
|
||
|
|
||
|
routes: [
|
||
|
// ... same as above
|
||
|
]
|
||
|
});
|
||
|
```
|
||
|
|
||
|
### Option 3: Per-Route Security Settings
|
||
|
|
||
|
```typescript
|
||
|
const proxy = new SmartProxy({
|
||
|
routes: [
|
||
|
{
|
||
|
name: 'websocket-passthrough',
|
||
|
match: {
|
||
|
ports: 443,
|
||
|
domains: ['ws.example.com']
|
||
|
},
|
||
|
action: {
|
||
|
type: 'forward',
|
||
|
target: { host: 'backend', port: 443 },
|
||
|
tls: { mode: 'passthrough' }
|
||
|
},
|
||
|
security: {
|
||
|
// Disable connection limits for this route
|
||
|
maxConnections: 0, // 0 = unlimited
|
||
|
maxConnectionsPerIP: 0 // 0 = unlimited
|
||
|
}
|
||
|
}
|
||
|
]
|
||
|
});
|
||
|
```
|
||
|
|
||
|
## Understanding the Issue
|
||
|
|
||
|
### Why Connections Drop at 30 Seconds
|
||
|
|
||
|
1. **WebSocket Heartbeat**: The HTTP proxy's WebSocket handler sends ping frames every 30 seconds
|
||
|
2. **SNI Passthrough**: In passthrough mode, traffic is encrypted end-to-end
|
||
|
3. **Can't Inject Pings**: The proxy can't inject ping frames into encrypted traffic
|
||
|
4. **No Pong Response**: Client doesn't respond to pings that were never sent
|
||
|
5. **Connection Terminated**: After 30 seconds, connection is marked inactive and closed
|
||
|
|
||
|
### Why Grace Periods Were Too Short
|
||
|
|
||
|
- Half-zombie detection: 30 seconds (now 5 minutes for TLS)
|
||
|
- Stuck connection detection: 60 seconds (now 5 minutes for TLS)
|
||
|
- These were too aggressive for encrypted long-lived connections
|
||
|
|
||
|
## Long-Term Solution
|
||
|
|
||
|
The fix involves:
|
||
|
|
||
|
1. **Detecting SNI Passthrough**: Skip WebSocket heartbeat for passthrough connections
|
||
|
2. **Longer Grace Periods**: 5-minute grace for encrypted connections
|
||
|
3. **TCP Keep-Alive**: Rely on OS-level TCP keep-alive instead
|
||
|
4. **Route-Aware Timeouts**: Different timeout strategies per route type
|
||
|
|
||
|
## TCP Keep-Alive Configuration
|
||
|
|
||
|
For best results, also configure TCP keep-alive at the OS level:
|
||
|
|
||
|
### Linux
|
||
|
```bash
|
||
|
# /etc/sysctl.conf
|
||
|
net.ipv4.tcp_keepalive_time = 600 # Start probes after 10 minutes
|
||
|
net.ipv4.tcp_keepalive_intvl = 60 # Probe every minute
|
||
|
net.ipv4.tcp_keepalive_probes = 9 # Drop after 9 failed probes
|
||
|
```
|
||
|
|
||
|
### Node.js Socket Options
|
||
|
The proxy already enables TCP keep-alive on sockets:
|
||
|
- Keep-alive is enabled by default
|
||
|
- Initial delay can be configured via `keepAliveInitialDelay`
|
||
|
|
||
|
## Monitoring
|
||
|
|
||
|
Check your connections:
|
||
|
|
||
|
```typescript
|
||
|
const stats = proxy.getStats();
|
||
|
console.log('Active connections:', stats.getActiveConnections());
|
||
|
console.log('Connections by route:', stats.getConnectionsByRoute());
|
||
|
|
||
|
// Monitor long-lived connections
|
||
|
setInterval(() => {
|
||
|
const connections = proxy.connectionManager.getConnections();
|
||
|
for (const [id, conn] of connections) {
|
||
|
const age = Date.now() - conn.incomingStartTime;
|
||
|
if (age > 300000) { // 5+ minutes
|
||
|
console.log(`Long-lived connection: ${id}, age: ${age}ms, route: ${conn.routeName}`);
|
||
|
}
|
||
|
}
|
||
|
}, 60000);
|
||
|
```
|
||
|
|
||
|
## Summary
|
||
|
|
||
|
- **Immediate Fix**: Use `keepAliveTreatment: 'extended'` or `'immortal'`
|
||
|
- **Applied Fix**: Increased grace periods for TLS connections to 5 minutes
|
||
|
- **Best Practice**: Use SNI passthrough for WebSocket when you need end-to-end encryption
|
||
|
- **Alternative**: Use TLS termination if you need application-level WebSocket features
|