@push.rocks/smartproxy 21.1.7 → 22.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/changelog.md +81 -0
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/core/utils/shared-security-manager.d.ts +17 -0
  4. package/dist_ts/core/utils/shared-security-manager.js +66 -1
  5. package/dist_ts/proxies/http-proxy/default-certificates.d.ts +54 -0
  6. package/dist_ts/proxies/http-proxy/default-certificates.js +127 -0
  7. package/dist_ts/proxies/http-proxy/http-proxy.d.ts +1 -1
  8. package/dist_ts/proxies/http-proxy/http-proxy.js +9 -14
  9. package/dist_ts/proxies/http-proxy/index.d.ts +5 -1
  10. package/dist_ts/proxies/http-proxy/index.js +6 -2
  11. package/dist_ts/proxies/http-proxy/security-manager.d.ts +4 -12
  12. package/dist_ts/proxies/http-proxy/security-manager.js +66 -99
  13. package/dist_ts/proxies/nftables-proxy/index.d.ts +1 -0
  14. package/dist_ts/proxies/nftables-proxy/index.js +2 -1
  15. package/dist_ts/proxies/nftables-proxy/nftables-proxy.d.ts +4 -26
  16. package/dist_ts/proxies/nftables-proxy/nftables-proxy.js +84 -236
  17. package/dist_ts/proxies/nftables-proxy/utils/index.d.ts +9 -0
  18. package/dist_ts/proxies/nftables-proxy/utils/index.js +12 -0
  19. package/dist_ts/proxies/nftables-proxy/utils/nft-command-executor.d.ts +66 -0
  20. package/dist_ts/proxies/nftables-proxy/utils/nft-command-executor.js +131 -0
  21. package/dist_ts/proxies/nftables-proxy/utils/nft-port-spec-normalizer.d.ts +39 -0
  22. package/dist_ts/proxies/nftables-proxy/utils/nft-port-spec-normalizer.js +112 -0
  23. package/dist_ts/proxies/nftables-proxy/utils/nft-rule-validator.d.ts +59 -0
  24. package/dist_ts/proxies/nftables-proxy/utils/nft-rule-validator.js +130 -0
  25. package/dist_ts/proxies/smart-proxy/certificate-manager.js +4 -3
  26. package/dist_ts/proxies/smart-proxy/connection-manager.d.ts +13 -2
  27. package/dist_ts/proxies/smart-proxy/connection-manager.js +16 -6
  28. package/dist_ts/proxies/smart-proxy/http-proxy-bridge.js +35 -10
  29. package/dist_ts/proxies/smart-proxy/models/interfaces.d.ts +0 -1
  30. package/dist_ts/proxies/smart-proxy/route-connection-handler.d.ts +17 -0
  31. package/dist_ts/proxies/smart-proxy/route-connection-handler.js +72 -9
  32. package/dist_ts/proxies/smart-proxy/security-manager.d.ts +14 -12
  33. package/dist_ts/proxies/smart-proxy/security-manager.js +80 -74
  34. package/dist_ts/proxies/smart-proxy/smart-proxy.js +1 -2
  35. package/dist_ts/proxies/smart-proxy/tls-manager.d.ts +2 -9
  36. package/dist_ts/proxies/smart-proxy/tls-manager.js +3 -26
  37. package/dist_ts/proxies/smart-proxy/utils/index.d.ts +1 -1
  38. package/dist_ts/proxies/smart-proxy/utils/index.js +3 -4
  39. package/dist_ts/proxies/smart-proxy/utils/route-helpers/api-helpers.d.ts +49 -0
  40. package/dist_ts/proxies/smart-proxy/utils/route-helpers/api-helpers.js +108 -0
  41. package/dist_ts/proxies/smart-proxy/utils/route-helpers/dynamic-helpers.d.ts +57 -0
  42. package/dist_ts/proxies/smart-proxy/utils/route-helpers/dynamic-helpers.js +89 -0
  43. package/dist_ts/proxies/smart-proxy/utils/route-helpers/http-helpers.d.ts +17 -0
  44. package/dist_ts/proxies/smart-proxy/utils/route-helpers/http-helpers.js +32 -0
  45. package/dist_ts/proxies/smart-proxy/utils/route-helpers/https-helpers.d.ts +68 -0
  46. package/dist_ts/proxies/smart-proxy/utils/route-helpers/https-helpers.js +117 -0
  47. package/dist_ts/proxies/smart-proxy/utils/route-helpers/index.d.ts +17 -0
  48. package/dist_ts/proxies/smart-proxy/utils/route-helpers/index.js +27 -0
  49. package/dist_ts/proxies/smart-proxy/utils/route-helpers/load-balancer-helpers.d.ts +63 -0
  50. package/dist_ts/proxies/smart-proxy/utils/route-helpers/load-balancer-helpers.js +105 -0
  51. package/dist_ts/proxies/smart-proxy/utils/route-helpers/nftables-helpers.d.ts +83 -0
  52. package/dist_ts/proxies/smart-proxy/utils/route-helpers/nftables-helpers.js +126 -0
  53. package/dist_ts/proxies/smart-proxy/utils/route-helpers/security-helpers.d.ts +47 -0
  54. package/dist_ts/proxies/smart-proxy/utils/route-helpers/security-helpers.js +66 -0
  55. package/dist_ts/proxies/smart-proxy/utils/route-helpers/socket-handlers.d.ts +70 -0
  56. package/dist_ts/proxies/smart-proxy/utils/route-helpers/socket-handlers.js +287 -0
  57. package/dist_ts/proxies/smart-proxy/utils/route-helpers/websocket-helpers.d.ts +46 -0
  58. package/dist_ts/proxies/smart-proxy/utils/route-helpers/websocket-helpers.js +67 -0
  59. package/dist_ts/proxies/smart-proxy/utils/route-helpers.d.ts +4 -457
  60. package/dist_ts/proxies/smart-proxy/utils/route-helpers.js +6 -950
  61. package/dist_ts/proxies/smart-proxy/utils/route-utils.js +2 -2
  62. package/dist_ts/proxies/smart-proxy/utils/route-validator.d.ts +67 -1
  63. package/dist_ts/proxies/smart-proxy/utils/route-validator.js +251 -3
  64. package/npmextra.json +12 -6
  65. package/package.json +34 -24
  66. package/readme.hints.md +184 -1
  67. package/readme.md +235 -172
  68. package/ts/00_commitinfo_data.ts +1 -1
  69. package/ts/core/utils/shared-security-manager.ts +98 -13
  70. package/ts/proxies/http-proxy/default-certificates.ts +150 -0
  71. package/ts/proxies/http-proxy/http-proxy.ts +9 -15
  72. package/ts/proxies/http-proxy/index.ts +6 -1
  73. package/ts/proxies/http-proxy/security-manager.ts +141 -161
  74. package/ts/proxies/nftables-proxy/index.ts +1 -0
  75. package/ts/proxies/nftables-proxy/nftables-proxy.ts +116 -290
  76. package/ts/proxies/nftables-proxy/utils/index.ts +38 -0
  77. package/ts/proxies/nftables-proxy/utils/nft-command-executor.ts +162 -0
  78. package/ts/proxies/nftables-proxy/utils/nft-port-spec-normalizer.ts +125 -0
  79. package/ts/proxies/nftables-proxy/utils/nft-rule-validator.ts +156 -0
  80. package/ts/proxies/smart-proxy/certificate-manager.ts +3 -2
  81. package/ts/proxies/smart-proxy/connection-manager.ts +21 -8
  82. package/ts/proxies/smart-proxy/http-proxy-bridge.ts +39 -13
  83. package/ts/proxies/smart-proxy/models/interfaces.ts +0 -1
  84. package/ts/proxies/smart-proxy/route-connection-handler.ts +88 -16
  85. package/ts/proxies/smart-proxy/security-manager.ts +98 -86
  86. package/ts/proxies/smart-proxy/smart-proxy.ts +0 -2
  87. package/ts/proxies/smart-proxy/tls-manager.ts +1 -37
  88. package/ts/proxies/smart-proxy/utils/index.ts +3 -5
  89. package/ts/proxies/smart-proxy/utils/route-helpers/api-helpers.ts +144 -0
  90. package/ts/proxies/smart-proxy/utils/route-helpers/dynamic-helpers.ts +124 -0
  91. package/ts/proxies/smart-proxy/utils/route-helpers/http-helpers.ts +40 -0
  92. package/ts/proxies/smart-proxy/utils/route-helpers/https-helpers.ts +163 -0
  93. package/ts/proxies/smart-proxy/utils/route-helpers/index.ts +62 -0
  94. package/ts/proxies/smart-proxy/utils/route-helpers/load-balancer-helpers.ts +154 -0
  95. package/ts/proxies/smart-proxy/utils/route-helpers/nftables-helpers.ts +202 -0
  96. package/ts/proxies/smart-proxy/utils/route-helpers/security-helpers.ts +96 -0
  97. package/ts/proxies/smart-proxy/utils/route-helpers/socket-handlers.ts +337 -0
  98. package/ts/proxies/smart-proxy/utils/route-helpers/websocket-helpers.ts +98 -0
  99. package/ts/proxies/smart-proxy/utils/route-helpers.ts +5 -1302
  100. package/ts/proxies/smart-proxy/utils/route-utils.ts +1 -1
  101. package/ts/proxies/smart-proxy/utils/route-validator.ts +274 -4
  102. package/ts/proxies/http-proxy/certificate-manager.ts +0 -244
  103. package/ts/proxies/smart-proxy/utils/route-validators.ts +0 -283
