@serve.zone/dcrouter 13.39.0 → 13.40.1

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.
@@ -143,8 +143,9 @@ export class MetricsManager {
143
143
  public async getServerStats() {
144
144
  return this.metricsCache.get('serverStats', async () => {
145
145
  const smartMetricsData = await this.smartMetrics.getMetrics();
146
- const proxyMetrics = this.dcRouter.smartProxy ? this.dcRouter.smartProxy.getMetrics() : null;
147
- const proxyStats = this.dcRouter.smartProxy ? await this.dcRouter.smartProxy.getStatistics() : null;
146
+ const smartProxy = this.dcRouter.smartProxy;
147
+ const proxyMetrics = smartProxy ? smartProxy.getMetrics() : null;
148
+ const proxyStats = smartProxy ? await smartProxy.getStatistics() : null;
148
149
  const { heapUsed, heapTotal, external, rss } = process.memoryUsage();
149
150
 
150
151
  return {
@@ -291,27 +292,44 @@ export class MetricsManager {
291
292
  });
292
293
  }
293
294
 
295
+ public async getActiveConnectionSnapshots(
296
+ options: plugins.smartproxy.IActiveConnectionSnapshotOptions = {},
297
+ ): Promise<plugins.smartproxy.IActiveConnectionSnapshot[]> {
298
+ const cacheKey = `activeConnectionSnapshots:${options.limit ?? 1000}:${options.routeId ?? ''}`;
299
+ return await this.metricsCache.get<plugins.smartproxy.IActiveConnectionSnapshot[]>(cacheKey, async () => {
300
+ if (!this.dcRouter.smartProxy) {
301
+ return [];
302
+ }
303
+ return this.dcRouter.smartProxy.getActiveConnectionSnapshots(options);
304
+ }, 500);
305
+ }
306
+
294
307
  // Get connection info from SmartProxy
295
308
  public async getConnectionInfo() {
296
- return this.metricsCache.get('connectionInfo', () => {
297
- const proxyMetrics = this.dcRouter.smartProxy ? this.dcRouter.smartProxy.getMetrics() : null;
298
-
299
- if (!proxyMetrics) {
300
- return [] as Array<{ type: string; count: number; source: string; lastActivity: Date }>;
309
+ return this.metricsCache.get('connectionInfo', async () => {
310
+ const snapshots = await this.getActiveConnectionSnapshots({ limit: 10000 });
311
+ const connectionsByRoute = new Map<string, { count: number; lastActivity: Date }>();
312
+
313
+ for (const snapshot of snapshots) {
314
+ const source = snapshot.routeId || snapshot.domain || `${snapshot.protocol || 'connection'}:${snapshot.localPort}`;
315
+ const existing = connectionsByRoute.get(source) || { count: 0, lastActivity: new Date(snapshot.startedAtMs) };
316
+ existing.count++;
317
+ if (snapshot.startedAtMs > existing.lastActivity.getTime()) {
318
+ existing.lastActivity = new Date(snapshot.startedAtMs);
319
+ }
320
+ connectionsByRoute.set(source, existing);
301
321
  }
302
322
 
303
- const connectionsByRoute = proxyMetrics.connections.byRoute();
304
323
  const connectionInfo: Array<{ type: string; count: number; source: string; lastActivity: Date }> = [];
305
-
306
- for (const [routeName, count] of connectionsByRoute) {
324
+ for (const [source, info] of connectionsByRoute) {
307
325
  connectionInfo.push({
308
326
  type: 'https',
309
- count,
310
- source: routeName,
311
- lastActivity: new Date(),
327
+ count: info.count,
328
+ source,
329
+ lastActivity: info.lastActivity,
312
330
  });
313
331
  }
314
-
332
+
315
333
  return connectionInfo;
316
334
  });
317
335
  }
@@ -547,7 +565,8 @@ export class MetricsManager {
547
565
  public async getNetworkStats() {
548
566
  // Use shorter cache TTL for network stats to ensure real-time updates
549
567
  return this.metricsCache.get('networkStats', async () => {
550
- const proxyMetrics = this.dcRouter.smartProxy ? this.dcRouter.smartProxy.getMetrics() : null;
568
+ const smartProxy = this.dcRouter.smartProxy;
569
+ const proxyMetrics = smartProxy ? smartProxy.getMetrics() : null;
551
570
 
552
571
  if (!proxyMetrics) {
553
572
  return {
@@ -568,8 +587,22 @@ export class MetricsManager {
568
587
  };
569
588
  }
570
589
 
571
- // Get metrics using the new API
572
- const connectionsByIP = proxyMetrics.connections.byIP();
590
+ const activeConnectionSnapshots = await this.getActiveConnectionSnapshots({ limit: 10000 });
591
+
592
+ const connectionsByIP = new Map<string, number>();
593
+ const connectionsByRoute = new Map<string, number>();
594
+ const activeConnectionsByDomain = new Map<string, number>();
595
+
596
+ for (const snapshot of activeConnectionSnapshots) {
597
+ connectionsByIP.set(snapshot.sourceIp, (connectionsByIP.get(snapshot.sourceIp) || 0) + 1);
598
+ if (snapshot.routeId) {
599
+ connectionsByRoute.set(snapshot.routeId, (connectionsByRoute.get(snapshot.routeId) || 0) + 1);
600
+ }
601
+ if (snapshot.domain) {
602
+ activeConnectionsByDomain.set(snapshot.domain, (activeConnectionsByDomain.get(snapshot.domain) || 0) + 1);
603
+ }
604
+ }
605
+
573
606
  const instantThroughput = proxyMetrics.throughput.instant();
574
607
 
575
608
  // Get throughput rate
@@ -578,8 +611,11 @@ export class MetricsManager {
578
611
  bytesOutPerSecond: instantThroughput.out
579
612
  };
580
613
 
581
- // Get top IPs by connection count
582
- const topIPs = proxyMetrics.connections.topIPs(10);
614
+ // Get top IPs by active connection count
615
+ const topIPs = Array.from(connectionsByIP.entries())
616
+ .sort((a, b) => b[1] - a[1])
617
+ .slice(0, 10)
618
+ .map(([ip, count]) => ({ ip, count }));
583
619
 
584
620
  // Get total data transferred
585
621
  const totalDataTransferred = {
@@ -738,7 +774,6 @@ export class MetricsManager {
738
774
  const topASNs = await this.buildTopASNs(observedIps, allIPData);
739
775
 
740
776
  // Build domain activity using per-IP domain request counts from Rust engine
741
- const connectionsByRoute = proxyMetrics.connections.byRoute();
742
777
  const throughputByRoute = proxyMetrics.throughput.byRoute();
743
778
 
744
779
  // Aggregate per-IP domain request counts into per-domain totals
@@ -773,6 +808,9 @@ export class MetricsManager {
773
808
  for (const entry of protocolCache) {
774
809
  if (entry.domain) allKnownDomains.add(entry.domain);
775
810
  }
811
+ for (const snapshot of activeConnectionSnapshots) {
812
+ if (snapshot.domain) allKnownDomains.add(snapshot.domain);
813
+ }
776
814
 
777
815
  // Build reverse map: concrete domain → canonical route key(s)
778
816
  const domainToRoutes = new Map<string, string[]>();
@@ -844,7 +882,7 @@ export class MetricsManager {
844
882
  }
845
883
 
846
884
  domainAgg.set(domain, {
847
- activeConnections: Math.round(totalConns),
885
+ activeConnections: activeConnectionsByDomain.get(domain) ?? Math.round(totalConns),
848
886
  bytesInPerSec: totalIn,
849
887
  bytesOutPerSec: totalOut,
850
888
  routeCount: routeKeys.length,
@@ -1,7 +1,6 @@
1
1
  import * as plugins from '../../plugins.js';
2
2
  import type { OpsServer } from '../classes.opsserver.js';
3
3
  import * as interfaces from '../../../ts_interfaces/index.js';
4
- import { MetricsManager } from '../../monitoring/index.js';
5
4
  import { requireOpsAuth } from '../helpers/auth.js';
6
5
 
7
6
  export class SecurityHandler {
@@ -46,18 +45,7 @@ export class SecurityHandler {
46
45
  'getActiveConnections',
47
46
  async (dataArg, toolsArg) => {
48
47
  await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'stats:read' });
49
- const connections = await this.getActiveConnections(dataArg.protocol, dataArg.state);
50
- const connectionInfos: interfaces.data.IConnectionInfo[] = connections.map(conn => ({
51
- id: conn.id,
52
- remoteAddress: conn.source.ip,
53
- localAddress: conn.destination.ip,
54
- startTime: conn.startTime,
55
- protocol: conn.type === 'http' ? 'https' : conn.type as any,
56
- state: conn.status === 'active' ? 'connected' : conn.status as any,
57
- bytesReceived: (conn as any)._throughputIn || 0,
58
- bytesSent: (conn as any)._throughputOut || 0,
59
- connectionCount: conn.bytesTransferred || 1,
60
- }));
48
+ const connectionInfos = await this.getActiveConnections(dataArg.protocol, dataArg.state);
61
49
  const totalConnections = connectionInfos.reduce((sum, conn) => sum + (conn.connectionCount || 1), 0);
62
50
 
63
51
  const summary = {
@@ -362,106 +350,66 @@ export class SecurityHandler {
362
350
  private async getActiveConnections(
363
351
  protocol?: 'http' | 'https' | 'smtp' | 'smtps',
364
352
  state?: string
365
- ): Promise<Array<{
366
- id: string;
367
- type: 'http' | 'smtp' | 'dns';
368
- source: {
369
- ip: string;
370
- port: number;
371
- country?: string;
372
- };
373
- destination: {
374
- ip: string;
375
- port: number;
376
- service?: string;
377
- };
378
- startTime: number;
379
- bytesTransferred: number;
380
- status: 'active' | 'idle' | 'closing';
381
- }>> {
382
- const connections: Array<{
383
- id: string;
384
- type: 'http' | 'smtp' | 'dns';
385
- source: {
386
- ip: string;
387
- port: number;
388
- country?: string;
389
- };
390
- destination: {
391
- ip: string;
392
- port: number;
393
- service?: string;
394
- };
395
- startTime: number;
396
- bytesTransferred: number;
397
- status: 'active' | 'idle' | 'closing';
398
- }> = [];
399
-
400
- // Get connection info and network stats from MetricsManager if available
401
- if (this.opsServerRef.dcRouterRef.metricsManager) {
402
- const connectionInfo = await this.opsServerRef.dcRouterRef.metricsManager.getConnectionInfo();
403
- const networkStats = await this.opsServerRef.dcRouterRef.metricsManager.getNetworkStats();
404
-
405
- // One aggregate row per IP with real throughput data
406
- if (networkStats.connectionsByIP && networkStats.connectionsByIP.size > 0) {
407
- let connIndex = 0;
408
- const publicIp = this.opsServerRef.dcRouterRef.options.publicIp || 'server';
353
+ ): Promise<interfaces.data.IConnectionInfo[]> {
354
+ const metricsManager = this.opsServerRef.dcRouterRef.metricsManager;
355
+ if (!metricsManager) {
356
+ return [];
357
+ }
409
358
 
410
- for (const [ip, count] of networkStats.connectionsByIP) {
411
- const tp = networkStats.throughputByIP?.get(ip);
412
- connections.push({
413
- id: `ip-${connIndex++}`,
414
- type: 'http',
415
- source: {
416
- ip: ip,
417
- port: 0,
418
- },
419
- destination: {
420
- ip: publicIp,
421
- port: 443,
422
- service: 'proxy',
423
- },
424
- startTime: 0,
425
- bytesTransferred: count, // Store connection count here
426
- status: 'active',
427
- // Attach real throughput for the handler mapping
428
- ...(tp ? { _throughputIn: tp.in, _throughputOut: tp.out } : {}),
429
- } as any);
430
- }
431
- } else if (connectionInfo.length > 0) {
432
- // Fallback to route-based connection info if no IP data available
433
- connectionInfo.forEach((info, index) => {
434
- connections.push({
435
- id: `conn-${index}`,
436
- type: 'http',
437
- source: {
438
- ip: 'unknown',
439
- port: 0,
440
- },
441
- destination: {
442
- ip: this.opsServerRef.dcRouterRef.options.publicIp || 'server',
443
- port: 443,
444
- service: info.source,
445
- },
446
- startTime: info.lastActivity.getTime(),
447
- bytesTransferred: 0,
448
- status: 'active',
449
- });
450
- });
359
+ const snapshots = await metricsManager.getActiveConnectionSnapshots({ limit: 10000 });
360
+ const connections = snapshots.map((snapshot): interfaces.data.IConnectionInfo => ({
361
+ id: String(snapshot.id),
362
+ remoteAddress: snapshot.sourcePort === null
363
+ ? snapshot.sourceIp
364
+ : `${snapshot.sourceIp}:${snapshot.sourcePort}`,
365
+ localAddress: snapshot.targetHost
366
+ ? `${snapshot.targetHost}:${snapshot.targetPort ?? snapshot.localPort}`
367
+ : `${this.opsServerRef.dcRouterRef.options.publicIp || 'server'}:${snapshot.localPort}`,
368
+ startTime: snapshot.startedAtMs,
369
+ protocol: this.mapSnapshotProtocol(snapshot),
370
+ state: this.mapSnapshotState(snapshot.state),
371
+ bytesReceived: snapshot.bytesIn,
372
+ bytesSent: snapshot.bytesOut,
373
+ }));
374
+
375
+ return connections.filter((connection) => {
376
+ if (protocol && connection.protocol !== protocol) {
377
+ return false;
378
+ }
379
+ if (state && connection.state !== state) {
380
+ return false;
451
381
  }
382
+ return true;
383
+ });
384
+ }
385
+
386
+ private mapSnapshotProtocol(
387
+ snapshot: plugins.smartproxy.IActiveConnectionSnapshot,
388
+ ): interfaces.data.IConnectionInfo['protocol'] {
389
+ if (snapshot.localPort === 465) {
390
+ return 'smtps';
452
391
  }
453
-
454
- // Filter by protocol if specified
455
- if (protocol) {
456
- return connections.filter(conn => {
457
- if (protocol === 'https' || protocol === 'http') {
458
- return conn.type === 'http';
459
- }
460
- return conn.type === protocol.replace('s', ''); // smtp/smtps -> smtp
461
- });
392
+ if ([25, 587, 2525].includes(snapshot.localPort)) {
393
+ return 'smtp';
462
394
  }
463
-
464
- return connections;
395
+
396
+ switch (snapshot.protocol) {
397
+ case 'http':
398
+ return 'http';
399
+ case 'https':
400
+ case 'tls':
401
+ case 'tls-passthrough':
402
+ case 'tls-reencrypt':
403
+ case 'tls-socket-handler':
404
+ case 'quic':
405
+ return 'https';
406
+ default:
407
+ return snapshot.localPort === 80 ? 'http' : 'https';
408
+ }
409
+ }
410
+
411
+ private mapSnapshotState(state: string): interfaces.data.IConnectionInfo['state'] {
412
+ return state === 'closing' ? 'closing' : 'connected';
465
413
  }
466
414
 
467
415
  private async getRateLimitStatus(
@@ -91,7 +91,6 @@ export class RadiusServer {
91
91
  private vlanManager: VlanManager;
92
92
  private accountingManager: AccountingManager;
93
93
  private config: IRadiusServerConfig;
94
- private clientSecrets: Map<string, string> = new Map();
95
94
  private running: boolean = false;
96
95
 
97
96
  // Statistics
@@ -138,24 +137,18 @@ export class RadiusServer {
138
137
  await this.vlanManager.importMappings(this.config.vlanAssignment.mappings);
139
138
  }
140
139
 
141
- // Build client secrets map
142
- this.buildClientSecretsMap();
140
+ const cidrSecrets = this.buildClientSecretsMap();
143
141
 
144
142
  // Create the RADIUS server
145
143
  this.radiusServer = new plugins.smartradius.RadiusServer({
146
144
  authPort: this.config.authPort,
147
145
  acctPort: this.config.acctPort,
148
146
  bindAddress: this.config.bindAddress,
149
- secretResolver: this.resolveClientSecret.bind(this),
147
+ cidrSecrets,
150
148
  authenticationHandler: this.handleAuthentication.bind(this),
151
149
  accountingHandler: this.handleAccounting.bind(this),
152
150
  });
153
151
 
154
- // Configure per-client secrets
155
- for (const [ip, secret] of this.clientSecrets) {
156
- this.radiusServer.setClientSecret(ip, secret);
157
- }
158
-
159
152
  // Start the server
160
153
  await this.radiusServer.start();
161
154
 
@@ -204,6 +197,7 @@ export class RadiusServer {
204
197
  calledStationId: request.calledStationId,
205
198
  callingStationId: request.callingStationId,
206
199
  serviceType: request.serviceType !== undefined ? String(request.serviceType) : undefined,
200
+ framedMtu: request.framedMtu,
207
201
  };
208
202
 
209
203
  logger.log('debug', `RADIUS Auth Request: user=${authData.username}, NAS=${authData.nasIpAddress}`);
@@ -288,6 +282,8 @@ export class RadiusServer {
288
282
  outputPackets: request.outputPackets,
289
283
  sessionTime: request.sessionTime,
290
284
  terminateCause: request.terminateCause !== undefined ? String(request.terminateCause) : undefined,
285
+ framedIpAddress: request.framedIpAddress,
286
+ serviceType: request.serviceType !== undefined ? String(request.serviceType) : undefined,
291
287
  };
292
288
 
293
289
  try {
@@ -394,66 +390,18 @@ export class RadiusServer {
394
390
  /**
395
391
  * Build client secrets map from configuration
396
392
  */
397
- private buildClientSecretsMap(): void {
398
- this.clientSecrets.clear();
393
+ private buildClientSecretsMap(): Record<string, string> {
394
+ const cidrSecrets: Record<string, string> = {};
399
395
 
400
396
  for (const client of this.config.clients) {
401
397
  if (!client.enabled) {
402
398
  continue;
403
399
  }
404
400
 
405
- if (!client.ipRange.includes('/')) {
406
- this.clientSecrets.set(client.ipRange, client.secret);
407
- }
408
- }
409
- }
410
-
411
- private resolveClientSecret(clientAddress: string): string | undefined {
412
- const normalizedClientAddress = this.normalizeClientAddress(clientAddress);
413
- if (!normalizedClientAddress) return undefined;
414
-
415
- const exactSecret = this.clientSecrets.get(normalizedClientAddress);
416
- if (exactSecret !== undefined) return exactSecret;
417
-
418
- for (const client of this.config.clients) {
419
- if (!client.enabled || !client.ipRange.includes('/')) continue;
420
- if (this.clientIpMatchesCidr(normalizedClientAddress, client.ipRange)) return client.secret;
401
+ cidrSecrets[client.ipRange] = client.secret;
421
402
  }
422
403
 
423
- return undefined;
424
- }
425
-
426
- private normalizeClientAddress(clientAddress: string): string | undefined {
427
- const trimmedAddress = clientAddress.trim();
428
- const normalizedAddress = trimmedAddress.startsWith('::ffff:')
429
- ? trimmedAddress.slice('::ffff:'.length)
430
- : trimmedAddress;
431
- return plugins.net.isIP(normalizedAddress) ? normalizedAddress : undefined;
432
- }
433
-
434
- private clientIpMatchesCidr(clientAddress: string, cidr: string): boolean {
435
- const [networkAddress, rawPrefix] = cidr.trim().split('/');
436
- const normalizedNetworkAddress = this.normalizeClientAddress(networkAddress || '');
437
- if (!normalizedNetworkAddress || rawPrefix === undefined) return false;
438
-
439
- const clientIp = this.ipv4ToNumber(clientAddress);
440
- const networkIp = this.ipv4ToNumber(normalizedNetworkAddress);
441
- if (clientIp === undefined || networkIp === undefined) return false;
442
-
443
- const prefix = Number(rawPrefix);
444
- if (!Number.isInteger(prefix) || prefix < 0 || prefix > 32) return false;
445
-
446
- const mask = prefix === 0 ? 0 : (0xffffffff << (32 - prefix)) >>> 0;
447
- return ((clientIp & mask) >>> 0) === ((networkIp & mask) >>> 0);
448
- }
449
-
450
- private ipv4ToNumber(ipAddress: string): number | undefined {
451
- if (plugins.net.isIP(ipAddress) !== 4) return undefined;
452
- const parts = ipAddress.split('.').map((part) => Number(part));
453
- if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
454
- return undefined;
455
- }
456
- return (((parts[0] * 256 + parts[1]) * 256 + parts[2]) * 256 + parts[3]) >>> 0;
404
+ return cidrSecrets;
457
405
  }
458
406
 
459
407
  /**
@@ -462,16 +410,20 @@ export class RadiusServer {
462
410
  async addClient(client: IRadiusClient): Promise<void> {
463
411
  // Check if client already exists
464
412
  const existingIndex = this.config.clients.findIndex(c => c.name === client.name);
413
+ const previousClient = existingIndex >= 0 ? this.config.clients[existingIndex] : undefined;
465
414
  if (existingIndex >= 0) {
466
415
  this.config.clients[existingIndex] = client;
467
416
  } else {
468
417
  this.config.clients.push(client);
469
418
  }
470
419
 
471
- this.buildClientSecretsMap();
472
-
473
- if (this.running && this.radiusServer && client.enabled && !client.ipRange.includes('/')) {
474
- this.radiusServer.setClientSecret(client.ipRange, client.secret);
420
+ if (this.running && this.radiusServer) {
421
+ if (previousClient) {
422
+ this.radiusServer.removeNetworkSecret(previousClient.ipRange);
423
+ }
424
+ if (client.enabled) {
425
+ this.radiusServer.setNetworkSecret(client.ipRange, client.secret);
426
+ }
475
427
  }
476
428
 
477
429
  logger.log('info', `RADIUS client ${client.enabled ? 'added' : 'disabled'}: ${client.name} (${client.ipRange})`);
@@ -486,10 +438,8 @@ export class RadiusServer {
486
438
  const client = this.config.clients[index];
487
439
  this.config.clients.splice(index, 1);
488
440
 
489
- this.buildClientSecretsMap();
490
-
491
- if (this.radiusServer && !client.ipRange.includes('/')) {
492
- this.radiusServer.removeClientSecret(client.ipRange);
441
+ if (this.radiusServer) {
442
+ this.radiusServer.removeNetworkSecret(client.ipRange);
493
443
  }
494
444
 
495
445
  logger.log('info', `RADIUS client removed: ${name}`);
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '13.39.0',
6
+ version: '13.40.1',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }