- 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.
12 KiB
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.
// 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
- All sockets are wrapped - Every incoming connection is wrapped in a WrappedSocket, not just those from trusted proxies
- Proxy pattern for delegation - Uses JavaScript Proxy to transparently delegate all Socket methods while allowing property overrides
- Not a Duplex stream - Simple wrapper approach avoids complexity and infinite loops
- Trust-based parsing - PROXY protocol parsing only occurs for connections from trusted proxy IPs
Configuration
Basic PROXY Protocol Configuration
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:
// 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
-
Client connects to Outer Proxy
Client (203.0.113.45:54321) → Outer Proxy (1.2.3.4:443)
-
Outer Proxy wraps the socket
// In RouteConnectionHandler.handleConnection() const wrappedSocket = new WrappedSocket(socket); // At this point: // wrappedSocket.remoteAddress = '203.0.113.45' // wrappedSocket.remotePort = 54321
-
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]
-
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:
wrappedSocket.setProxyInfo('203.0.113.45', 54321);
-
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
// 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
// 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:
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
- No actual PROXY protocol parsing - The infrastructure is in place but the protocol parsing is not yet implemented
- Manual configuration required - No automatic detection of PROXY protocol support
- Limited to TCP connections - WebSocket connections through proxy chains may not preserve client IPs
Testing Proxy Chains
Basic Proxy Chain Test
// 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
// 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
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
// For proxy chains, always disable half-open
setupBidirectionalForwarding(client, server, {
enableHalfOpen: false // Ensures proper cascade cleanup
});
4. Monitor Connection Counts
// 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
// 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:
- Verify
enableHalfOpen: false
in socket forwarding - Check that both proxies have proper cleanup handlers
- Monitor with connection count logging
- Use
test.proxy-chain-simple.node.ts
as reference
Real Client IP Not Preserved
If the backend sees proxy IP instead of client IP:
- Verify outer proxy has
sendProxyProtocol: true
- Verify inner proxy has outer proxy IP in
proxyIPs
list - Check logs for "Connection from trusted proxy" message
- 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 - Low-level socket handling
- Connection Manager - Connection lifecycle
- Route Handler - Request routing
- Test Suite - WrappedSocket unit tests