@push.rocks/smartproxy 21.1.6 → 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.
- package/changelog.md +89 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/core/utils/shared-security-manager.d.ts +17 -0
- package/dist_ts/core/utils/shared-security-manager.js +66 -1
- package/dist_ts/proxies/http-proxy/default-certificates.d.ts +54 -0
- package/dist_ts/proxies/http-proxy/default-certificates.js +127 -0
- package/dist_ts/proxies/http-proxy/http-proxy.d.ts +1 -1
- package/dist_ts/proxies/http-proxy/http-proxy.js +9 -14
- package/dist_ts/proxies/http-proxy/index.d.ts +5 -1
- package/dist_ts/proxies/http-proxy/index.js +6 -2
- package/dist_ts/proxies/http-proxy/security-manager.d.ts +4 -12
- package/dist_ts/proxies/http-proxy/security-manager.js +66 -99
- package/dist_ts/proxies/nftables-proxy/index.d.ts +1 -0
- package/dist_ts/proxies/nftables-proxy/index.js +2 -1
- package/dist_ts/proxies/nftables-proxy/nftables-proxy.d.ts +4 -26
- package/dist_ts/proxies/nftables-proxy/nftables-proxy.js +84 -236
- package/dist_ts/proxies/nftables-proxy/utils/index.d.ts +9 -0
- package/dist_ts/proxies/nftables-proxy/utils/index.js +12 -0
- package/dist_ts/proxies/nftables-proxy/utils/nft-command-executor.d.ts +66 -0
- package/dist_ts/proxies/nftables-proxy/utils/nft-command-executor.js +131 -0
- package/dist_ts/proxies/nftables-proxy/utils/nft-port-spec-normalizer.d.ts +39 -0
- package/dist_ts/proxies/nftables-proxy/utils/nft-port-spec-normalizer.js +112 -0
- package/dist_ts/proxies/nftables-proxy/utils/nft-rule-validator.d.ts +59 -0
- package/dist_ts/proxies/nftables-proxy/utils/nft-rule-validator.js +130 -0
- package/dist_ts/proxies/smart-proxy/certificate-manager.js +4 -3
- package/dist_ts/proxies/smart-proxy/connection-manager.d.ts +13 -2
- package/dist_ts/proxies/smart-proxy/connection-manager.js +16 -6
- package/dist_ts/proxies/smart-proxy/http-proxy-bridge.js +35 -10
- package/dist_ts/proxies/smart-proxy/models/interfaces.d.ts +0 -1
- package/dist_ts/proxies/smart-proxy/route-connection-handler.d.ts +17 -0
- package/dist_ts/proxies/smart-proxy/route-connection-handler.js +72 -9
- package/dist_ts/proxies/smart-proxy/security-manager.d.ts +14 -12
- package/dist_ts/proxies/smart-proxy/security-manager.js +80 -74
- package/dist_ts/proxies/smart-proxy/smart-proxy.js +1 -2
- package/dist_ts/proxies/smart-proxy/tls-manager.d.ts +2 -9
- package/dist_ts/proxies/smart-proxy/tls-manager.js +3 -26
- package/dist_ts/proxies/smart-proxy/utils/index.d.ts +1 -1
- package/dist_ts/proxies/smart-proxy/utils/index.js +3 -4
- package/dist_ts/proxies/smart-proxy/utils/route-helpers/api-helpers.d.ts +49 -0
- package/dist_ts/proxies/smart-proxy/utils/route-helpers/api-helpers.js +108 -0
- package/dist_ts/proxies/smart-proxy/utils/route-helpers/dynamic-helpers.d.ts +57 -0
- package/dist_ts/proxies/smart-proxy/utils/route-helpers/dynamic-helpers.js +89 -0
- package/dist_ts/proxies/smart-proxy/utils/route-helpers/http-helpers.d.ts +17 -0
- package/dist_ts/proxies/smart-proxy/utils/route-helpers/http-helpers.js +32 -0
- package/dist_ts/proxies/smart-proxy/utils/route-helpers/https-helpers.d.ts +68 -0
- package/dist_ts/proxies/smart-proxy/utils/route-helpers/https-helpers.js +117 -0
- package/dist_ts/proxies/smart-proxy/utils/route-helpers/index.d.ts +17 -0
- package/dist_ts/proxies/smart-proxy/utils/route-helpers/index.js +27 -0
- package/dist_ts/proxies/smart-proxy/utils/route-helpers/load-balancer-helpers.d.ts +63 -0
- package/dist_ts/proxies/smart-proxy/utils/route-helpers/load-balancer-helpers.js +105 -0
- package/dist_ts/proxies/smart-proxy/utils/route-helpers/nftables-helpers.d.ts +83 -0
- package/dist_ts/proxies/smart-proxy/utils/route-helpers/nftables-helpers.js +126 -0
- package/dist_ts/proxies/smart-proxy/utils/route-helpers/security-helpers.d.ts +47 -0
- package/dist_ts/proxies/smart-proxy/utils/route-helpers/security-helpers.js +66 -0
- package/dist_ts/proxies/smart-proxy/utils/route-helpers/socket-handlers.d.ts +70 -0
- package/dist_ts/proxies/smart-proxy/utils/route-helpers/socket-handlers.js +287 -0
- package/dist_ts/proxies/smart-proxy/utils/route-helpers/websocket-helpers.d.ts +46 -0
- package/dist_ts/proxies/smart-proxy/utils/route-helpers/websocket-helpers.js +67 -0
- package/dist_ts/proxies/smart-proxy/utils/route-helpers.d.ts +4 -457
- package/dist_ts/proxies/smart-proxy/utils/route-helpers.js +6 -950
- package/dist_ts/proxies/smart-proxy/utils/route-utils.js +2 -2
- package/dist_ts/proxies/smart-proxy/utils/route-validator.d.ts +67 -1
- package/dist_ts/proxies/smart-proxy/utils/route-validator.js +266 -6
- package/npmextra.json +12 -6
- package/package.json +34 -24
- package/readme.hints.md +184 -1
- package/readme.md +235 -172
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/core/utils/shared-security-manager.ts +98 -13
- package/ts/proxies/http-proxy/default-certificates.ts +150 -0
- package/ts/proxies/http-proxy/http-proxy.ts +9 -15
- package/ts/proxies/http-proxy/index.ts +6 -1
- package/ts/proxies/http-proxy/security-manager.ts +141 -161
- package/ts/proxies/nftables-proxy/index.ts +1 -0
- package/ts/proxies/nftables-proxy/nftables-proxy.ts +116 -290
- package/ts/proxies/nftables-proxy/utils/index.ts +38 -0
- package/ts/proxies/nftables-proxy/utils/nft-command-executor.ts +162 -0
- package/ts/proxies/nftables-proxy/utils/nft-port-spec-normalizer.ts +125 -0
- package/ts/proxies/nftables-proxy/utils/nft-rule-validator.ts +156 -0
- package/ts/proxies/smart-proxy/certificate-manager.ts +3 -2
- package/ts/proxies/smart-proxy/connection-manager.ts +21 -8
- package/ts/proxies/smart-proxy/http-proxy-bridge.ts +39 -13
- package/ts/proxies/smart-proxy/models/interfaces.ts +0 -1
- package/ts/proxies/smart-proxy/route-connection-handler.ts +88 -16
- package/ts/proxies/smart-proxy/security-manager.ts +98 -86
- package/ts/proxies/smart-proxy/smart-proxy.ts +0 -2
- package/ts/proxies/smart-proxy/tls-manager.ts +1 -37
- package/ts/proxies/smart-proxy/utils/index.ts +3 -5
- package/ts/proxies/smart-proxy/utils/route-helpers/api-helpers.ts +144 -0
- package/ts/proxies/smart-proxy/utils/route-helpers/dynamic-helpers.ts +124 -0
- package/ts/proxies/smart-proxy/utils/route-helpers/http-helpers.ts +40 -0
- package/ts/proxies/smart-proxy/utils/route-helpers/https-helpers.ts +163 -0
- package/ts/proxies/smart-proxy/utils/route-helpers/index.ts +62 -0
- package/ts/proxies/smart-proxy/utils/route-helpers/load-balancer-helpers.ts +154 -0
- package/ts/proxies/smart-proxy/utils/route-helpers/nftables-helpers.ts +202 -0
- package/ts/proxies/smart-proxy/utils/route-helpers/security-helpers.ts +96 -0
- package/ts/proxies/smart-proxy/utils/route-helpers/socket-handlers.ts +337 -0
- package/ts/proxies/smart-proxy/utils/route-helpers/websocket-helpers.ts +98 -0
- package/ts/proxies/smart-proxy/utils/route-helpers.ts +5 -1302
- package/ts/proxies/smart-proxy/utils/route-utils.ts +1 -1
- package/ts/proxies/smart-proxy/utils/route-validator.ts +289 -7
- package/ts/proxies/http-proxy/certificate-manager.ts +0 -244
- 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
|
-
//
|
|
91
|
-
|
|
92
|
-
const
|
|
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 ${
|
|
98
|
-
{ remoteIP:
|
|
99
|
-
|
|
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
|
-
|
|
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 -
|
|
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
|
-
|
|
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 &&
|
|
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
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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(
|
|
61
|
+
const timestamps = this.connectionRateByIP.get(key)!.filter((time) => now - time < minute);
|
|
41
62
|
timestamps.push(now);
|
|
42
|
-
this.connectionRateByIP.set(
|
|
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
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
//
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
153
|
-
*
|
|
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
|
|
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
|
|
12
|
-
export * from './route-
|
|
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
|
+
}
|