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.
This commit is contained in:
Juergen Kunz 2025-06-06 13:45:44 +00:00
parent 527cacb1a8
commit b3714d583d
12 changed files with 1521 additions and 32 deletions

View File

@ -849,4 +849,11 @@ The WrappedSocket class has been implemented as the foundation for PROXY protoco
- Phase 2: Parse PROXY protocol header from trusted proxies
- Phase 3: Update real client IP/port after parsing
- Phase 4: Test with HAProxy and AWS ELB
- Phase 5: Documentation and configuration
- Phase 5: Documentation and configuration
## Proxy Protocol Documentation
For detailed information about proxy protocol implementation and proxy chaining:
- **[Proxy Protocol Guide](./readme.proxy-protocol.md)** - Complete implementation details and configuration
- **[Proxy Protocol Examples](./readme.proxy-protocol-example.md)** - Code examples and conceptual implementation
- **[Proxy Chain Summary](./readme.proxy-chain-summary.md)** - Quick reference for proxy chaining setup

View File

@ -98,26 +98,32 @@ This phase creates the socket wrapper infrastructure that all subsequent phases
**Deliverables**: ✅ Working WrappedSocket that can wrap any socket and provide transparent access to client info.
#### Phase 2: PROXY Protocol Parser - DEPENDS ON PHASE 1
#### Phase 2: PROXY Protocol Parser - ✅ COMPLETED (v19.5.21)
Only after WrappedSocket is working can we add protocol parsing.
1. Create `ProxyProtocolParser` class in `ts/core/utils/proxy-protocol.ts`
2. Implement v1 text format parsing
3. Add validation and error handling
4. Integrate parser to work WITH WrappedSocket (not into it)
1. Created `ProxyProtocolParser` class in `ts/core/utils/proxy-protocol.ts`
2. Implemented v1 text format parsing with full validation
3. ✅ Added comprehensive error handling and IP validation
4. ✅ Integrated parser to work WITH WrappedSocket in RouteConnectionHandler
#### Phase 3: Connection Handler Integration - DEPENDS ON PHASES 1 & 2
**Deliverables**: ✅ Working PROXY protocol v1 parser that validates headers, extracts client info, and handles both TCP4 and TCP6 protocols.
#### Phase 3: Connection Handler Integration - ✅ COMPLETED (v19.5.21)
1. ✅ Modify `RouteConnectionHandler` to create WrappedSocket for all connections
2. Check if connection is from trusted proxy IP
3. If trusted, attempt to parse PROXY protocol header
4. Update wrapped socket with real client info
5. Continue normal connection handling with wrapped socket
2. Check if connection is from trusted proxy IP
3. If trusted, attempt to parse PROXY protocol header
4. Update wrapped socket with real client info
5. Continue normal connection handling with wrapped socket
#### Phase 4: Outbound PROXY Protocol - DEPENDS ON PHASES 1-3
1. Add PROXY header generation in `setupDirectConnection`
2. Make it configurable per route
3. Send header immediately after TCP connection
4. Use ProxyProtocolSocket for outbound connections too
**Deliverables**: ✅ RouteConnectionHandler now parses PROXY protocol from trusted proxies and updates connection records with real client info.
#### Phase 4: Outbound PROXY Protocol - ✅ COMPLETED (v19.5.21)
1. ✅ Add PROXY header generation in `setupDirectConnection`
2. ✅ Make it configurable per route via `sendProxyProtocol` option
3. ✅ Send header immediately after TCP connection
4. ✅ Added remotePort tracking to connection records
**Deliverables**: ✅ SmartProxy can now send PROXY protocol headers to backend servers when configured, preserving client IP through proxy chains.
#### Phase 5: Security & Validation - FINAL PHASE
1. Validate PROXY headers strictly
@ -293,11 +299,10 @@ if (wrappedSocket instanceof ProxyProtocolSocket) {
### 5. Configuration Examples
#### Basic Setup
#### Basic Setup (IMPLEMENTED ✅)
```typescript
// Outer proxy - sends PROXY protocol
const outerProxy = new SmartProxy({
ports: [443],
routes: [{
name: 'to-inner-proxy',
match: { ports: 443 },
@ -311,9 +316,8 @@ const outerProxy = new SmartProxy({
// Inner proxy - accepts PROXY protocol from outer proxy
const innerProxy = new SmartProxy({
ports: [443],
proxyIPs: ['212.95.99.130'], // Outer proxy IP
// acceptProxyProtocol: true is automatic for proxyIPs
acceptProxyProtocol: true, // Optional - defaults to true when proxyIPs is set
routes: [{
name: 'to-backend',
match: { ports: 443 },

View File

@ -0,0 +1,112 @@
# SmartProxy: Proxy Protocol and Proxy Chaining Summary
## Quick Summary
SmartProxy supports proxy chaining through the **WrappedSocket** infrastructure, which is designed to handle PROXY protocol for preserving real client IP addresses across multiple proxy layers. While the infrastructure is in place (v19.5.19+), the actual PROXY protocol parsing is not yet implemented.
## Current State
### ✅ What's Implemented
- **WrappedSocket class** - Foundation for proxy protocol support
- **Proxy IP configuration** - `proxyIPs` setting to define trusted proxies
- **Socket wrapping** - All incoming connections wrapped automatically
- **Connection tracking** - Real client IP tracking in connection records
- **Test infrastructure** - Tests for proxy chaining scenarios
### ❌ What's Missing
- **PROXY protocol v1 parsing** - Header parsing not implemented
- **PROXY protocol v2 support** - Binary format not supported
- **Automatic header generation** - Must be manually implemented
- **Production testing** - No HAProxy/AWS ELB compatibility tests
## Key Files
### Core Implementation
- `ts/core/models/wrapped-socket.ts` - WrappedSocket class
- `ts/core/models/socket-types.ts` - Helper functions
- `ts/proxies/smart-proxy/route-connection-handler.ts` - Connection handling
- `ts/proxies/smart-proxy/models/interfaces.ts` - Configuration interfaces
### Tests
- `test/test.wrapped-socket.ts` - WrappedSocket unit tests
- `test/test.proxy-chain-simple.node.ts` - Basic proxy chain test
- `test/test.proxy-chaining-accumulation.node.ts` - Connection leak tests
### Documentation
- `readme.proxy-protocol.md` - Detailed implementation guide
- `readme.proxy-protocol-example.md` - Code examples and future implementation
- `readme.hints.md` - Project overview with WrappedSocket notes
## Quick Configuration Example
```typescript
// Outer proxy (internet-facing)
const outerProxy = new SmartProxy({
sendProxyProtocol: true, // Will send PROXY protocol (when implemented)
routes: [{
name: 'forward-to-inner',
match: { ports: 443 },
action: {
type: 'forward',
target: { host: 'inner-proxy.local', port: 443 },
tls: { mode: 'passthrough' }
}
}]
});
// Inner proxy (backend-facing)
const innerProxy = new SmartProxy({
proxyIPs: ['outer-proxy.local'], // Trust the outer proxy
acceptProxyProtocol: true, // Will parse PROXY protocol (when implemented)
routes: [{
name: 'forward-to-backend',
match: { ports: 443, domains: 'api.example.com' },
action: {
type: 'forward',
target: { host: 'backend.local', port: 8080 },
tls: { mode: 'terminate' }
}
}]
});
```
## How It Works (Conceptually)
1. **Client** connects to **Outer Proxy**
2. **Outer Proxy** wraps socket in WrappedSocket
3. **Outer Proxy** forwards to **Inner Proxy**
- Would prepend: `PROXY TCP4 <client-ip> <proxy-ip> <client-port> <proxy-port>\r\n`
4. **Inner Proxy** receives connection from trusted proxy
5. **Inner Proxy** would parse PROXY protocol header
6. **Inner Proxy** updates WrappedSocket with real client IP
7. **Backend** receives connection with preserved client information
## Important Notes
### Connection Cleanup
The fix for proxy chain connection accumulation (v19.5.14+) changed the default socket behavior:
- **Before**: Half-open connections supported by default (caused accumulation)
- **After**: Both sockets close when one closes (prevents accumulation)
- **Override**: Set `enableHalfOpen: true` if half-open needed
### Security
- Only parse PROXY protocol from IPs listed in `proxyIPs`
- Never use `0.0.0.0/0` as a trusted proxy range
- Each proxy in chain must explicitly trust the previous proxy
### Testing
Use the test files as reference implementations:
- Simple chains: `test.proxy-chain-simple.node.ts`
- Connection leaks: `test.proxy-chaining-accumulation.node.ts`
- Rapid reconnects: `test.rapid-retry-cleanup.node.ts`
## Next Steps
To fully implement PROXY protocol support:
1. Implement the parser in `ProxyProtocolParser` class
2. Integrate parser into `handleConnection` method
3. Add header generation to `setupDirectConnection`
4. Test with real proxies (HAProxy, nginx, AWS ELB)
5. Add PROXY protocol v2 support for better performance
See `readme.proxy-protocol-example.md` for detailed implementation examples.

View File

@ -0,0 +1,462 @@
# 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

415
readme.proxy-protocol.md Normal file
View File

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

133
test/test.proxy-protocol.ts Normal file
View File

@ -0,0 +1,133 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as smartproxy from '../ts/index.js';
import { ProxyProtocolParser } from '../ts/core/utils/proxy-protocol.js';
tap.test('PROXY protocol v1 parser - valid headers', async () => {
// Test TCP4 format
const tcp4Header = Buffer.from('PROXY TCP4 192.168.1.1 10.0.0.1 56324 443\r\n', 'ascii');
const tcp4Result = ProxyProtocolParser.parse(tcp4Header);
expect(tcp4Result.proxyInfo).property('protocol').toEqual('TCP4');
expect(tcp4Result.proxyInfo).property('sourceIP').toEqual('192.168.1.1');
expect(tcp4Result.proxyInfo).property('sourcePort').toEqual(56324);
expect(tcp4Result.proxyInfo).property('destinationIP').toEqual('10.0.0.1');
expect(tcp4Result.proxyInfo).property('destinationPort').toEqual(443);
expect(tcp4Result.remainingData.length).toEqual(0);
// Test TCP6 format
const tcp6Header = Buffer.from('PROXY TCP6 2001:db8::1 2001:db8::2 56324 443\r\n', 'ascii');
const tcp6Result = ProxyProtocolParser.parse(tcp6Header);
expect(tcp6Result.proxyInfo).property('protocol').toEqual('TCP6');
expect(tcp6Result.proxyInfo).property('sourceIP').toEqual('2001:db8::1');
expect(tcp6Result.proxyInfo).property('sourcePort').toEqual(56324);
expect(tcp6Result.proxyInfo).property('destinationIP').toEqual('2001:db8::2');
expect(tcp6Result.proxyInfo).property('destinationPort').toEqual(443);
// Test UNKNOWN protocol
const unknownHeader = Buffer.from('PROXY UNKNOWN\r\n', 'ascii');
const unknownResult = ProxyProtocolParser.parse(unknownHeader);
expect(unknownResult.proxyInfo).property('protocol').toEqual('UNKNOWN');
expect(unknownResult.proxyInfo).property('sourceIP').toEqual('');
expect(unknownResult.proxyInfo).property('sourcePort').toEqual(0);
});
tap.test('PROXY protocol v1 parser - with remaining data', async () => {
const headerWithData = Buffer.concat([
Buffer.from('PROXY TCP4 192.168.1.1 10.0.0.1 56324 443\r\n', 'ascii'),
Buffer.from('GET / HTTP/1.1\r\n', 'ascii')
]);
const result = ProxyProtocolParser.parse(headerWithData);
expect(result.proxyInfo).property('protocol').toEqual('TCP4');
expect(result.proxyInfo).property('sourceIP').toEqual('192.168.1.1');
expect(result.remainingData.toString()).toEqual('GET / HTTP/1.1\r\n');
});
tap.test('PROXY protocol v1 parser - invalid headers', async () => {
// Not a PROXY protocol header
const notProxy = Buffer.from('GET / HTTP/1.1\r\n', 'ascii');
const notProxyResult = ProxyProtocolParser.parse(notProxy);
expect(notProxyResult.proxyInfo).toBeNull();
expect(notProxyResult.remainingData).toEqual(notProxy);
// Invalid protocol
expect(() => {
ProxyProtocolParser.parse(Buffer.from('PROXY INVALID 1.1.1.1 2.2.2.2 80 443\r\n', 'ascii'));
}).toThrow();
// Wrong number of fields
expect(() => {
ProxyProtocolParser.parse(Buffer.from('PROXY TCP4 192.168.1.1 10.0.0.1 56324\r\n', 'ascii'));
}).toThrow();
// Invalid port
expect(() => {
ProxyProtocolParser.parse(Buffer.from('PROXY TCP4 192.168.1.1 10.0.0.1 99999 443\r\n', 'ascii'));
}).toThrow();
// Invalid IP for protocol
expect(() => {
ProxyProtocolParser.parse(Buffer.from('PROXY TCP4 2001:db8::1 10.0.0.1 56324 443\r\n', 'ascii'));
}).toThrow();
});
tap.test('PROXY protocol v1 parser - incomplete headers', async () => {
// Header without terminator
const incomplete = Buffer.from('PROXY TCP4 192.168.1.1 10.0.0.1 56324 443', 'ascii');
const result = ProxyProtocolParser.parse(incomplete);
expect(result.proxyInfo).toBeNull();
expect(result.remainingData).toEqual(incomplete);
// Header exceeding max length - create a buffer that actually starts with PROXY
const longHeader = Buffer.from('PROXY TCP4 ' + '1'.repeat(100), 'ascii');
expect(() => {
ProxyProtocolParser.parse(longHeader);
}).toThrow();
});
tap.test('PROXY protocol v1 generator', async () => {
// Generate TCP4 header
const tcp4Info = {
protocol: 'TCP4' as const,
sourceIP: '192.168.1.1',
sourcePort: 56324,
destinationIP: '10.0.0.1',
destinationPort: 443
};
const tcp4Header = ProxyProtocolParser.generate(tcp4Info);
expect(tcp4Header.toString('ascii')).toEqual('PROXY TCP4 192.168.1.1 10.0.0.1 56324 443\r\n');
// Generate TCP6 header
const tcp6Info = {
protocol: 'TCP6' as const,
sourceIP: '2001:db8::1',
sourcePort: 56324,
destinationIP: '2001:db8::2',
destinationPort: 443
};
const tcp6Header = ProxyProtocolParser.generate(tcp6Info);
expect(tcp6Header.toString('ascii')).toEqual('PROXY TCP6 2001:db8::1 2001:db8::2 56324 443\r\n');
// Generate UNKNOWN header
const unknownInfo = {
protocol: 'UNKNOWN' as const,
sourceIP: '',
sourcePort: 0,
destinationIP: '',
destinationPort: 0
};
const unknownHeader = ProxyProtocolParser.generate(unknownInfo);
expect(unknownHeader.toString('ascii')).toEqual('PROXY UNKNOWN\r\n');
});
// Skipping integration tests for now - focus on unit tests
// Integration tests would require more complex setup and teardown
tap.start();

View File

@ -15,3 +15,4 @@ export * from './lifecycle-component.js';
export * from './binary-heap.js';
export * from './enhanced-connection-pool.js';
export * from './socket-utils.js';
export * from './proxy-protocol.js';

View File

@ -0,0 +1,246 @@
import * as plugins from '../../plugins.js';
import { logger } from './logger.js';
/**
* Interface representing parsed PROXY protocol information
*/
export interface IProxyInfo {
protocol: 'TCP4' | 'TCP6' | 'UNKNOWN';
sourceIP: string;
sourcePort: number;
destinationIP: string;
destinationPort: number;
}
/**
* Interface for parse result including remaining data
*/
export interface IProxyParseResult {
proxyInfo: IProxyInfo | null;
remainingData: Buffer;
}
/**
* Parser for PROXY protocol v1 (text format)
* Spec: https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
*/
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 'TCP4' | 'TCP6' | 'UNKNOWN';
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: protocol as 'TCP4' | 'TCP6',
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
*/
private static isValidIP(ip: string, protocol: 'TCP4' | 'TCP6' | 'UNKNOWN'): boolean {
if (protocol === 'TCP4') {
return plugins.net.isIPv4(ip);
} else if (protocol === 'TCP6') {
return plugins.net.isIPv6(ip);
}
return false;
}
/**
* Attempt to read a complete PROXY protocol header from a socket
* Returns null if no PROXY protocol detected or incomplete
*/
static async readFromSocket(socket: plugins.net.Socket, timeout: number = 5000): Promise<IProxyParseResult | null> {
return new Promise((resolve) => {
let buffer = Buffer.alloc(0);
let resolved = false;
const cleanup = () => {
socket.removeListener('data', onData);
socket.removeListener('error', onError);
clearTimeout(timer);
};
const timer = setTimeout(() => {
if (!resolved) {
resolved = true;
cleanup();
resolve({
proxyInfo: null,
remainingData: buffer
});
}
}, timeout);
const onData = (chunk: Buffer) => {
buffer = Buffer.concat([buffer, chunk]);
// Check if we have enough data
if (!buffer.toString('ascii', 0, Math.min(6, buffer.length)).startsWith(this.PROXY_V1_SIGNATURE)) {
// Not PROXY protocol
resolved = true;
cleanup();
resolve({
proxyInfo: null,
remainingData: buffer
});
return;
}
// Try to parse
try {
const result = this.parse(buffer);
if (result.proxyInfo) {
// Successfully parsed
resolved = true;
cleanup();
resolve(result);
} else if (buffer.length > this.MAX_HEADER_LENGTH) {
// Header too long
resolved = true;
cleanup();
resolve({
proxyInfo: null,
remainingData: buffer
});
}
// Otherwise continue reading
} catch (error) {
// Parse error
logger.log('error', `PROXY protocol parse error: ${error.message}`);
resolved = true;
cleanup();
resolve({
proxyInfo: null,
remainingData: buffer
});
}
};
const onError = (error: Error) => {
logger.log('error', `Socket error while reading PROXY protocol: ${error.message}`);
resolved = true;
cleanup();
resolve({
proxyInfo: null,
remainingData: buffer
});
};
socket.on('data', onData);
socket.on('error', onError);
});
}
}

View File

@ -70,6 +70,7 @@ export class ConnectionManager extends LifecycleComponent {
const connectionId = this.generateConnectionId();
const remoteIP = socket.remoteAddress || '';
const remotePort = socket.remotePort || 0;
const localPort = socket.localPort || 0;
const now = Date.now();
@ -85,6 +86,7 @@ export class ConnectionManager extends LifecycleComponent {
bytesReceived: 0,
bytesSent: 0,
remoteIP,
remotePort,
localPort,
isTLS: false,
tlsHandshakeComplete: false,

View File

@ -151,6 +151,7 @@ export interface IConnectionRecord {
bytesReceived: number; // Total bytes received
bytesSent: number; // Total bytes sent
remoteIP: string; // Remote IP (cached for logging after socket close)
remotePort: number; // Remote port (cached for logging after socket close)
localPort: number; // Local port (cached for logging)
isTLS: boolean; // Whether this connection is a TLS connection
tlsHandshakeComplete: boolean; // Whether the TLS handshake is complete

View File

@ -250,6 +250,9 @@ export interface IRouteAction {
// Socket handler function (when type is 'socket-handler')
socketHandler?: TSocketHandler;
// PROXY protocol support
sendProxyProtocol?: boolean;
}
/**

View File

@ -13,6 +13,7 @@ import { SharedRouteManager as RouteManager } from '../../core/routing/route-man
import { cleanupSocket, createIndependentSocketHandlers, setupSocketHandlers, createSocketWithErrorHandler, setupBidirectionalForwarding } from '../../core/utils/socket-utils.js';
import { WrappedSocket } from '../../core/models/wrapped-socket.js';
import { getUnderlyingSocket } from '../../core/models/socket-types.js';
import { ProxyProtocolParser } from '../../core/utils/proxy-protocol.js';
/**
* Handles new connection processing and setup logic with support for route-based configuration
@ -295,17 +296,8 @@ export class RouteConnectionHandler {
}
});
// First data handler to capture initial TLS handshake
socket.once('data', (chunk: Buffer) => {
// Clear the initial timeout since we've received data
if (initialTimeout) {
clearTimeout(initialTimeout);
initialTimeout = null;
}
initialDataReceived = true;
record.hasReceivedInitialData = true;
// Handler for processing initial data (after potential PROXY protocol)
const processInitialData = (chunk: Buffer) => {
// Block non-TLS connections on port 443
if (!this.tlsManager.isTlsHandshake(chunk) && localPort === 443) {
logger.log('warn', `Non-TLS connection ${connectionId} detected on port 443. Terminating connection - only TLS traffic is allowed on standard HTTPS port.`, {
@ -381,6 +373,67 @@ export class RouteConnectionHandler {
// Find the appropriate route for this connection
this.routeConnection(socket, record, serverName, chunk);
};
// First data handler to capture initial TLS handshake or PROXY protocol
socket.once('data', async (chunk: Buffer) => {
// Clear the initial timeout since we've received data
if (initialTimeout) {
clearTimeout(initialTimeout);
initialTimeout = null;
}
initialDataReceived = true;
record.hasReceivedInitialData = true;
// Check if this is from a trusted proxy and might have PROXY protocol
if (this.settings.proxyIPs?.includes(socket.remoteAddress || '') && this.settings.acceptProxyProtocol !== false) {
// Check if this starts with PROXY protocol
if (chunk.toString('ascii', 0, Math.min(6, chunk.length)).startsWith('PROXY ')) {
try {
const parseResult = ProxyProtocolParser.parse(chunk);
if (parseResult.proxyInfo) {
// Update the wrapped socket with real client info (if it's a WrappedSocket)
if (socket instanceof WrappedSocket) {
socket.setProxyInfo(parseResult.proxyInfo.sourceIP, parseResult.proxyInfo.sourcePort);
}
// Update connection record with real client info
record.remoteIP = parseResult.proxyInfo.sourceIP;
record.remotePort = parseResult.proxyInfo.sourcePort;
logger.log('info', `PROXY protocol parsed successfully`, {
connectionId,
realClientIP: parseResult.proxyInfo.sourceIP,
realClientPort: parseResult.proxyInfo.sourcePort,
proxyIP: socket.remoteAddress,
component: 'route-handler'
});
// Process remaining data if any
if (parseResult.remainingData.length > 0) {
processInitialData(parseResult.remainingData);
} else {
// Wait for more data
socket.once('data', processInitialData);
}
return;
}
} catch (error) {
logger.log('error', `Failed to parse PROXY protocol from trusted proxy`, {
connectionId,
error: error.message,
proxyIP: socket.remoteAddress,
component: 'route-handler'
});
// Continue processing as normal data
}
}
}
// Process as normal data (no PROXY protocol)
processInitialData(chunk);
});
}
@ -1119,7 +1172,7 @@ export class RouteConnectionHandler {
// Clean up the connection record - this is critical!
this.connectionManager.cleanupConnection(record, `connection_failed_${(error as any).code || 'unknown'}`);
},
onConnect: () => {
onConnect: async () => {
if (this.settings.enableDetailedLogging) {
logger.log('info', `Connection ${connectionId} established to target ${finalTargetHost}:${finalTargetPort}`, {
connectionId,
@ -1135,6 +1188,56 @@ export class RouteConnectionHandler {
// Add the normal error handler for established connections
targetSocket.on('error', this.connectionManager.handleError('outgoing', record));
// Check if we should send PROXY protocol header
const shouldSendProxyProtocol = record.routeConfig?.action?.sendProxyProtocol ||
this.settings.sendProxyProtocol;
if (shouldSendProxyProtocol) {
try {
// Generate PROXY protocol header
const proxyInfo = {
protocol: (record.remoteIP.includes(':') ? 'TCP6' : 'TCP4') as 'TCP4' | 'TCP6',
sourceIP: record.remoteIP,
sourcePort: record.remotePort || socket.remotePort || 0,
destinationIP: socket.localAddress || '',
destinationPort: socket.localPort || 0
};
const proxyHeader = ProxyProtocolParser.generate(proxyInfo);
// Send PROXY protocol header first
await new Promise<void>((resolve, reject) => {
targetSocket.write(proxyHeader, (err) => {
if (err) {
logger.log('error', `Failed to send PROXY protocol header`, {
connectionId,
error: err.message,
component: 'route-handler'
});
reject(err);
} else {
logger.log('info', `PROXY protocol header sent to backend`, {
connectionId,
targetHost: finalTargetHost,
targetPort: finalTargetPort,
sourceIP: proxyInfo.sourceIP,
sourcePort: proxyInfo.sourcePort,
component: 'route-handler'
});
resolve();
}
});
});
} catch (error) {
logger.log('error', `Error sending PROXY protocol header`, {
connectionId,
error: error.message,
component: 'route-handler'
});
// Continue anyway - don't break the connection
}
}
// Flush any pending data to target
if (record.pendingData.length > 0) {
const combinedData = Buffer.concat(record.pendingData);