/** * PROXY Protocol Parser * Implementation of HAProxy PROXY protocol v1 (text format) * Spec: https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt */ import type { IProxyInfo, IProxyParseResult, TProxyProtocol } from './types.js'; /** * PROXY protocol parser */ export class ProxyProtocolParser { static readonly PROXY_V1_SIGNATURE = 'PROXY '; static readonly MAX_HEADER_LENGTH = 107; // Max length for v1 header static readonly HEADER_TERMINATOR = '\r\n'; /** * Parse PROXY protocol v1 header from buffer * Returns proxy info and remaining data after header */ static parse(data: Buffer): IProxyParseResult { // Check if buffer starts with PROXY signature if (!data.toString('ascii', 0, 6).startsWith(this.PROXY_V1_SIGNATURE)) { return { proxyInfo: null, remainingData: data }; } // Find header terminator const headerEndIndex = data.indexOf(this.HEADER_TERMINATOR); if (headerEndIndex === -1) { // Header incomplete, need more data if (data.length > this.MAX_HEADER_LENGTH) { // Header too long, invalid throw new Error('PROXY protocol header exceeds maximum length'); } return { proxyInfo: null, remainingData: data }; } // Extract header line const headerLine = data.toString('ascii', 0, headerEndIndex); const remainingData = data.slice(headerEndIndex + 2); // Skip \r\n // Parse header const parts = headerLine.split(' '); if (parts.length < 2) { throw new Error(`Invalid PROXY protocol header format: ${headerLine}`); } const [signature, protocol] = parts; // Validate protocol if (!['TCP4', 'TCP6', 'UNKNOWN'].includes(protocol)) { throw new Error(`Invalid PROXY protocol: ${protocol}`); } // For UNKNOWN protocol, ignore addresses if (protocol === 'UNKNOWN') { return { proxyInfo: { protocol: 'UNKNOWN', sourceIP: '', sourcePort: 0, destinationIP: '', destinationPort: 0 }, remainingData }; } // For TCP4/TCP6, we need all 6 parts if (parts.length !== 6) { throw new Error(`Invalid PROXY protocol header format: ${headerLine}`); } const [, , srcIP, dstIP, srcPort, dstPort] = parts; // Validate and parse ports const sourcePort = parseInt(srcPort, 10); const destinationPort = parseInt(dstPort, 10); if (isNaN(sourcePort) || sourcePort < 0 || sourcePort > 65535) { throw new Error(`Invalid source port: ${srcPort}`); } if (isNaN(destinationPort) || destinationPort < 0 || destinationPort > 65535) { throw new Error(`Invalid destination port: ${dstPort}`); } // Validate IP addresses const protocolType = protocol as TProxyProtocol; if (!this.isValidIP(srcIP, protocolType)) { throw new Error(`Invalid source IP for ${protocol}: ${srcIP}`); } if (!this.isValidIP(dstIP, protocolType)) { throw new Error(`Invalid destination IP for ${protocol}: ${dstIP}`); } return { proxyInfo: { protocol: protocolType, sourceIP: srcIP, sourcePort, destinationIP: dstIP, destinationPort }, remainingData }; } /** * Generate PROXY protocol v1 header */ static generate(info: IProxyInfo): Buffer { if (info.protocol === 'UNKNOWN') { return Buffer.from(`PROXY UNKNOWN\r\n`, 'ascii'); } const header = `PROXY ${info.protocol} ${info.sourceIP} ${info.destinationIP} ${info.sourcePort} ${info.destinationPort}\r\n`; if (header.length > this.MAX_HEADER_LENGTH) { throw new Error('Generated PROXY protocol header exceeds maximum length'); } return Buffer.from(header, 'ascii'); } /** * Validate IP address format */ static isValidIP(ip: string, protocol: TProxyProtocol): boolean { if (protocol === 'TCP4') { return this.isIPv4(ip); } else if (protocol === 'TCP6') { return this.isIPv6(ip); } return false; } /** * Check if string is valid IPv4 */ static isIPv4(ip: string): boolean { const parts = ip.split('.'); if (parts.length !== 4) return false; for (const part of parts) { const num = parseInt(part, 10); if (isNaN(num) || num < 0 || num > 255 || part !== num.toString()) { return false; } } return true; } /** * Check if string is valid IPv6 */ static isIPv6(ip: string): boolean { // Basic IPv6 validation const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/; return ipv6Regex.test(ip); } /** * Create a connection ID string for tracking */ static createConnectionId(connectionInfo: { sourceIp?: string; sourcePort?: number; destIp?: string; destPort?: number; }): string { const { sourceIp, sourcePort, destIp, destPort } = connectionInfo; return `${sourceIp}:${sourcePort}-${destIp}:${destPort}`; } }