# SmartProxy PROXY Protocol Implementation Example This document shows how PROXY protocol parsing could be implemented in SmartProxy. Note that this is a conceptual implementation guide - the actual parsing is not yet implemented in the current version. ## Conceptual PROXY Protocol v1 Parser Implementation ### Parser Class ```typescript // This would go in ts/core/utils/proxy-protocol-parser.ts import { logger } from './logger.js'; export interface IProxyProtocolInfo { version: 1 | 2; command: 'PROXY' | 'LOCAL'; family: 'TCP4' | 'TCP6' | 'UNKNOWN'; sourceIP: string; destIP: string; sourcePort: number; destPort: number; headerLength: number; } export class ProxyProtocolParser { private static readonly PROXY_V1_SIGNATURE = 'PROXY '; private static readonly MAX_V1_HEADER_LENGTH = 108; // Max possible v1 header /** * Parse PROXY protocol v1 header from buffer * Returns null if not a valid PROXY protocol header */ static parseV1(buffer: Buffer): IProxyProtocolInfo | null { // Need at least 8 bytes for "PROXY " + newline if (buffer.length < 8) { return null; } // Check for v1 signature const possibleHeader = buffer.toString('ascii', 0, 6); if (possibleHeader !== this.PROXY_V1_SIGNATURE) { return null; } // Find the end of the header (CRLF) let headerEnd = -1; for (let i = 6; i < Math.min(buffer.length, this.MAX_V1_HEADER_LENGTH); i++) { if (buffer[i] === 0x0D && buffer[i + 1] === 0x0A) { // \r\n headerEnd = i + 2; break; } } if (headerEnd === -1) { // No complete header found return null; } // Parse the header line const headerLine = buffer.toString('ascii', 0, headerEnd - 2); const parts = headerLine.split(' '); if (parts.length !== 6) { logger.log('warn', 'Invalid PROXY v1 header format', { headerLine, partCount: parts.length }); return null; } const [proxy, family, srcIP, dstIP, srcPort, dstPort] = parts; // Validate family if (!['TCP4', 'TCP6', 'UNKNOWN'].includes(family)) { logger.log('warn', 'Invalid PROXY protocol family', { family }); return null; } // Validate ports const sourcePort = parseInt(srcPort); const destPort = parseInt(dstPort); if (isNaN(sourcePort) || sourcePort < 1 || sourcePort > 65535 || isNaN(destPort) || destPort < 1 || destPort > 65535) { logger.log('warn', 'Invalid PROXY protocol ports', { srcPort, dstPort }); return null; } return { version: 1, command: 'PROXY', family: family as 'TCP4' | 'TCP6' | 'UNKNOWN', sourceIP: srcIP, destIP: dstIP, sourcePort, destPort, headerLength: headerEnd }; } /** * Check if buffer potentially contains PROXY protocol */ static mightBeProxyProtocol(buffer: Buffer): boolean { if (buffer.length < 6) return false; // Check for v1 signature const start = buffer.toString('ascii', 0, 6); if (start === this.PROXY_V1_SIGNATURE) return true; // Check for v2 signature (12 bytes: \x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A) if (buffer.length >= 12) { const v2Sig = Buffer.from([0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A]); if (buffer.compare(v2Sig, 0, 12, 0, 12) === 0) return true; } return false; } } ``` ### Integration with RouteConnectionHandler ```typescript // This shows how it would be integrated into route-connection-handler.ts private async handleProxyProtocol( socket: plugins.net.Socket, wrappedSocket: WrappedSocket, record: IConnectionRecord ): Promise { const remoteIP = socket.remoteAddress || ''; // Only parse PROXY protocol from trusted IPs if (!this.settings.proxyIPs?.includes(remoteIP)) { return null; } return new Promise((resolve) => { let buffer = Buffer.alloc(0); let headerParsed = false; const parseHandler = (chunk: Buffer) => { // Accumulate data buffer = Buffer.concat([buffer, chunk]); // Try to parse PROXY protocol const proxyInfo = ProxyProtocolParser.parseV1(buffer); if (proxyInfo) { // Update wrapped socket with real client info wrappedSocket.setProxyInfo(proxyInfo.sourceIP, proxyInfo.sourcePort); // Update connection record record.remoteIP = proxyInfo.sourceIP; logger.log('info', 'PROXY protocol parsed', { connectionId: record.id, realIP: proxyInfo.sourceIP, realPort: proxyInfo.sourcePort, proxyIP: remoteIP }); // Remove this handler socket.removeListener('data', parseHandler); headerParsed = true; // Return remaining data after header const remaining = buffer.slice(proxyInfo.headerLength); resolve(remaining.length > 0 ? remaining : null); } else if (buffer.length > 108) { // Max v1 header length exceeded, not PROXY protocol socket.removeListener('data', parseHandler); headerParsed = true; resolve(buffer); } }; // Set timeout for PROXY protocol parsing const timeout = setTimeout(() => { if (!headerParsed) { socket.removeListener('data', parseHandler); logger.log('warn', 'PROXY protocol parsing timeout', { connectionId: record.id, bufferLength: buffer.length }); resolve(buffer.length > 0 ? buffer : null); } }, 1000); // 1 second timeout socket.on('data', parseHandler); // Clean up on early close socket.once('close', () => { clearTimeout(timeout); if (!headerParsed) { socket.removeListener('data', parseHandler); resolve(null); } }); }); } // Modified handleConnection to include PROXY protocol parsing public async handleConnection(socket: plugins.net.Socket): void { const remoteIP = socket.remoteAddress || ''; const localPort = socket.localPort || 0; // Always wrap the socket const wrappedSocket = new WrappedSocket(socket); // Create connection record const record = this.connectionManager.createConnection(wrappedSocket); if (!record) return; // If from trusted proxy, parse PROXY protocol if (this.settings.proxyIPs?.includes(remoteIP)) { const remainingData = await this.handleProxyProtocol(socket, wrappedSocket, record); if (remainingData) { // Process remaining data as normal this.handleInitialData(wrappedSocket, record, remainingData); } else { // Wait for more data this.handleInitialData(wrappedSocket, record); } } else { // Not from trusted proxy, handle normally this.handleInitialData(wrappedSocket, record); } } ``` ### Sending PROXY Protocol When Forwarding ```typescript // This would be added to setupDirectConnection method private setupDirectConnection( socket: plugins.net.Socket | WrappedSocket, record: IConnectionRecord, serverName?: string, initialChunk?: Buffer, overridePort?: number, targetHost?: string, targetPort?: number ): void { // ... existing code ... // Create target socket const targetSocket = createSocketWithErrorHandler({ port: finalTargetPort, host: finalTargetHost, onConnect: () => { // If sendProxyProtocol is enabled, send PROXY header first if (this.settings.sendProxyProtocol) { const proxyHeader = this.buildProxyProtocolHeader(wrappedSocket, targetSocket); targetSocket.write(proxyHeader); } // Then send any pending data if (record.pendingData.length > 0) { const combinedData = Buffer.concat(record.pendingData); targetSocket.write(combinedData); } // ... rest of connection setup ... } }); } private buildProxyProtocolHeader( clientSocket: WrappedSocket, serverSocket: net.Socket ): Buffer { const family = clientSocket.remoteFamily === 'IPv6' ? 'TCP6' : 'TCP4'; const srcIP = clientSocket.remoteAddress || '0.0.0.0'; const srcPort = clientSocket.remotePort || 0; const dstIP = serverSocket.localAddress || '0.0.0.0'; const dstPort = serverSocket.localPort || 0; const header = `PROXY ${family} ${srcIP} ${dstIP} ${srcPort} ${dstPort}\r\n`; return Buffer.from(header, 'ascii'); } ``` ## Complete Example: HAProxy Compatible Setup ```typescript // Example showing a complete HAProxy-compatible SmartProxy setup import { SmartProxy } from '@push.rocks/smartproxy'; // Configuration matching HAProxy's proxy protocol behavior const proxy = new SmartProxy({ // Accept PROXY protocol from these sources (like HAProxy's 'accept-proxy') proxyIPs: [ '10.0.0.0/8', // Private network load balancers '172.16.0.0/12', // Docker networks '192.168.0.0/16' // Local networks ], // Send PROXY protocol to backends (like HAProxy's 'send-proxy') sendProxyProtocol: true, routes: [ { name: 'web-app', match: { ports: 443, domains: ['app.example.com', 'www.example.com'] }, action: { type: 'forward', target: { host: 'backend-pool.internal', port: 8080 }, tls: { mode: 'terminate', certificate: 'auto', acme: { email: 'ssl@example.com' } } } } ] }); // Start the proxy await proxy.start(); // The proxy will now: // 1. Accept connections on port 443 // 2. Parse PROXY protocol from trusted IPs // 3. Terminate TLS // 4. Forward to backend with PROXY protocol header // 5. Backend sees real client IP ``` ## Testing PROXY Protocol ```typescript // Test client that sends PROXY protocol import * as net from 'net'; function createProxyProtocolClient( realClientIP: string, realClientPort: number, proxyHost: string, proxyPort: number ): net.Socket { const client = net.connect(proxyPort, proxyHost); client.on('connect', () => { // Send PROXY protocol header const header = `PROXY TCP4 ${realClientIP} ${proxyHost} ${realClientPort} ${proxyPort}\r\n`; client.write(header); // Then send actual request client.write('GET / HTTP/1.1\r\nHost: example.com\r\n\r\n'); }); return client; } // Usage const client = createProxyProtocolClient( '203.0.113.45', // Real client IP 54321, // Real client port 'localhost', // Proxy host 8080 // Proxy port ); ``` ## AWS Network Load Balancer Example ```typescript // Configuration for AWS NLB with PROXY protocol v2 const proxy = new SmartProxy({ // AWS NLB IP ranges (get current list from AWS) proxyIPs: [ '10.0.0.0/8', // VPC CIDR // Add specific NLB IPs or use AWS IP ranges ], // AWS NLB uses PROXY protocol v2 by default acceptProxyProtocolV2: true, // Future feature routes: [{ name: 'aws-app', match: { ports: 443 }, action: { type: 'forward', target: { host: 'app-cluster.internal', port: 8443 }, tls: { mode: 'passthrough' } } }] }); // The proxy will: // 1. Accept PROXY protocol v2 from AWS NLB // 2. Preserve VPC endpoint IDs and other metadata // 3. Forward to backend with real client information ``` ## Debugging PROXY Protocol ```typescript // Enable detailed logging to debug PROXY protocol parsing const proxy = new SmartProxy({ enableDetailedLogging: true, proxyIPs: ['10.0.0.1'], // Add custom logging for debugging routes: [{ name: 'debug-route', match: { ports: 8080 }, action: { type: 'socket-handler', socketHandler: async (socket, context) => { console.log('Socket handler called with context:', { clientIp: context.clientIp, // Real IP from PROXY protocol port: context.port, connectionId: context.connectionId, timestamp: context.timestamp }); // Handle the socket... } } }] }); ``` ## Security Considerations 1. **Always validate trusted proxy IPs** - Never accept PROXY protocol from untrusted sources 2. **Use specific IP ranges** - Avoid wildcards like `0.0.0.0/0` 3. **Implement rate limiting** - PROXY protocol parsing has a computational cost 4. **Validate header format** - Reject malformed headers immediately 5. **Set parsing timeouts** - Prevent slow loris attacks via PROXY headers 6. **Log parsing failures** - Monitor for potential attacks or misconfigurations ## Performance Considerations 1. **Header parsing overhead** - Minimal, one-time cost per connection 2. **Memory usage** - Small buffer for header accumulation (max 108 bytes for v1) 3. **Connection establishment** - Slight delay for PROXY protocol parsing 4. **Throughput impact** - None after initial header parsing 5. **CPU usage** - Negligible for well-formed headers ## Future Enhancements 1. **PROXY Protocol v2** - Binary format for better performance 2. **TLS information preservation** - Pass TLS version, cipher, SNI via PP2 3. **Custom type-length-value (TLV) fields** - Extended metadata support 4. **Connection pooling** - Reuse backend connections with different client IPs 5. **Health checks** - Skip PROXY protocol for health check connections