@@ -69,6 +69,58 @@ export class RouteConnectionHandler {
69
69
  };
70
70
  }
71
71
 
72
+ /**
73
+ * Determines if SNI is required for routing decisions on this port.
74
+ *
75
+ * SNI is REQUIRED when:
76
+ * - Multiple routes exist on this port (need SNI to pick correct route)
77
+ * - Route has dynamic target function (needs ctx.domain)
78
+ * - Route has specific domain restriction (strict validation)
79
+ *
80
+ * SNI is NOT required when:
81
+ * - TLS termination mode (HttpProxy handles session resumption)
82
+ * - Single route with static target and no domain restriction (or wildcard)
83
+ */
84
+ private calculateSniRequirement(port: number): boolean {
85
+ const routesOnPort = this.smartProxy.routeManager.getRoutesForPort(port);
86
+
87
+ // No routes = no SNI requirement (will fail routing anyway)
88
+ if (routesOnPort.length === 0) return false;
89
+
90
+ // Check if any route terminates TLS - if so, SNI not required
91
+ // (HttpProxy handles session resumption internally)
92
+ const hasTermination = routesOnPort.some(route =>
93
+ route.action.tls?.mode === 'terminate' ||
94
+ route.action.tls?.mode === 'terminate-and-reencrypt'
95
+ );
96
+ if (hasTermination) return false;
97
+
98
+ // Multiple routes = need SNI to pick the correct route
99
+ if (routesOnPort.length > 1) return true;
100
+
101
+ // Single route - check if it needs SNI for validation or routing
102
+ const route = routesOnPort[0];
103
+
104
+ // Dynamic host selection requires SNI (function receives ctx.domain)
105
+ const hasDynamicTarget = route.action.targets?.some(t => typeof t.host === 'function');
106
+ if (hasDynamicTarget) return true;
107
+
108
+ // Specific domain restriction requires SNI for strict validation
109
+ const hasSpecificDomain = route.match.domains && !this.isWildcardOnly(route.match.domains);
110
+ if (hasSpecificDomain) return true;
111
+
112
+ // Single route, static target(s), no domain restriction = SNI not required
113
+ return false;
114
+ }
115
+
116
+ /**
117
+ * Check if domains config is wildcard-only (matches everything)
118
+ */
119
+ private isWildcardOnly(domains: string | string[]): boolean {
120
+ const domainList = Array.isArray(domains) ? domains : [domains];
121
+ return domainList.length === 1 && domainList[0] === '*';
122
+ }
123
+
72
124
  /**
73
125
  * Handle a new incoming connection
74
126
  */
