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

@ -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);