@push.rocks/smartproxy 19.6.13 → 19.6.15
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/dist_ts/core/utils/log-deduplicator.d.ts +39 -0
- package/dist_ts/core/utils/log-deduplicator.js +297 -0
- package/dist_ts/core/utils/shared-security-manager.d.ts +2 -1
- package/dist_ts/core/utils/shared-security-manager.js +22 -2
- package/dist_ts/proxies/http-proxy/http-proxy.d.ts +1 -0
- package/dist_ts/proxies/http-proxy/http-proxy.js +94 -9
- package/dist_ts/proxies/http-proxy/models/types.d.ts +2 -0
- package/dist_ts/proxies/http-proxy/models/types.js +1 -1
- package/dist_ts/proxies/http-proxy/security-manager.d.ts +42 -1
- package/dist_ts/proxies/http-proxy/security-manager.js +121 -2
- package/dist_ts/proxies/smart-proxy/connection-manager.d.ts +14 -0
- package/dist_ts/proxies/smart-proxy/connection-manager.js +86 -32
- package/dist_ts/proxies/smart-proxy/http-proxy-bridge.js +5 -1
- package/dist_ts/proxies/smart-proxy/models/interfaces.d.ts +1 -0
- package/dist_ts/proxies/smart-proxy/route-connection-handler.js +25 -9
- package/dist_ts/proxies/smart-proxy/security-manager.d.ts +9 -0
- package/dist_ts/proxies/smart-proxy/security-manager.js +63 -1
- package/dist_ts/proxies/smart-proxy/smart-proxy.js +4 -1
- package/package.json +1 -1
- package/readme.hints.md +113 -1
- package/readme.plan.md +34 -353
- package/ts/core/utils/log-deduplicator.ts +361 -0
- package/ts/core/utils/shared-security-manager.ts +24 -1
- package/ts/proxies/http-proxy/http-proxy.ts +129 -9
- package/ts/proxies/http-proxy/models/types.ts +4 -0
- package/ts/proxies/http-proxy/security-manager.ts +136 -1
- package/ts/proxies/smart-proxy/connection-manager.ts +113 -36
- package/ts/proxies/smart-proxy/http-proxy-bridge.ts +5 -0
- package/ts/proxies/smart-proxy/models/interfaces.ts +1 -0
- package/ts/proxies/smart-proxy/route-connection-handler.ts +52 -15
- package/ts/proxies/smart-proxy/security-manager.ts +76 -1
- package/ts/proxies/smart-proxy/smart-proxy.ts +4 -0
|
@@ -14,7 +14,14 @@ export class SecurityManager {
|
|
|
14
14
|
// Store rate limits per route and key
|
|
15
15
|
private rateLimits: Map<string, Map<string, { count: number, expiry: number }>> = new Map();
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
// Connection tracking by IP
|
|
18
|
+
private connectionsByIP: Map<string, Set<string>> = new Map();
|
|
19
|
+
private connectionRateByIP: Map<string, number[]> = new Map();
|
|
20
|
+
|
|
21
|
+
constructor(private logger: ILogger, private routes: IRouteConfig[] = [], private maxConnectionsPerIP: number = 100, private connectionRateLimitPerMinute: number = 300) {
|
|
22
|
+
// Start periodic cleanup for connection tracking
|
|
23
|
+
this.startPeriodicIpCleanup();
|
|
24
|
+
}
|
|
18
25
|
|
|
19
26
|
/**
|
|
20
27
|
* Update the routes configuration
|
|
@@ -295,4 +302,132 @@ export class SecurityManager {
|
|
|
295
302
|
return false;
|
|
296
303
|
}
|
|
297
304
|
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Get connections count by IP
|
|
308
|
+
*/
|
|
309
|
+
public getConnectionCountByIP(ip: string): number {
|
|
310
|
+
return this.connectionsByIP.get(ip)?.size || 0;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Check and update connection rate for an IP
|
|
315
|
+
* @returns true if within rate limit, false if exceeding limit
|
|
316
|
+
*/
|
|
317
|
+
public checkConnectionRate(ip: string): boolean {
|
|
318
|
+
const now = Date.now();
|
|
319
|
+
const minute = 60 * 1000;
|
|
320
|
+
|
|
321
|
+
if (!this.connectionRateByIP.has(ip)) {
|
|
322
|
+
this.connectionRateByIP.set(ip, [now]);
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Get timestamps and filter out entries older than 1 minute
|
|
327
|
+
const timestamps = this.connectionRateByIP.get(ip)!.filter((time) => now - time < minute);
|
|
328
|
+
timestamps.push(now);
|
|
329
|
+
this.connectionRateByIP.set(ip, timestamps);
|
|
330
|
+
|
|
331
|
+
// Check if rate exceeds limit
|
|
332
|
+
return timestamps.length <= this.connectionRateLimitPerMinute;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Track connection by IP
|
|
337
|
+
*/
|
|
338
|
+
public trackConnectionByIP(ip: string, connectionId: string): void {
|
|
339
|
+
if (!this.connectionsByIP.has(ip)) {
|
|
340
|
+
this.connectionsByIP.set(ip, new Set());
|
|
341
|
+
}
|
|
342
|
+
this.connectionsByIP.get(ip)!.add(connectionId);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Remove connection tracking for an IP
|
|
347
|
+
*/
|
|
348
|
+
public removeConnectionByIP(ip: string, connectionId: string): void {
|
|
349
|
+
if (this.connectionsByIP.has(ip)) {
|
|
350
|
+
const connections = this.connectionsByIP.get(ip)!;
|
|
351
|
+
connections.delete(connectionId);
|
|
352
|
+
if (connections.size === 0) {
|
|
353
|
+
this.connectionsByIP.delete(ip);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Check if IP should be allowed considering connection rate and max connections
|
|
360
|
+
* @returns Object with result and reason
|
|
361
|
+
*/
|
|
362
|
+
public validateIP(ip: string): { allowed: boolean; reason?: string } {
|
|
363
|
+
// Check connection count limit
|
|
364
|
+
if (this.getConnectionCountByIP(ip) >= this.maxConnectionsPerIP) {
|
|
365
|
+
return {
|
|
366
|
+
allowed: false,
|
|
367
|
+
reason: `Maximum connections per IP (${this.maxConnectionsPerIP}) exceeded`
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Check connection rate limit
|
|
372
|
+
if (!this.checkConnectionRate(ip)) {
|
|
373
|
+
return {
|
|
374
|
+
allowed: false,
|
|
375
|
+
reason: `Connection rate limit (${this.connectionRateLimitPerMinute}/min) exceeded`
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return { allowed: true };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Clears all IP tracking data (for shutdown)
|
|
384
|
+
*/
|
|
385
|
+
public clearIPTracking(): void {
|
|
386
|
+
this.connectionsByIP.clear();
|
|
387
|
+
this.connectionRateByIP.clear();
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Start periodic cleanup of IP tracking data
|
|
392
|
+
*/
|
|
393
|
+
private startPeriodicIpCleanup(): void {
|
|
394
|
+
// Clean up IP tracking data every minute
|
|
395
|
+
setInterval(() => {
|
|
396
|
+
this.performIpCleanup();
|
|
397
|
+
}, 60000).unref();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Perform cleanup of expired IP data
|
|
402
|
+
*/
|
|
403
|
+
private performIpCleanup(): void {
|
|
404
|
+
const now = Date.now();
|
|
405
|
+
const minute = 60 * 1000;
|
|
406
|
+
let cleanedRateLimits = 0;
|
|
407
|
+
let cleanedIPs = 0;
|
|
408
|
+
|
|
409
|
+
// Clean up expired rate limit timestamps
|
|
410
|
+
for (const [ip, timestamps] of this.connectionRateByIP.entries()) {
|
|
411
|
+
const validTimestamps = timestamps.filter(time => now - time < minute);
|
|
412
|
+
|
|
413
|
+
if (validTimestamps.length === 0) {
|
|
414
|
+
this.connectionRateByIP.delete(ip);
|
|
415
|
+
cleanedRateLimits++;
|
|
416
|
+
} else if (validTimestamps.length < timestamps.length) {
|
|
417
|
+
this.connectionRateByIP.set(ip, validTimestamps);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Clean up IPs with no active connections
|
|
422
|
+
for (const [ip, connections] of this.connectionsByIP.entries()) {
|
|
423
|
+
if (connections.size === 0) {
|
|
424
|
+
this.connectionsByIP.delete(ip);
|
|
425
|
+
cleanedIPs++;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (cleanedRateLimits > 0 || cleanedIPs > 0) {
|
|
430
|
+
this.logger.debug(`IP cleanup: removed ${cleanedIPs} IPs and ${cleanedRateLimits} rate limits`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
298
433
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as plugins from '../../plugins.js';
|
|
2
2
|
import type { IConnectionRecord } from './models/interfaces.js';
|
|
3
3
|
import { logger } from '../../core/utils/logger.js';
|
|
4
|
+
import { connectionLogDeduplicator } from '../../core/utils/log-deduplicator.js';
|
|
4
5
|
import { LifecycleComponent } from '../../core/utils/lifecycle-component.js';
|
|
5
6
|
import { cleanupSocket } from '../../core/utils/socket-utils.js';
|
|
6
7
|
import { WrappedSocket } from '../../core/models/wrapped-socket.js';
|
|
@@ -26,6 +27,10 @@ export class ConnectionManager extends LifecycleComponent {
|
|
|
26
27
|
// Cleanup queue for batched processing
|
|
27
28
|
private cleanupQueue: Set<string> = new Set();
|
|
28
29
|
private cleanupTimer: NodeJS.Timeout | null = null;
|
|
30
|
+
private isProcessingCleanup: boolean = false;
|
|
31
|
+
|
|
32
|
+
// Route-level connection tracking
|
|
33
|
+
private connectionsByRoute: Map<string, Set<string>> = new Map();
|
|
29
34
|
|
|
30
35
|
constructor(
|
|
31
36
|
private smartProxy: SmartProxy
|
|
@@ -56,11 +61,19 @@ export class ConnectionManager extends LifecycleComponent {
|
|
|
56
61
|
public createConnection(socket: plugins.net.Socket | WrappedSocket): IConnectionRecord | null {
|
|
57
62
|
// Enforce connection limit
|
|
58
63
|
if (this.connectionRecords.size >= this.maxConnections) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
+
// Use deduplicated logging for connection limit
|
|
65
|
+
connectionLogDeduplicator.log(
|
|
66
|
+
'connection-rejected',
|
|
67
|
+
'warn',
|
|
68
|
+
'Global connection limit reached',
|
|
69
|
+
{
|
|
70
|
+
reason: 'global-limit',
|
|
71
|
+
currentConnections: this.connectionRecords.size,
|
|
72
|
+
maxConnections: this.maxConnections,
|
|
73
|
+
component: 'connection-manager'
|
|
74
|
+
},
|
|
75
|
+
'global-limit'
|
|
76
|
+
);
|
|
64
77
|
socket.destroy();
|
|
65
78
|
return null;
|
|
66
79
|
}
|
|
@@ -165,18 +178,53 @@ export class ConnectionManager extends LifecycleComponent {
|
|
|
165
178
|
return this.connectionRecords.size;
|
|
166
179
|
}
|
|
167
180
|
|
|
181
|
+
/**
|
|
182
|
+
* Track connection by route
|
|
183
|
+
*/
|
|
184
|
+
public trackConnectionByRoute(routeId: string, connectionId: string): void {
|
|
185
|
+
if (!this.connectionsByRoute.has(routeId)) {
|
|
186
|
+
this.connectionsByRoute.set(routeId, new Set());
|
|
187
|
+
}
|
|
188
|
+
this.connectionsByRoute.get(routeId)!.add(connectionId);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Remove connection tracking for a route
|
|
193
|
+
*/
|
|
194
|
+
public removeConnectionByRoute(routeId: string, connectionId: string): void {
|
|
195
|
+
if (this.connectionsByRoute.has(routeId)) {
|
|
196
|
+
const connections = this.connectionsByRoute.get(routeId)!;
|
|
197
|
+
connections.delete(connectionId);
|
|
198
|
+
if (connections.size === 0) {
|
|
199
|
+
this.connectionsByRoute.delete(routeId);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Get connection count by route
|
|
206
|
+
*/
|
|
207
|
+
public getConnectionCountByRoute(routeId: string): number {
|
|
208
|
+
return this.connectionsByRoute.get(routeId)?.size || 0;
|
|
209
|
+
}
|
|
210
|
+
|
|
168
211
|
/**
|
|
169
212
|
* Initiates cleanup once for a connection
|
|
170
213
|
*/
|
|
171
214
|
public initiateCleanupOnce(record: IConnectionRecord, reason: string = 'normal'): void {
|
|
172
|
-
|
|
173
|
-
|
|
215
|
+
// Use deduplicated logging for cleanup events
|
|
216
|
+
connectionLogDeduplicator.log(
|
|
217
|
+
'connection-cleanup',
|
|
218
|
+
'info',
|
|
219
|
+
`Connection cleanup: ${reason}`,
|
|
220
|
+
{
|
|
174
221
|
connectionId: record.id,
|
|
175
222
|
remoteIP: record.remoteIP,
|
|
176
223
|
reason,
|
|
177
224
|
component: 'connection-manager'
|
|
178
|
-
}
|
|
179
|
-
|
|
225
|
+
},
|
|
226
|
+
reason
|
|
227
|
+
);
|
|
180
228
|
|
|
181
229
|
if (record.incomingTerminationReason == null) {
|
|
182
230
|
record.incomingTerminationReason = reason;
|
|
@@ -200,10 +248,10 @@ export class ConnectionManager extends LifecycleComponent {
|
|
|
200
248
|
|
|
201
249
|
this.cleanupQueue.add(connectionId);
|
|
202
250
|
|
|
203
|
-
// Process immediately if queue is getting large
|
|
204
|
-
if (this.cleanupQueue.size >= this.cleanupBatchSize) {
|
|
251
|
+
// Process immediately if queue is getting large and not already processing
|
|
252
|
+
if (this.cleanupQueue.size >= this.cleanupBatchSize && !this.isProcessingCleanup) {
|
|
205
253
|
this.processCleanupQueue();
|
|
206
|
-
} else if (!this.cleanupTimer) {
|
|
254
|
+
} else if (!this.cleanupTimer && !this.isProcessingCleanup) {
|
|
207
255
|
// Otherwise, schedule batch processing
|
|
208
256
|
this.cleanupTimer = this.setTimeout(() => {
|
|
209
257
|
this.processCleanupQueue();
|
|
@@ -215,27 +263,40 @@ export class ConnectionManager extends LifecycleComponent {
|
|
|
215
263
|
* Process the cleanup queue in batches
|
|
216
264
|
*/
|
|
217
265
|
private processCleanupQueue(): void {
|
|
266
|
+
// Prevent concurrent processing
|
|
267
|
+
if (this.isProcessingCleanup) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
this.isProcessingCleanup = true;
|
|
272
|
+
|
|
218
273
|
if (this.cleanupTimer) {
|
|
219
274
|
this.clearTimeout(this.cleanupTimer);
|
|
220
275
|
this.cleanupTimer = null;
|
|
221
276
|
}
|
|
222
277
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
278
|
+
try {
|
|
279
|
+
// Take a snapshot of items to process
|
|
280
|
+
const toCleanup = Array.from(this.cleanupQueue).slice(0, this.cleanupBatchSize);
|
|
281
|
+
|
|
282
|
+
// Remove only the items we're processing from the queue
|
|
283
|
+
for (const connectionId of toCleanup) {
|
|
284
|
+
this.cleanupQueue.delete(connectionId);
|
|
285
|
+
const record = this.connectionRecords.get(connectionId);
|
|
286
|
+
if (record) {
|
|
287
|
+
this.cleanupConnection(record, record.incomingTerminationReason || 'normal');
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
} finally {
|
|
291
|
+
// Always reset the processing flag
|
|
292
|
+
this.isProcessingCleanup = false;
|
|
293
|
+
|
|
294
|
+
// Check if more items were added while we were processing
|
|
295
|
+
if (this.cleanupQueue.size > 0) {
|
|
296
|
+
this.cleanupTimer = this.setTimeout(() => {
|
|
297
|
+
this.processCleanupQueue();
|
|
298
|
+
}, 10);
|
|
231
299
|
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// If there are more in queue, schedule next batch
|
|
235
|
-
if (this.cleanupQueue.size > 0) {
|
|
236
|
-
this.cleanupTimer = this.setTimeout(() => {
|
|
237
|
-
this.processCleanupQueue();
|
|
238
|
-
}, 10);
|
|
239
300
|
}
|
|
240
301
|
}
|
|
241
302
|
|
|
@@ -252,6 +313,11 @@ export class ConnectionManager extends LifecycleComponent {
|
|
|
252
313
|
// Track connection termination
|
|
253
314
|
this.smartProxy.securityManager.removeConnectionByIP(record.remoteIP, record.id);
|
|
254
315
|
|
|
316
|
+
// Remove from route tracking
|
|
317
|
+
if (record.routeId) {
|
|
318
|
+
this.removeConnectionByRoute(record.routeId, record.id);
|
|
319
|
+
}
|
|
320
|
+
|
|
255
321
|
// Remove from metrics tracking
|
|
256
322
|
if (this.smartProxy.metricsCollector) {
|
|
257
323
|
this.smartProxy.metricsCollector.removeConnection(record.id);
|
|
@@ -335,23 +401,34 @@ export class ConnectionManager extends LifecycleComponent {
|
|
|
335
401
|
// Remove the record from the tracking map
|
|
336
402
|
this.connectionRecords.delete(record.id);
|
|
337
403
|
|
|
338
|
-
//
|
|
404
|
+
// Use deduplicated logging for connection termination
|
|
339
405
|
if (this.smartProxy.settings.enableDetailedLogging) {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
406
|
+
// For detailed logging, include more info but still deduplicate by IP+reason
|
|
407
|
+
connectionLogDeduplicator.log(
|
|
408
|
+
'connection-terminated',
|
|
409
|
+
'info',
|
|
410
|
+
`Connection terminated: ${record.remoteIP}:${record.localPort}`,
|
|
411
|
+
{
|
|
412
|
+
...logData,
|
|
413
|
+
duration_ms: duration,
|
|
414
|
+
bytesIn: record.bytesReceived,
|
|
415
|
+
bytesOut: record.bytesSent
|
|
416
|
+
},
|
|
417
|
+
`${record.remoteIP}-${reason}`
|
|
344
418
|
);
|
|
345
419
|
} else {
|
|
346
|
-
|
|
347
|
-
|
|
420
|
+
// For normal logging, deduplicate by termination reason
|
|
421
|
+
connectionLogDeduplicator.log(
|
|
422
|
+
'connection-terminated',
|
|
423
|
+
'info',
|
|
424
|
+
`Connection terminated`,
|
|
348
425
|
{
|
|
349
|
-
connectionId: record.id,
|
|
350
426
|
remoteIP: record.remoteIP,
|
|
351
427
|
reason,
|
|
352
428
|
activeConnections: this.connectionRecords.size,
|
|
353
429
|
component: 'connection-manager'
|
|
354
|
-
}
|
|
430
|
+
},
|
|
431
|
+
reason // Group by termination reason
|
|
355
432
|
);
|
|
356
433
|
}
|
|
357
434
|
}
|
|
@@ -121,6 +121,11 @@ export class HttpProxyBridge {
|
|
|
121
121
|
proxySocket.on('error', reject);
|
|
122
122
|
});
|
|
123
123
|
|
|
124
|
+
// Send client IP information header first (custom protocol)
|
|
125
|
+
// Format: "CLIENT_IP:<ip>\r\n"
|
|
126
|
+
const clientIPHeader = Buffer.from(`CLIENT_IP:${record.remoteIP}\r\n`);
|
|
127
|
+
proxySocket.write(clientIPHeader);
|
|
128
|
+
|
|
124
129
|
// Send initial chunk if present
|
|
125
130
|
if (initialChunk) {
|
|
126
131
|
// Count the initial chunk bytes
|
|
@@ -165,6 +165,7 @@ export interface IConnectionRecord {
|
|
|
165
165
|
tlsHandshakeComplete: boolean; // Whether the TLS handshake is complete
|
|
166
166
|
hasReceivedInitialData: boolean; // Whether initial data has been received
|
|
167
167
|
routeConfig?: IRouteConfig; // Associated route config for this connection
|
|
168
|
+
routeId?: string; // ID of the route this connection is associated with
|
|
168
169
|
|
|
169
170
|
// Target information (for dynamic port/host mapping)
|
|
170
171
|
targetHost?: string; // Resolved target host
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as plugins from '../../plugins.js';
|
|
2
2
|
import type { IConnectionRecord, ISmartProxyOptions } from './models/interfaces.js';
|
|
3
3
|
import { logger } from '../../core/utils/logger.js';
|
|
4
|
+
import { connectionLogDeduplicator } from '../../core/utils/log-deduplicator.js';
|
|
4
5
|
// Route checking functions have been removed
|
|
5
6
|
import type { IRouteConfig, IRouteAction } from './models/route-types.js';
|
|
6
7
|
import type { IRouteContext } from '../../core/models/route-context.js';
|
|
@@ -89,7 +90,13 @@ export class RouteConnectionHandler {
|
|
|
89
90
|
// Note: For wrapped sockets, this will use the underlying socket IP until PROXY protocol is parsed
|
|
90
91
|
const ipValidation = this.smartProxy.securityManager.validateIP(wrappedSocket.remoteAddress || '');
|
|
91
92
|
if (!ipValidation.allowed) {
|
|
92
|
-
|
|
93
|
+
connectionLogDeduplicator.log(
|
|
94
|
+
'ip-rejected',
|
|
95
|
+
'warn',
|
|
96
|
+
`Connection rejected from ${wrappedSocket.remoteAddress}`,
|
|
97
|
+
{ remoteIP: wrappedSocket.remoteAddress, reason: ipValidation.reason, component: 'route-handler' },
|
|
98
|
+
wrappedSocket.remoteAddress
|
|
99
|
+
);
|
|
93
100
|
cleanupSocket(wrappedSocket.socket, `rejected-${ipValidation.reason}`, { immediate: true });
|
|
94
101
|
return;
|
|
95
102
|
}
|
|
@@ -563,12 +570,20 @@ export class RouteConnectionHandler {
|
|
|
563
570
|
);
|
|
564
571
|
|
|
565
572
|
if (!isIPAllowed) {
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
573
|
+
// Deduplicated logging for route IP blocks
|
|
574
|
+
connectionLogDeduplicator.log(
|
|
575
|
+
'ip-rejected',
|
|
576
|
+
'warn',
|
|
577
|
+
`IP blocked by route security`,
|
|
578
|
+
{
|
|
579
|
+
connectionId,
|
|
580
|
+
remoteIP,
|
|
581
|
+
routeName: route.name || 'unnamed',
|
|
582
|
+
reason: 'route-ip-blocked',
|
|
583
|
+
component: 'route-handler'
|
|
584
|
+
},
|
|
585
|
+
remoteIP
|
|
586
|
+
);
|
|
572
587
|
socket.end();
|
|
573
588
|
this.smartProxy.connectionManager.cleanupConnection(record, 'route_ip_blocked');
|
|
574
589
|
return;
|
|
@@ -577,14 +592,28 @@ export class RouteConnectionHandler {
|
|
|
577
592
|
|
|
578
593
|
// Check max connections per route
|
|
579
594
|
if (route.security.maxConnections !== undefined) {
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
595
|
+
const routeId = route.id || route.name || 'unnamed';
|
|
596
|
+
const currentConnections = this.smartProxy.connectionManager.getConnectionCountByRoute(routeId);
|
|
597
|
+
|
|
598
|
+
if (currentConnections >= route.security.maxConnections) {
|
|
599
|
+
// Deduplicated logging for route connection limits
|
|
600
|
+
connectionLogDeduplicator.log(
|
|
601
|
+
'connection-rejected',
|
|
602
|
+
'warn',
|
|
603
|
+
`Route connection limit reached`,
|
|
604
|
+
{
|
|
605
|
+
connectionId,
|
|
606
|
+
routeName: route.name,
|
|
607
|
+
currentConnections,
|
|
608
|
+
maxConnections: route.security.maxConnections,
|
|
609
|
+
reason: 'route-limit',
|
|
610
|
+
component: 'route-handler'
|
|
611
|
+
},
|
|
612
|
+
`route-limit-${route.name}`
|
|
613
|
+
);
|
|
614
|
+
socket.end();
|
|
615
|
+
this.smartProxy.connectionManager.cleanupConnection(record, 'route_connection_limit');
|
|
616
|
+
return;
|
|
588
617
|
}
|
|
589
618
|
}
|
|
590
619
|
|
|
@@ -642,6 +671,10 @@ export class RouteConnectionHandler {
|
|
|
642
671
|
|
|
643
672
|
// Store the route config in the connection record for metrics and other uses
|
|
644
673
|
record.routeConfig = route;
|
|
674
|
+
record.routeId = route.id || route.name || 'unnamed';
|
|
675
|
+
|
|
676
|
+
// Track connection by route
|
|
677
|
+
this.smartProxy.connectionManager.trackConnectionByRoute(record.routeId, record.id);
|
|
645
678
|
|
|
646
679
|
// Check if this route uses NFTables for forwarding
|
|
647
680
|
if (action.forwardingEngine === 'nftables') {
|
|
@@ -960,6 +993,10 @@ export class RouteConnectionHandler {
|
|
|
960
993
|
|
|
961
994
|
// Store the route config in the connection record for metrics and other uses
|
|
962
995
|
record.routeConfig = route;
|
|
996
|
+
record.routeId = route.id || route.name || 'unnamed';
|
|
997
|
+
|
|
998
|
+
// Track connection by route
|
|
999
|
+
this.smartProxy.connectionManager.trackConnectionByRoute(record.routeId, record.id);
|
|
963
1000
|
|
|
964
1001
|
if (!route.action.socketHandler) {
|
|
965
1002
|
logger.log('error', 'socket-handler action missing socketHandler function', {
|
|
@@ -1,5 +1,7 @@
|
|
|
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
|
+
import { connectionLogDeduplicator } from '../../core/utils/log-deduplicator.js';
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
7
|
* Handles security aspects like IP tracking, rate limiting, and authorization
|
|
@@ -7,8 +9,12 @@ import type { SmartProxy } from './smart-proxy.js';
|
|
|
7
9
|
export class SecurityManager {
|
|
8
10
|
private connectionsByIP: Map<string, Set<string>> = new Map();
|
|
9
11
|
private connectionRateByIP: Map<string, number[]> = new Map();
|
|
12
|
+
private cleanupInterval: NodeJS.Timeout | null = null;
|
|
10
13
|
|
|
11
|
-
constructor(private smartProxy: SmartProxy) {
|
|
14
|
+
constructor(private smartProxy: SmartProxy) {
|
|
15
|
+
// Start periodic cleanup every 60 seconds
|
|
16
|
+
this.startPeriodicCleanup();
|
|
17
|
+
}
|
|
12
18
|
|
|
13
19
|
/**
|
|
14
20
|
* Get connections count by IP
|
|
@@ -164,7 +170,76 @@ export class SecurityManager {
|
|
|
164
170
|
* Clears all IP tracking data (for shutdown)
|
|
165
171
|
*/
|
|
166
172
|
public clearIPTracking(): void {
|
|
173
|
+
if (this.cleanupInterval) {
|
|
174
|
+
clearInterval(this.cleanupInterval);
|
|
175
|
+
this.cleanupInterval = null;
|
|
176
|
+
}
|
|
167
177
|
this.connectionsByIP.clear();
|
|
168
178
|
this.connectionRateByIP.clear();
|
|
169
179
|
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Start periodic cleanup of expired data
|
|
183
|
+
*/
|
|
184
|
+
private startPeriodicCleanup(): void {
|
|
185
|
+
this.cleanupInterval = setInterval(() => {
|
|
186
|
+
this.performCleanup();
|
|
187
|
+
}, 60000); // Run every minute
|
|
188
|
+
|
|
189
|
+
// Unref the timer so it doesn't keep the process alive
|
|
190
|
+
if (this.cleanupInterval.unref) {
|
|
191
|
+
this.cleanupInterval.unref();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Perform cleanup of expired rate limits and empty IP entries
|
|
197
|
+
*/
|
|
198
|
+
private performCleanup(): void {
|
|
199
|
+
const now = Date.now();
|
|
200
|
+
const minute = 60 * 1000;
|
|
201
|
+
let cleanedRateLimits = 0;
|
|
202
|
+
let cleanedIPs = 0;
|
|
203
|
+
|
|
204
|
+
// Clean up expired rate limit timestamps
|
|
205
|
+
for (const [ip, timestamps] of this.connectionRateByIP.entries()) {
|
|
206
|
+
const validTimestamps = timestamps.filter(time => now - time < minute);
|
|
207
|
+
|
|
208
|
+
if (validTimestamps.length === 0) {
|
|
209
|
+
// No valid timestamps, remove the IP entry
|
|
210
|
+
this.connectionRateByIP.delete(ip);
|
|
211
|
+
cleanedRateLimits++;
|
|
212
|
+
} else if (validTimestamps.length < timestamps.length) {
|
|
213
|
+
// Some timestamps expired, update with valid ones
|
|
214
|
+
this.connectionRateByIP.set(ip, validTimestamps);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Clean up IPs with no active connections
|
|
219
|
+
for (const [ip, connections] of this.connectionsByIP.entries()) {
|
|
220
|
+
if (connections.size === 0) {
|
|
221
|
+
this.connectionsByIP.delete(ip);
|
|
222
|
+
cleanedIPs++;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Log cleanup stats if anything was cleaned
|
|
227
|
+
if (cleanedRateLimits > 0 || cleanedIPs > 0) {
|
|
228
|
+
if (this.smartProxy.settings.enableDetailedLogging) {
|
|
229
|
+
connectionLogDeduplicator.log(
|
|
230
|
+
'ip-cleanup',
|
|
231
|
+
'debug',
|
|
232
|
+
'IP tracking cleanup completed',
|
|
233
|
+
{
|
|
234
|
+
cleanedRateLimits,
|
|
235
|
+
cleanedIPs,
|
|
236
|
+
remainingIPs: this.connectionsByIP.size,
|
|
237
|
+
remainingRateLimits: this.connectionRateByIP.size,
|
|
238
|
+
component: 'security-manager'
|
|
239
|
+
},
|
|
240
|
+
'periodic-cleanup'
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
170
245
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as plugins from '../../plugins.js';
|
|
2
2
|
import { logger } from '../../core/utils/logger.js';
|
|
3
|
+
import { connectionLogDeduplicator } from '../../core/utils/log-deduplicator.js';
|
|
3
4
|
|
|
4
5
|
// Importing required components
|
|
5
6
|
import { ConnectionManager } from './connection-manager.js';
|
|
@@ -515,6 +516,9 @@ export class SmartProxy extends plugins.EventEmitter {
|
|
|
515
516
|
|
|
516
517
|
// Stop metrics collector
|
|
517
518
|
this.metricsCollector.stop();
|
|
519
|
+
|
|
520
|
+
// Flush any pending deduplicated logs
|
|
521
|
+
connectionLogDeduplicator.flushAll();
|
|
518
522
|
|
|
519
523
|
logger.log('info', 'SmartProxy shutdown complete.');
|
|
520
524
|
}
|