smartproxy/readme.proxy-protocol-example.md
Juergen Kunz b3714d583d Implement PROXY protocol v1 support in SmartProxy
- 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.
2025-06-06 13:45:44 +00:00

462 lines
13 KiB
Markdown

# 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<Buffer | null> {
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