- Added ProxyProtocolParser class for parsing and generating PROXY protocol v1 headers. - Integrated PROXY protocol parsing into RouteConnectionHandler for handling incoming connections from trusted proxies. - Implemented WrappedSocket class to encapsulate real client information. - Configured SmartProxy to accept and send PROXY protocol headers in routing actions. - Developed comprehensive unit tests for PROXY protocol parsing and generation. - Documented usage patterns, configuration, and best practices for proxy chaining scenarios. - Added security and performance considerations for PROXY protocol implementation.
415 lines
12 KiB
Markdown
415 lines
12 KiB
Markdown
# SmartProxy PROXY Protocol and Proxy Chaining Documentation
|
|
|
|
## Overview
|
|
|
|
SmartProxy implements support for the PROXY protocol v1 to enable proxy chaining and preserve real client IP addresses across multiple proxy layers. This documentation covers the implementation details, configuration, and usage patterns for proxy chaining scenarios.
|
|
|
|
## Architecture
|
|
|
|
### WrappedSocket Implementation
|
|
|
|
The foundation of PROXY protocol support is the `WrappedSocket` class, which wraps regular `net.Socket` instances to provide transparent access to real client information when behind a proxy.
|
|
|
|
```typescript
|
|
// ts/core/models/wrapped-socket.ts
|
|
export class WrappedSocket {
|
|
public readonly socket: plugins.net.Socket;
|
|
private realClientIP?: string;
|
|
private realClientPort?: number;
|
|
|
|
constructor(
|
|
socket: plugins.net.Socket,
|
|
realClientIP?: string,
|
|
realClientPort?: number
|
|
) {
|
|
this.socket = socket;
|
|
this.realClientIP = realClientIP;
|
|
this.realClientPort = realClientPort;
|
|
|
|
// Uses JavaScript Proxy to delegate all methods to underlying socket
|
|
return new Proxy(this, {
|
|
get(target, prop, receiver) {
|
|
// Override specific properties
|
|
if (prop === 'remoteAddress') {
|
|
return target.remoteAddress;
|
|
}
|
|
if (prop === 'remotePort') {
|
|
return target.remotePort;
|
|
}
|
|
// ... delegate other properties to underlying socket
|
|
}
|
|
});
|
|
}
|
|
|
|
get remoteAddress(): string | undefined {
|
|
return this.realClientIP || this.socket.remoteAddress;
|
|
}
|
|
|
|
get remotePort(): number | undefined {
|
|
return this.realClientPort || this.socket.remotePort;
|
|
}
|
|
|
|
get isFromTrustedProxy(): boolean {
|
|
return !!this.realClientIP;
|
|
}
|
|
}
|
|
```
|
|
|
|
### Key Design Decisions
|
|
|
|
1. **All sockets are wrapped** - Every incoming connection is wrapped in a WrappedSocket, not just those from trusted proxies
|
|
2. **Proxy pattern for delegation** - Uses JavaScript Proxy to transparently delegate all Socket methods while allowing property overrides
|
|
3. **Not a Duplex stream** - Simple wrapper approach avoids complexity and infinite loops
|
|
4. **Trust-based parsing** - PROXY protocol parsing only occurs for connections from trusted proxy IPs
|
|
|
|
## Configuration
|
|
|
|
### Basic PROXY Protocol Configuration
|
|
|
|
```typescript
|
|
const proxy = new SmartProxy({
|
|
// List of trusted proxy IPs that can send PROXY protocol
|
|
proxyIPs: ['10.0.0.1', '10.0.0.2', '192.168.1.0/24'],
|
|
|
|
// Global option to accept PROXY protocol (defaults based on proxyIPs)
|
|
acceptProxyProtocol: true,
|
|
|
|
// Global option to send PROXY protocol to all targets
|
|
sendProxyProtocol: false,
|
|
|
|
routes: [
|
|
{
|
|
name: 'backend-app',
|
|
match: { ports: 443, domains: 'app.example.com' },
|
|
action: {
|
|
type: 'forward',
|
|
target: { host: 'backend.internal', port: 8443 },
|
|
tls: { mode: 'passthrough' }
|
|
}
|
|
}
|
|
]
|
|
});
|
|
```
|
|
|
|
### Proxy Chain Configuration
|
|
|
|
Setting up two SmartProxies in a chain:
|
|
|
|
```typescript
|
|
// Outer Proxy (Internet-facing)
|
|
const outerProxy = new SmartProxy({
|
|
proxyIPs: [], // No trusted proxies for outer proxy
|
|
sendProxyProtocol: true, // Send PROXY protocol to inner proxy
|
|
|
|
routes: [{
|
|
name: 'to-inner-proxy',
|
|
match: { ports: 443 },
|
|
action: {
|
|
type: 'forward',
|
|
target: {
|
|
host: 'inner-proxy.internal',
|
|
port: 443
|
|
},
|
|
tls: { mode: 'passthrough' }
|
|
}
|
|
}]
|
|
});
|
|
|
|
// Inner Proxy (Backend-facing)
|
|
const innerProxy = new SmartProxy({
|
|
proxyIPs: ['outer-proxy.internal'], // Trust the outer proxy
|
|
acceptProxyProtocol: true,
|
|
|
|
routes: [{
|
|
name: 'to-backend',
|
|
match: { ports: 443, domains: 'app.example.com' },
|
|
action: {
|
|
type: 'forward',
|
|
target: {
|
|
host: 'backend.internal',
|
|
port: 8080
|
|
},
|
|
tls: {
|
|
mode: 'terminate',
|
|
certificate: 'auto'
|
|
}
|
|
}
|
|
}]
|
|
});
|
|
```
|
|
|
|
## How Two SmartProxies Communicate
|
|
|
|
### Connection Flow
|
|
|
|
1. **Client connects to Outer Proxy**
|
|
```
|
|
Client (203.0.113.45:54321) → Outer Proxy (1.2.3.4:443)
|
|
```
|
|
|
|
2. **Outer Proxy wraps the socket**
|
|
```typescript
|
|
// In RouteConnectionHandler.handleConnection()
|
|
const wrappedSocket = new WrappedSocket(socket);
|
|
// At this point:
|
|
// wrappedSocket.remoteAddress = '203.0.113.45'
|
|
// wrappedSocket.remotePort = 54321
|
|
```
|
|
|
|
3. **Outer Proxy forwards to Inner Proxy**
|
|
- Creates new connection to inner proxy
|
|
- If `sendProxyProtocol` is enabled, prepends PROXY protocol header:
|
|
```
|
|
PROXY TCP4 203.0.113.45 1.2.3.4 54321 443\r\n
|
|
[Original TLS/HTTP data follows]
|
|
```
|
|
|
|
4. **Inner Proxy receives connection**
|
|
- Sees connection from outer proxy IP
|
|
- Checks if IP is in `proxyIPs` list
|
|
- If trusted, parses PROXY protocol header
|
|
- Updates WrappedSocket with real client info:
|
|
```typescript
|
|
wrappedSocket.setProxyInfo('203.0.113.45', 54321);
|
|
```
|
|
|
|
5. **Inner Proxy routes based on real client IP**
|
|
- Security checks use real client IP
|
|
- Connection records track real client IP
|
|
- Backend sees requests from the original client IP
|
|
|
|
### Connection Record Tracking
|
|
|
|
```typescript
|
|
// In ConnectionManager
|
|
interface IConnectionRecord {
|
|
id: string;
|
|
incoming: WrappedSocket; // Wrapped socket with real client info
|
|
outgoing: net.Socket | null;
|
|
remoteIP: string; // Real client IP from PROXY protocol or direct connection
|
|
localPort: number;
|
|
// ... other fields
|
|
}
|
|
```
|
|
|
|
## Implementation Details
|
|
|
|
### Socket Wrapping in Route Handler
|
|
|
|
```typescript
|
|
// ts/proxies/smart-proxy/route-connection-handler.ts
|
|
public handleConnection(socket: plugins.net.Socket): void {
|
|
const remoteIP = socket.remoteAddress || '';
|
|
|
|
// Always wrap the socket to prepare for potential PROXY protocol
|
|
const wrappedSocket = new WrappedSocket(socket);
|
|
|
|
// If this is from a trusted proxy, log it
|
|
if (this.settings.proxyIPs?.includes(remoteIP)) {
|
|
logger.log('debug', `Connection from trusted proxy ${remoteIP}, PROXY protocol parsing will be enabled`);
|
|
}
|
|
|
|
// Create connection record with wrapped socket
|
|
const record = this.connectionManager.createConnection(wrappedSocket);
|
|
|
|
// Continue with normal connection handling...
|
|
}
|
|
```
|
|
|
|
### Socket Utility Integration
|
|
|
|
When passing wrapped sockets to socket utility functions, the underlying socket must be extracted:
|
|
|
|
```typescript
|
|
import { getUnderlyingSocket } from '../../core/models/socket-types.js';
|
|
|
|
// In setupDirectConnection()
|
|
const incomingSocket = getUnderlyingSocket(socket); // Extract raw socket
|
|
|
|
setupBidirectionalForwarding(incomingSocket, targetSocket, {
|
|
onClientData: (chunk) => {
|
|
record.bytesReceived += chunk.length;
|
|
},
|
|
onServerData: (chunk) => {
|
|
record.bytesSent += chunk.length;
|
|
},
|
|
onCleanup: (reason) => {
|
|
this.connectionManager.cleanupConnection(record, reason);
|
|
},
|
|
enableHalfOpen: false // Required for proxy chains
|
|
});
|
|
```
|
|
|
|
## Current Status and Limitations
|
|
|
|
### Implemented (v19.5.19+)
|
|
- ✅ WrappedSocket foundation class
|
|
- ✅ Socket wrapping in connection handler
|
|
- ✅ Connection manager support for wrapped sockets
|
|
- ✅ Socket utility integration helpers
|
|
- ✅ Proxy IP configuration options
|
|
|
|
### Not Yet Implemented
|
|
- ❌ PROXY protocol v1 header parsing
|
|
- ❌ PROXY protocol v2 binary format support
|
|
- ❌ Automatic PROXY protocol header generation when forwarding
|
|
- ❌ HAProxy compatibility testing
|
|
- ❌ AWS ELB/NLB compatibility testing
|
|
|
|
### Known Issues
|
|
1. **No actual PROXY protocol parsing** - The infrastructure is in place but the protocol parsing is not yet implemented
|
|
2. **Manual configuration required** - No automatic detection of PROXY protocol support
|
|
3. **Limited to TCP connections** - WebSocket connections through proxy chains may not preserve client IPs
|
|
|
|
## Testing Proxy Chains
|
|
|
|
### Basic Proxy Chain Test
|
|
|
|
```typescript
|
|
// test/test.proxy-chain-simple.node.ts
|
|
tap.test('simple proxy chain test', async () => {
|
|
// Create backend server
|
|
const backend = net.createServer((socket) => {
|
|
console.log('Backend: Connection received');
|
|
socket.write('HTTP/1.1 200 OK\r\n\r\nHello from backend');
|
|
socket.end();
|
|
});
|
|
|
|
// Create inner proxy (downstream)
|
|
const innerProxy = new SmartProxy({
|
|
proxyIPs: ['127.0.0.1'], // Trust localhost for testing
|
|
routes: [{
|
|
name: 'to-backend',
|
|
match: { ports: 8591 },
|
|
action: {
|
|
type: 'forward',
|
|
target: { host: 'localhost', port: 9999 }
|
|
}
|
|
}]
|
|
});
|
|
|
|
// Create outer proxy (upstream)
|
|
const outerProxy = new SmartProxy({
|
|
sendProxyProtocol: true, // Send PROXY to inner
|
|
routes: [{
|
|
name: 'to-inner',
|
|
match: { ports: 8590 },
|
|
action: {
|
|
type: 'forward',
|
|
target: { host: 'localhost', port: 8591 }
|
|
}
|
|
}]
|
|
});
|
|
|
|
// Test connection through chain
|
|
const client = net.connect(8590, 'localhost');
|
|
client.write('GET / HTTP/1.1\r\nHost: test.com\r\n\r\n');
|
|
|
|
// Verify no connection accumulation
|
|
const counts = getConnectionCounts();
|
|
expect(counts.proxy1).toEqual(0);
|
|
expect(counts.proxy2).toEqual(0);
|
|
});
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
### 1. Always Configure Trusted Proxies
|
|
```typescript
|
|
// Be specific about which IPs can send PROXY protocol
|
|
proxyIPs: ['10.0.0.1', '10.0.0.2'], // Good
|
|
proxyIPs: ['0.0.0.0/0'], // Bad - trusts everyone
|
|
```
|
|
|
|
### 2. Use CIDR Notation for Subnets
|
|
```typescript
|
|
proxyIPs: [
|
|
'10.0.0.0/24', // Trust entire subnet
|
|
'192.168.1.5', // Trust specific IP
|
|
'172.16.0.0/16' // Trust private network
|
|
]
|
|
```
|
|
|
|
### 3. Enable Half-Open Only When Needed
|
|
```typescript
|
|
// For proxy chains, always disable half-open
|
|
setupBidirectionalForwarding(client, server, {
|
|
enableHalfOpen: false // Ensures proper cascade cleanup
|
|
});
|
|
```
|
|
|
|
### 4. Monitor Connection Counts
|
|
```typescript
|
|
// Regular monitoring prevents connection leaks
|
|
setInterval(() => {
|
|
const stats = proxy.getStatistics();
|
|
console.log(`Active connections: ${stats.activeConnections}`);
|
|
if (stats.activeConnections > 1000) {
|
|
console.warn('High connection count detected');
|
|
}
|
|
}, 60000);
|
|
```
|
|
|
|
## Future Enhancements
|
|
|
|
### Phase 2: PROXY Protocol v1 Parser
|
|
```typescript
|
|
// Planned implementation
|
|
class ProxyProtocolParser {
|
|
static parse(buffer: Buffer): ProxyInfo | null {
|
|
// Parse "PROXY TCP4 <src-ip> <dst-ip> <src-port> <dst-port>\r\n"
|
|
const header = buffer.toString('ascii', 0, 108);
|
|
const match = header.match(/^PROXY (TCP4|TCP6) (\S+) (\S+) (\d+) (\d+)\r\n/);
|
|
if (match) {
|
|
return {
|
|
protocol: match[1],
|
|
sourceIP: match[2],
|
|
destIP: match[3],
|
|
sourcePort: parseInt(match[4]),
|
|
destPort: parseInt(match[5]),
|
|
headerLength: match[0].length
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
```
|
|
|
|
### Phase 3: Automatic PROXY Protocol Detection
|
|
- Peek at first bytes to detect PROXY protocol signature
|
|
- Automatic fallback to direct connection if not present
|
|
- Configurable timeout for protocol detection
|
|
|
|
### Phase 4: PROXY Protocol v2 Support
|
|
- Binary protocol format for better performance
|
|
- Additional metadata support (TLS info, ALPN, etc.)
|
|
- AWS VPC endpoint ID preservation
|
|
|
|
## Troubleshooting
|
|
|
|
### Connection Accumulation in Proxy Chains
|
|
If connections accumulate when chaining proxies:
|
|
1. Verify `enableHalfOpen: false` in socket forwarding
|
|
2. Check that both proxies have proper cleanup handlers
|
|
3. Monitor with connection count logging
|
|
4. Use `test.proxy-chain-simple.node.ts` as reference
|
|
|
|
### Real Client IP Not Preserved
|
|
If the backend sees proxy IP instead of client IP:
|
|
1. Verify outer proxy has `sendProxyProtocol: true`
|
|
2. Verify inner proxy has outer proxy IP in `proxyIPs` list
|
|
3. Check logs for "Connection from trusted proxy" message
|
|
4. Ensure PROXY protocol parsing is implemented (currently pending)
|
|
|
|
### Performance Impact
|
|
PROXY protocol adds minimal overhead:
|
|
- One-time parsing cost per connection
|
|
- Small memory overhead for real client info storage
|
|
- No impact on data transfer performance
|
|
- Negligible CPU impact for header generation
|
|
|
|
## Related Documentation
|
|
- [Socket Utilities](./ts/core/utils/socket-utils.ts) - Low-level socket handling
|
|
- [Connection Manager](./ts/proxies/smart-proxy/connection-manager.ts) - Connection lifecycle
|
|
- [Route Handler](./ts/proxies/smart-proxy/route-connection-handler.ts) - Request routing
|
|
- [Test Suite](./test/test.wrapped-socket.ts) - WrappedSocket unit tests |