462 lines
13 KiB
Markdown
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
|