@@ -78,7 +130,7 @@ export class RouteConnectionHandler {
78
130
 
79
131
  // Always wrap the socket to prepare for potential PROXY protocol
80
132
  const wrappedSocket = new WrappedSocket(socket);
81
-
133
+
82
134
  // If this is from a trusted proxy, log it
83
135
  if (this.smartProxy.settings.proxyIPs?.includes(remoteIP)) {
84
136
  logger.log('debug', `Connection from trusted proxy ${remoteIP}, PROXY protocol parsing will be enabled`, {
@@ -87,31 +139,40 @@ export class RouteConnectionHandler {
87
139
  });
88
140
  }
89
141
 
90
- // Validate IP against rate limits and connection limits
91
- // Note: For wrapped sockets, this will use the underlying socket IP until PROXY protocol is parsed
92
- const ipValidation = this.smartProxy.securityManager.validateIP(wrappedSocket.remoteAddress || '');
142
+ // Generate connection ID first for atomic IP validation and tracking
143
+ const connectionId = this.smartProxy.connectionManager.generateConnectionId();
144
+ const clientIP = wrappedSocket.remoteAddress || '';
145
+
146
+ // Atomically validate IP and track the connection to prevent race conditions
147
+ // This ensures concurrent connections from the same IP are properly limited
148
+ const ipValidation = this.smartProxy.securityManager.validateAndTrackIP(clientIP, connectionId);
93
149
  if (!ipValidation.allowed) {
94
150
  connectionLogDeduplicator.log(
95
151
  'ip-rejected',
96
152
  'warn',
97
- `Connection rejected from ${wrappedSocket.remoteAddress}`,
98
- { remoteIP: wrappedSocket.remoteAddress, reason: ipValidation.reason, component: 'route-handler' },
99
- wrappedSocket.remoteAddress
153
+ `Connection rejected from ${clientIP}`,
154
+ { remoteIP: clientIP, reason: ipValidation.reason, component: 'route-handler' },
155
+ clientIP
100
156
  );
101
157
  cleanupSocket(wrappedSocket.socket, `rejected-${ipValidation.reason}`, { immediate: true });
102
158
  return;
103
159
  }
104
160
 
105
161
  // Create a new connection record with the wrapped socket
106
- const record = this.smartProxy.connectionManager.createConnection(wrappedSocket);
162
+ // Skip IP tracking since we already did it atomically above
163
+ const record = this.smartProxy.connectionManager.createConnection(wrappedSocket, {
164
+ connectionId,
165
+ skipIpTracking: true
166
+ });
107
167
  if (!record) {
108
- // Connection was rejected due to limit - socket already destroyed by connection manager
168
+ // Connection was rejected due to global limit - clean up the IP tracking we did
169
+ this.smartProxy.securityManager.removeConnectionByIP(clientIP, connectionId);
109
170
  return;
110
171
  }
111
172
 
112
173
  // Emit new connection event
113
174
  this.newConnectionSubject.next(record);
114
- const connectionId = record.id;
175
+ // Note: connectionId was already generated above for atomic IP tracking
115
176
 
116
177
  // Apply socket optimizations (apply to underlying socket)
117
178
  const underlyingSocket = wrappedSocket.socket;
@@ -184,14 +245,19 @@ export class RouteConnectionHandler {
184
245
  const needsTlsHandling = allRoutes.some(route => {
185
246
  // Check if route matches this port
186
247
  const matchesPort = this.smartProxy.routeManager.getRoutesForPort(localPort).includes(route);
187
-
188
- return matchesPort &&
189
- route.action.type === 'forward' &&
190
- route.action.tls &&
191
- (route.action.tls.mode === 'terminate' ||
248
+
249
+ return matchesPort &&
250
+ route.action.type === 'forward' &&
251
+ route.action.tls &&
252
+ (route.action.tls.mode === 'terminate' ||
192
253
  route.action.tls.mode === 'passthrough');
193
254
  });
194
255
 
256
+ // Smart SNI requirement calculation
257
+ // Determines if we need SNI for routing decisions on this port
258
+ const needsSniForRouting = this.calculateSniRequirement(localPort);
259
+ const allowSessionTicket = !needsSniForRouting;
260
+
195
261
  // If no routes require TLS handling and it's not port 443, route immediately
196
262
  if (!needsTlsHandling && localPort !== 443) {
197
263
  // Extract underlying socket for socket-utils functions
@@ -345,7 +411,7 @@ export class RouteConnectionHandler {
345
411
  record.lockedDomain = serverName;
346
412
 
347
413
  // Check if we should reject connections without SNI
348
- if (!serverName && this.smartProxy.settings.allowSessionTicket === false) {
414
+ if (!serverName && allowSessionTicket === false) {
349
415
  logger.log('warn', `No SNI detected in TLS ClientHello for connection ${record.id}; sending TLS alert`, {
350
416
  connectionId: record.id,
351
417
  component: 'route-handler'
@@ -1424,6 +1490,12 @@ export class RouteConnectionHandler {
1424
1490
  );
1425
1491
  }
1426
1492
 
1493
+ // Record the initial chunk bytes for metrics
1494
+ record.bytesReceived += combinedData.length;
1495
+ if (this.smartProxy.metricsCollector) {
1496
+ this.smartProxy.metricsCollector.recordBytes(record.id, combinedData.length, 0);
1497
+ }
1498
+
1427
1499
  // Write pending data immediately
1428
1500
  targetSocket.write(combinedData, (err) => {
1429
1501
  if (err) {
@@ -1,10 +1,11 @@
1
1
  import * as plugins from '../../plugins.js';
2
2
  import type { SmartProxy } from './smart-proxy.js';
3
- import { logger } from '../../core/utils/logger.js';
4
3
  import { connectionLogDeduplicator } from '../../core/utils/log-deduplicator.js';
4
+ import { isIPAuthorized, normalizeIP } from '../../core/utils/security-utils.js';
5
5
 
6
6
  /**
7
7
  * Handles security aspects like IP tracking, rate limiting, and authorization
8
+ * for SmartProxy. This is a lightweight wrapper that uses shared utilities.
8
9
  */
9
10
  export class SecurityManager {
10
11
  private connectionsByIP: Map<string, Set<string>> = new Map();
@@ -15,14 +16,22 @@ export class SecurityManager {
15
16
  // Start periodic cleanup every 60 seconds
16
17
  this.startPeriodicCleanup();
17
18
  }
18
-
19
+
19
20
  /**
20
- * Get connections count by IP
21
+ * Get connections count by IP (checks normalized variants)
21
22
  */
22
23
  public getConnectionCountByIP(ip: string): number {
23
- return this.connectionsByIP.get(ip)?.size || 0;
24
+ // Check all normalized variants of the IP
25
+ const variants = normalizeIP(ip);
26
+ for (const variant of variants) {
27
+ const connections = this.connectionsByIP.get(variant);
28
+ if (connections) {
29
+ return connections.size;
30
+ }
31
+ }
32
+ return 0;
24
33
  }
25
-
34
+
26
35
  /**
27
36
  * Check and update connection rate for an IP
28
37
  * @returns true if within rate limit, false if exceeding limit
@@ -31,43 +40,73 @@ export class SecurityManager {
31
40
  const now = Date.now();
32
41
  const minute = 60 * 1000;
33
42
 
34
- if (!this.connectionRateByIP.has(ip)) {
35
- this.connectionRateByIP.set(ip, [now]);
43
+ // Find existing rate tracking (check normalized variants)
44
+ const variants = normalizeIP(ip);
45
+ let existingKey: string | null = null;
46
+ for (const variant of variants) {
47
+ if (this.connectionRateByIP.has(variant)) {
48
+ existingKey = variant;
49
+ break;
50
+ }
51
+ }
52
+
53
+ const key = existingKey || ip;
54
+
55
+ if (!this.connectionRateByIP.has(key)) {
56
+ this.connectionRateByIP.set(key, [now]);
36
57
  return true;
37
58
  }
38
59
 
39
60
  // Get timestamps and filter out entries older than 1 minute
40
- const timestamps = this.connectionRateByIP.get(ip)!.filter((time) => now - time < minute);
61
+ const timestamps = this.connectionRateByIP.get(key)!.filter((time) => now - time < minute);
41
62
  timestamps.push(now);
42
- this.connectionRateByIP.set(ip, timestamps);
63
+ this.connectionRateByIP.set(key, timestamps);
43
64
 
44
65
  // Check if rate exceeds limit
45
66
  return timestamps.length <= this.smartProxy.settings.connectionRateLimitPerMinute!;
46
67
  }
47
-
68
+
48
69
  /**
49
70
  * Track connection by IP
50
71
  */
51
72
  public trackConnectionByIP(ip: string, connectionId: string): void {
52
- if (!this.connectionsByIP.has(ip)) {
53
- this.connectionsByIP.set(ip, new Set());
73
+ // Check if any variant already exists
74
+ const variants = normalizeIP(ip);
75
+ let existingKey: string | null = null;
76
+
77
+ for (const variant of variants) {
78
+ if (this.connectionsByIP.has(variant)) {
79
+ existingKey = variant;
80
+ break;
81
+ }
54
82
  }
55
- this.connectionsByIP.get(ip)!.add(connectionId);
83
+
84
+ const key = existingKey || ip;
85
+ if (!this.connectionsByIP.has(key)) {
86
+ this.connectionsByIP.set(key, new Set());
87
+ }
88
+ this.connectionsByIP.get(key)!.add(connectionId);
56
89
  }
57
-
90
+
58
91
  /**
59
92
  * Remove connection tracking for an IP
60
93
  */
61
94
  public removeConnectionByIP(ip: string, connectionId: string): void {
62
- if (this.connectionsByIP.has(ip)) {
63
- const connections = this.connectionsByIP.get(ip)!;
64
- connections.delete(connectionId);
65
- if (connections.size === 0) {
66
- this.connectionsByIP.delete(ip);
95
+ // Check all variants to find where the connection is tracked
96
+ const variants = normalizeIP(ip);
97
+
98
+ for (const variant of variants) {
99
+ if (this.connectionsByIP.has(variant)) {
100
+ const connections = this.connectionsByIP.get(variant)!;
101
+ connections.delete(connectionId);
102
+ if (connections.size === 0) {
103
+ this.connectionsByIP.delete(variant);
104
+ }
105
+ break;
67
106
  }
68
107
  }
69
108
  }
70
-
109
+
71
110
  /**
72
111
  * Check if an IP is authorized using security rules
73
112
  *
@@ -81,79 +120,49 @@ export class SecurityManager {
81
120
  * @returns true if IP is authorized, false if blocked
82
121
  */
83
122
  public isIPAuthorized(ip: string, allowedIPs: string[], blockedIPs: string[] = []): boolean {
84
- // Skip IP validation if allowedIPs is empty
85
- if (!ip || (allowedIPs.length === 0 && blockedIPs.length === 0)) {
86
- return true;
87
- }
88
-
89
- // First check if IP is blocked - blocked IPs take precedence
90
- if (blockedIPs.length > 0 && this.isGlobIPMatch(ip, blockedIPs)) {
91
- return false;
92
- }
93
-
94
- // Then check if IP is allowed
95
- return this.isGlobIPMatch(ip, allowedIPs);
123
+ return isIPAuthorized(ip, allowedIPs, blockedIPs);
96
124
  }
97
125
 
98
126
  /**
99
- * Check if the IP matches any of the glob patterns from security configuration
100
- *
101
- * This method checks IP addresses against glob patterns and handles IPv4/IPv6 normalization.
102
- * It's used to implement IP filtering based on the route.security configuration.
103
- *
104
- * @param ip - The IP address to check
105
- * @param patterns - Array of glob patterns from security.ipAllowList or ipBlockList
106
- * @returns true if IP matches any pattern, false otherwise
127
+ * Check if IP should be allowed considering connection rate and max connections
128
+ * @returns Object with result and reason
107
129
  */
108
- private isGlobIPMatch(ip: string, patterns: string[]): boolean {
109
- if (!ip || !patterns || patterns.length === 0) return false;
110
-
111
- // Handle IPv4/IPv6 normalization for proper matching
112
- const normalizeIP = (ip: string): string[] => {
113
- if (!ip) return [];
114
- // Handle IPv4-mapped IPv6 addresses (::ffff:127.0.0.1)
115
- if (ip.startsWith('::ffff:')) {
116
- const ipv4 = ip.slice(7);
117
- return [ip, ipv4];
118
- }
119
- // Handle IPv4 addresses by also checking IPv4-mapped form
120
- if (/^\d{1,3}(\.\d{1,3}){3}$/.test(ip)) {
121
- return [ip, `::ffff:${ip}`];
122
- }
123
- return [ip];
124
- };
125
-
126
- // Normalize the IP being checked
127
- const normalizedIPVariants = normalizeIP(ip);
128
- if (normalizedIPVariants.length === 0) return false;
130
+ public validateIP(ip: string): { allowed: boolean; reason?: string } {
131
+ // Check connection count limit
132
+ if (
133
+ this.smartProxy.settings.maxConnectionsPerIP &&
134
+ this.getConnectionCountByIP(ip) >= this.smartProxy.settings.maxConnectionsPerIP
135
+ ) {
136
+ return {
137
+ allowed: false,
138
+ reason: `Maximum connections per IP (${this.smartProxy.settings.maxConnectionsPerIP}) exceeded`
139
+ };
140
+ }
129
141
 
130
- // Expand shorthand patterns and normalize IPs for consistent comparison
131
- const expandShorthand = (pattern: string): string => {
132
- // Expand shorthand IP patterns like '192.168.*' to '192.168.*.*'
133
- if (pattern.includes('*') && !pattern.includes(':')) {
134
- const parts = pattern.split('.');
135
- while (parts.length < 4) {
136
- parts.push('*');
137
- }
138
- return parts.join('.');
139
- }
140
- return pattern;
141
- };
142
-
143
- const expandedPatterns = patterns.map(expandShorthand).flatMap(normalizeIP);
142
+ // Check connection rate limit
143
+ if (
144
+ this.smartProxy.settings.connectionRateLimitPerMinute &&
145
+ !this.checkConnectionRate(ip)
146
+ ) {
147
+ return {
148
+ allowed: false,
149
+ reason: `Connection rate limit (${this.smartProxy.settings.connectionRateLimitPerMinute}/min) exceeded`
150
+ };
151
+ }
144
152
 
145
- // Check for any match between normalized IP variants and patterns
146
- return normalizedIPVariants.some((ipVariant) =>
147
- expandedPatterns.some((pattern) => plugins.minimatch(ipVariant, pattern))
148
- );
153
+ return { allowed: true };
149
154
  }
150
-
155
+
151
156
  /**
152
- * Check if IP should be allowed considering connection rate and max connections
153
- * @returns Object with result and reason
157
+ * Atomically validate an IP and track the connection if allowed.
158
+ * This prevents race conditions where concurrent connections could bypass per-IP limits.
159
+ *
160
+ * @param ip - The IP address to validate
161
+ * @param connectionId - The connection ID to track if validation passes
162
+ * @returns Object with validation result and reason
154
163
  */
155
- public validateIP(ip: string): { allowed: boolean; reason?: string } {
156
- // Check connection count limit
164
+ public validateAndTrackIP(ip: string, connectionId: string): { allowed: boolean; reason?: string } {
165
+ // Check connection count limit BEFORE tracking
157
166
  if (
158
167
  this.smartProxy.settings.maxConnectionsPerIP &&
159
168
  this.getConnectionCountByIP(ip) >= this.smartProxy.settings.maxConnectionsPerIP
@@ -166,7 +175,7 @@ export class SecurityManager {
166
175
 
167
176
  // Check connection rate limit
168
177
  if (
169
- this.smartProxy.settings.connectionRateLimitPerMinute &&
178
+ this.smartProxy.settings.connectionRateLimitPerMinute &&
170
179
  !this.checkConnectionRate(ip)
171
180
  ) {
172
181
  return {
@@ -174,7 +183,10 @@ export class SecurityManager {
174
183
  reason: `Connection rate limit (${this.smartProxy.settings.connectionRateLimitPerMinute}/min) exceeded`
175
184
  };
176
185
  }
177
-
186
+
187
+ // Validation passed - immediately track to prevent race conditions
188
+ this.trackConnectionByIP(ip, connectionId);
189
+
178
190
  return { allowed: true };
179
191
  }
180
192
 
@@ -137,8 +137,6 @@ export class SmartProxy extends plugins.EventEmitter {
137
137
  enableDetailedLogging: settingsArg.enableDetailedLogging || false,
138
138
  enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false,
139
139
  enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false,
140
- allowSessionTicket:
141
- settingsArg.allowSessionTicket !== undefined ? settingsArg.allowSessionTicket : true,
142
140
  maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100,
143
141
  connectionRateLimitPerMinute: settingsArg.connectionRateLimitPerMinute || 300,
144
142
  keepAliveTreatment: settingsArg.keepAliveTreatment || 'extended',
@@ -50,43 +50,7 @@ export class TlsManager {
50
50
  );
51
51
  }
52
52
 
53
- /**
54
- * Handle session resumption attempts
55
- */
56
- public handleSessionResumption(
57
- chunk: Buffer,
58
- connectionId: string,
59
- hasSNI: boolean
60
- ): { shouldBlock: boolean; reason?: string } {
61
- // Skip if session tickets are allowed
62
- if (this.smartProxy.settings.allowSessionTicket !== false) {
63
- return { shouldBlock: false };
64
- }
65
-
66
- // Check for session resumption attempt
67
- const resumptionInfo = SniHandler.hasSessionResumption(
68
- chunk,
69
- this.smartProxy.settings.enableTlsDebugLogging || false
70
- );
71
-
72
- // If this is a resumption attempt without SNI, block it
73
- if (resumptionInfo.isResumption && !hasSNI && !resumptionInfo.hasSNI) {
74
- if (this.smartProxy.settings.enableTlsDebugLogging) {
75
- console.log(
76
- `[${connectionId}] Session resumption detected without SNI and allowSessionTicket=false. ` +
77
- `Terminating connection to force new TLS handshake.`
78
- );
79
- }
80
- return {
81
- shouldBlock: true,
82
- reason: 'session_ticket_blocked'
83
- };
84
- }
85
-
86
- return { shouldBlock: false };
87
- }
88
-
89
- /**
53
+ /**
90
54
  * Check for SNI mismatch during renegotiation
91
55
  */
92
56
  public checkRenegotiationSNI(
@@ -8,8 +8,8 @@
8
8
  // Export route helpers for creating route configurations
9
9
  export * from './route-helpers.js';
10
10
 
11
- // Export route validators for validating route configurations
12
- export * from './route-validators.js';
11
+ // Export route validator (class-based and functional API)
12
+ export * from './route-validator.js';
13
13
 
14
14
  // Export route utilities for route operations
15
15
  export * from './route-utils.js';
@@ -20,6 +20,4 @@ export {
20
20
  addRateLimiting,
21
21
  addBasicAuth,
22
22
  addJwtAuth
23
- } from './route-helpers.js';
24
-
25
- // Migration utilities have been removed as they are no longer needed
23
+ } from './route-helpers.js';
@@ -0,0 +1,144 @@
1
+ /**
2
+ * API Route Helper Functions
3
+ *
4
+ * This module provides utility functions for creating API route configurations.
5
+ */
6
+
7
+ import type { IRouteConfig, IRouteMatch, IRouteAction } from '../../models/route-types.js';
8
+ import { mergeRouteConfigs } from '../route-utils.js';
9
+ import { createHttpRoute } from './http-helpers.js';
10
+ import { createHttpsTerminateRoute } from './https-helpers.js';
11
+
12
+ /**
13
+ * Create an API route configuration
14
+ * @param domains Domain(s) to match
15
+ * @param apiPath API base path (e.g., "/api")
16
+ * @param target Target host and port
17
+ * @param options Additional route options
18
+ * @returns Route configuration object
19
+ */
20
+ export function createApiRoute(
21
+ domains: string | string[],
22
+ apiPath: string,
23
+ target: { host: string | string[]; port: number },
24
+ options: {
25
+ useTls?: boolean;
26
+ certificate?: 'auto' | { key: string; cert: string };
27
+ addCorsHeaders?: boolean;
28
+ httpPort?: number | number[];
29
+ httpsPort?: number | number[];
30
+ name?: string;
31
+ [key: string]: any;
32
+ } = {}
33
+ ): IRouteConfig {
34
+ // Normalize API path
35
+ const normalizedPath = apiPath.startsWith('/') ? apiPath : `/${apiPath}`;
36
+ const pathWithWildcard = normalizedPath.endsWith('/')
37
+ ? `${normalizedPath}*`
38
+ : `${normalizedPath}/*`;
39
+
40
+ // Create route match
41
+ const match: IRouteMatch = {
42
+ ports: options.useTls
43
+ ? (options.httpsPort || 443)
44
+ : (options.httpPort || 80),
45
+ domains,
46
+ path: pathWithWildcard
47
+ };
48
+
49
+ // Create route action
50
+ const action: IRouteAction = {
51
+ type: 'forward',
52
+ targets: [target]
53
+ };
54
+
55
+ // Add TLS configuration if using HTTPS
56
+ if (options.useTls) {
57
+ action.tls = {
58
+ mode: 'terminate',
59
+ certificate: options.certificate || 'auto'
60
+ };
61
+ }
62
+
63
+ // Add CORS headers if requested
64
+ const headers: Record<string, Record<string, string>> = {};
65
+ if (options.addCorsHeaders) {
66
+ headers.response = {
67
+ 'Access-Control-Allow-Origin': '*',
68
+ 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
69
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
70
+ 'Access-Control-Max-Age': '86400'
71
+ };
72
+ }
73
+
74
+ // Create the route config
75
+ return {
76
+ match,
77
+ action,
78
+ headers: Object.keys(headers).length > 0 ? headers : undefined,
79
+ name: options.name || `API Route ${normalizedPath} for ${Array.isArray(domains) ? domains.join(', ') : domains}`,
80
+ priority: options.priority || 100, // Higher priority for specific path matches
81
+ ...options
82
+ };
83
+ }
84
+
85
+ /**
86
+ * Create an API Gateway route pattern
87
+ * @param domains Domain(s) to match
88
+ * @param apiBasePath Base path for API endpoints (e.g., '/api')
89
+ * @param target Target host and port
90
+ * @param options Additional route options
91
+ * @returns API route configuration
92
+ */
93
+ export function createApiGatewayRoute(
94
+ domains: string | string[],
95
+ apiBasePath: string,
96
+ target: { host: string | string[]; port: number },
97
+ options: {
98
+ useTls?: boolean;
99
+ certificate?: 'auto' | { key: string; cert: string };
100
+ addCorsHeaders?: boolean;
101
+ [key: string]: any;
102
+ } = {}
103
+ ): IRouteConfig {
104
+ // Normalize apiBasePath to ensure it starts with / and doesn't end with /
105
+ const normalizedPath = apiBasePath.startsWith('/')
106
+ ? apiBasePath
107
+ : `/${apiBasePath}`;
108
+
109
+ // Add wildcard to path to match all API endpoints
110
+ const apiPath = normalizedPath.endsWith('/')
111
+ ? `${normalizedPath}*`
112
+ : `${normalizedPath}/*`;
113
+
114
+ // Create base route
115
+ const baseRoute = options.useTls
116
+ ? createHttpsTerminateRoute(domains, target, {
117
+ certificate: options.certificate || 'auto'
118
+ })
119
+ : createHttpRoute(domains, target);
120
+
121
+ // Add API-specific configurations
122
+ const apiRoute: Partial<IRouteConfig> = {
123
+ match: {
124
+ ...baseRoute.match,
125
+ path: apiPath
126
+ },
127
+ name: options.name || `API Gateway: ${apiPath} -> ${Array.isArray(target.host) ? target.host.join(', ') : target.host}:${target.port}`,
128
+ priority: options.priority || 100 // Higher priority for specific path matching
129
+ };
130
+
131
+ // Add CORS headers if requested
132
+ if (options.addCorsHeaders) {
133
+ apiRoute.headers = {
134
+ response: {
135
+ 'Access-Control-Allow-Origin': '*',
136
+ 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
137
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
138
+ 'Access-Control-Max-Age': '86400'
139
+ }
140
+ };
141
+ }
142
+
143
+ return mergeRouteConfigs(baseRoute, apiRoute);
144
+ }