feat(smart-proxy): Improve connection/rate-limit atomicity, SNI parsing, HttpProxy & ACME orchestration, and routing utilities

This commit is contained in:
2025-12-09 13:07:29 +00:00
parent a0b23a8e7e
commit 9c25bf0a27
8 changed files with 363 additions and 114 deletions

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@push.rocks/smartproxy',
version: '22.0.0',
version: '22.1.0',
description: 'A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.'
}

View File

@@ -58,8 +58,16 @@ export class ConnectionManager extends LifecycleComponent {
/**
* Create and track a new connection
* Accepts either a regular net.Socket or a WrappedSocket for transparent PROXY protocol support
*
* @param socket - The socket for the connection
* @param options - Optional configuration
* @param options.connectionId - Pre-generated connection ID (for atomic IP tracking)
* @param options.skipIpTracking - Skip IP tracking (if already done atomically)
*/
public createConnection(socket: plugins.net.Socket | WrappedSocket): IConnectionRecord | null {
public createConnection(
socket: plugins.net.Socket | WrappedSocket,
options?: { connectionId?: string; skipIpTracking?: boolean }
): IConnectionRecord | null {
// Enforce connection limit
if (this.connectionRecords.size >= this.maxConnections) {
// Use deduplicated logging for connection limit
@@ -78,8 +86,8 @@ export class ConnectionManager extends LifecycleComponent {
socket.destroy();
return null;
}
const connectionId = this.generateConnectionId();
const connectionId = options?.connectionId || this.generateConnectionId();
const remoteIP = socket.remoteAddress || '';
const remotePort = socket.remotePort || 0;
const localPort = socket.localPort || 0;
@@ -109,18 +117,23 @@ export class ConnectionManager extends LifecycleComponent {
isBrowserConnection: false,
domainSwitches: 0
};
this.trackConnection(connectionId, record);
this.trackConnection(connectionId, record, options?.skipIpTracking);
return record;
}
/**
* Track an existing connection
* @param connectionId - The connection ID
* @param record - The connection record
* @param skipIpTracking - Skip IP tracking if already done atomically
*/
public trackConnection(connectionId: string, record: IConnectionRecord): void {
public trackConnection(connectionId: string, record: IConnectionRecord, skipIpTracking?: boolean): void {
this.connectionRecords.set(connectionId, record);
this.smartProxy.securityManager.trackConnectionByIP(record.remoteIP, connectionId);
if (!skipIpTracking) {
this.smartProxy.securityManager.trackConnectionByIP(record.remoteIP, connectionId);
}
// Schedule inactivity check
if (!this.smartProxy.settings.disableInactivityCheck) {
this.scheduleInactivityCheck(connectionId, record);

View File

@@ -78,7 +78,7 @@ export class RouteConnectionHandler {
// 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.smartProxy.settings.proxyIPs?.includes(remoteIP)) {
logger.log('debug', `Connection from trusted proxy ${remoteIP}, PROXY protocol parsing will be enabled`, {
@@ -87,31 +87,40 @@ export class RouteConnectionHandler {
});
}
// Validate IP against rate limits and connection limits
// Note: For wrapped sockets, this will use the underlying socket IP until PROXY protocol is parsed
const ipValidation = this.smartProxy.securityManager.validateIP(wrappedSocket.remoteAddress || '');
// Generate connection ID first for atomic IP validation and tracking
const connectionId = this.smartProxy.connectionManager.generateConnectionId();
const clientIP = wrappedSocket.remoteAddress || '';
// Atomically validate IP and track the connection to prevent race conditions
// This ensures concurrent connections from the same IP are properly limited
const ipValidation = this.smartProxy.securityManager.validateAndTrackIP(clientIP, connectionId);
if (!ipValidation.allowed) {
connectionLogDeduplicator.log(
'ip-rejected',
'warn',
`Connection rejected from ${wrappedSocket.remoteAddress}`,
{ remoteIP: wrappedSocket.remoteAddress, reason: ipValidation.reason, component: 'route-handler' },
wrappedSocket.remoteAddress
`Connection rejected from ${clientIP}`,
{ remoteIP: clientIP, reason: ipValidation.reason, component: 'route-handler' },
clientIP
);
cleanupSocket(wrappedSocket.socket, `rejected-${ipValidation.reason}`, { immediate: true });
return;
}
// Create a new connection record with the wrapped socket
const record = this.smartProxy.connectionManager.createConnection(wrappedSocket);
// Skip IP tracking since we already did it atomically above
const record = this.smartProxy.connectionManager.createConnection(wrappedSocket, {
connectionId,
skipIpTracking: true
});
if (!record) {
// Connection was rejected due to limit - socket already destroyed by connection manager
// Connection was rejected due to global limit - clean up the IP tracking we did
this.smartProxy.securityManager.removeConnectionByIP(clientIP, connectionId);
return;
}
// Emit new connection event
this.newConnectionSubject.next(record);
const connectionId = record.id;
// Note: connectionId was already generated above for atomic IP tracking
// Apply socket optimizations (apply to underlying socket)
const underlyingSocket = wrappedSocket.socket;

View File

@@ -166,7 +166,7 @@ export class SecurityManager {
// Check connection rate limit
if (
this.smartProxy.settings.connectionRateLimitPerMinute &&
this.smartProxy.settings.connectionRateLimitPerMinute &&
!this.checkConnectionRate(ip)
) {
return {
@@ -174,7 +174,44 @@ export class SecurityManager {
reason: `Connection rate limit (${this.smartProxy.settings.connectionRateLimitPerMinute}/min) exceeded`
};
}
return { allowed: true };
}
/**
* Atomically validate an IP and track the connection if allowed.
* This prevents race conditions where concurrent connections could bypass per-IP limits.
*
* @param ip - The IP address to validate
* @param connectionId - The connection ID to track if validation passes
* @returns Object with validation result and reason
*/
public validateAndTrackIP(ip: string, connectionId: string): { allowed: boolean; reason?: string } {
// Check connection count limit BEFORE tracking
if (
this.smartProxy.settings.maxConnectionsPerIP &&
this.getConnectionCountByIP(ip) >= this.smartProxy.settings.maxConnectionsPerIP
) {
return {
allowed: false,
reason: `Maximum connections per IP (${this.smartProxy.settings.maxConnectionsPerIP}) exceeded`
};
}
// Check connection rate limit
if (
this.smartProxy.settings.connectionRateLimitPerMinute &&
!this.checkConnectionRate(ip)
) {
return {
allowed: false,
reason: `Connection rate limit (${this.smartProxy.settings.connectionRateLimitPerMinute}/min) exceeded`
};
}
// Validation passed - immediately track to prevent race conditions
this.trackConnectionByIP(ip, connectionId);
return { allowed: true };
}