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:
@ -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);
|
||||
|
Reference in New Issue
Block a user