@serve.zone/dcrouter 17.1.5 → 17.1.6

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 (32) hide show
  1. package/deno.json +1 -1
  2. package/dist_serve/bundle.js +559 -559
  3. package/dist_ts/00_commitinfo_data.js +1 -1
  4. package/dist_ts/classes.dcrouter.js +2 -2
  5. package/dist_ts/dns/manager.dns.js +23 -6
  6. package/dist_ts/email/classes.mail-dns-sync.d.ts +18 -0
  7. package/dist_ts/email/classes.mail-dns-sync.js +101 -2
  8. package/dist_ts/monitoring/classes.metricscache.d.ts +2 -0
  9. package/dist_ts/monitoring/classes.metricscache.js +28 -6
  10. package/dist_ts/monitoring/classes.metricsmanager.d.ts +10 -0
  11. package/dist_ts/monitoring/classes.metricsmanager.js +99 -31
  12. package/dist_ts/opsserver/handlers/security.handler.js +24 -6
  13. package/dist_ts/opsserver/handlers/stats.handler.js +2 -3
  14. package/dist_ts/remoteingress/classes.hub-lifecycle.js +2 -2
  15. package/dist_ts_interfaces/requests/stats.d.ts +7 -0
  16. package/dist_ts_migrations/index.js +277 -152
  17. package/dist_ts_web/00_commitinfo_data.js +1 -1
  18. package/dist_ts_web/appstate/network.js +11 -2
  19. package/dist_ts_web/appstate/ui.js +1 -8
  20. package/package.json +2 -2
  21. package/ts/00_commitinfo_data.ts +1 -1
  22. package/ts/classes.dcrouter.ts +3 -1
  23. package/ts/dns/manager.dns.ts +22 -5
  24. package/ts/email/classes.mail-dns-sync.ts +105 -1
  25. package/ts/monitoring/classes.metricscache.ts +36 -8
  26. package/ts/monitoring/classes.metricsmanager.ts +113 -32
  27. package/ts/opsserver/handlers/security.handler.ts +35 -6
  28. package/ts/opsserver/handlers/stats.handler.ts +1 -2
  29. package/ts/remoteingress/classes.hub-lifecycle.ts +3 -1
  30. package/ts_web/00_commitinfo_data.ts +1 -1
  31. package/ts_web/appstate/network.ts +12 -1
  32. package/ts_web/appstate/ui.ts +0 -9
@@ -5,11 +5,20 @@ import { SecurityLogger, SecurityEventType } from '../security/classes.securityl
5
5
  import { logger } from '../logger.js';
6
6
  import type { IAsnActivity } from '../../ts_interfaces/data/stats.js';
7
7
 
8
+ const NETWORK_STATS_CACHE_TTL_MS = 3_000;
9
+ const ASN_INTELLIGENCE_CACHE_TTL_MS = 60_000;
10
+ const ACTIVE_CONNECTION_SNAPSHOT_CACHE_TTL_MS = 3_000;
11
+ const OBSERVED_IP_SAMPLE_INTERVAL_MS = 60_000;
12
+ const MAX_ASN_CANDIDATE_IPS = 500;
13
+ const MAX_OBSERVED_IP_SAMPLE_SIZE = 20;
14
+ const MAX_SNAPSHOT_LIMIT = 1_000;
15
+
8
16
  export class MetricsManager {
9
17
  private metricsLogger: plugins.smartlog.Smartlog;
10
18
  private smartMetrics: plugins.smartmetrics.SmartMetrics;
11
19
  private dcRouter: DcRouter;
12
20
  private resetInterval?: NodeJS.Timeout;
21
+ private observedIpSamplerInterval?: NodeJS.Timeout;
13
22
  private metricsCache: MetricsCache;
14
23
 
15
24
  // Constants
@@ -69,7 +78,7 @@ export class MetricsManager {
69
78
  }
70
79
  });
71
80
  this.smartMetrics = new plugins.smartmetrics.SmartMetrics(this.metricsLogger, 'dcrouter');
72
- // Initialize metrics cache with 500ms TTL
81
+ // Individual metrics select an appropriate TTL where their collection cost differs.
73
82
  this.metricsCache = new MetricsCache(500);
74
83
  }
75
84
 
@@ -118,6 +127,11 @@ export class MetricsManager {
118
127
  // Prune old time-series buckets every minute (don't wait for lazy query)
119
128
  this.pruneOldBuckets();
120
129
  }, 60000); // Check every minute
130
+
131
+ this.observedIpSamplerInterval = setInterval(() => {
132
+ this.sampleObservedIps();
133
+ }, OBSERVED_IP_SAMPLE_INTERVAL_MS);
134
+ this.observedIpSamplerInterval.unref();
121
135
 
122
136
  logger.log('info', 'MetricsManager started');
123
137
  }
@@ -128,6 +142,10 @@ export class MetricsManager {
128
142
  clearInterval(this.resetInterval);
129
143
  this.resetInterval = undefined;
130
144
  }
145
+ if (this.observedIpSamplerInterval) {
146
+ clearInterval(this.observedIpSamplerInterval);
147
+ this.observedIpSamplerInterval = undefined;
148
+ }
131
149
 
132
150
  this.smartMetrics.stop();
133
151
 
@@ -295,13 +313,35 @@ export class MetricsManager {
295
313
  public async getActiveConnectionSnapshots(
296
314
  options: plugins.smartproxy.IActiveConnectionSnapshotOptions = {},
297
315
  ): Promise<plugins.smartproxy.IActiveConnectionSnapshot[]> {
298
- const cacheKey = `activeConnectionSnapshots:${options.limit ?? 1000}:${options.routeId ?? ''}`;
316
+ const normalizedOptions = {
317
+ ...options,
318
+ limit: Math.min(Math.max(options.limit ?? MAX_SNAPSHOT_LIMIT, 1), MAX_SNAPSHOT_LIMIT),
319
+ };
320
+ const afterId = (normalizedOptions as typeof normalizedOptions & { afterId?: string }).afterId ?? '';
321
+ const cacheKey = `activeConnectionSnapshots:${normalizedOptions.limit}:${normalizedOptions.routeId ?? ''}:${afterId}`;
299
322
  return await this.metricsCache.get<plugins.smartproxy.IActiveConnectionSnapshot[]>(cacheKey, async () => {
300
323
  if (!this.dcRouter.smartProxy) {
301
324
  return [];
302
325
  }
303
- return this.dcRouter.smartProxy.getActiveConnectionSnapshots(options);
304
- }, 500);
326
+ return this.dcRouter.smartProxy.getActiveConnectionSnapshots(normalizedOptions);
327
+ }, ACTIVE_CONNECTION_SNAPSHOT_CACHE_TTL_MS);
328
+ }
329
+
330
+ public async getActiveConnectionSnapshotPage(
331
+ options: plugins.smartproxy.IActiveConnectionSnapshotOptions = {},
332
+ ): Promise<plugins.smartproxy.IActiveConnectionSnapshotPage> {
333
+ const normalizedOptions = {
334
+ ...options,
335
+ limit: Math.min(Math.max(options.limit ?? MAX_SNAPSHOT_LIMIT, 1), MAX_SNAPSHOT_LIMIT),
336
+ };
337
+ const afterId = normalizedOptions.afterId ?? '';
338
+ const cacheKey = `activeConnectionSnapshotPage:${normalizedOptions.limit}:${normalizedOptions.routeId ?? ''}:${afterId}`;
339
+ return await this.metricsCache.get<plugins.smartproxy.IActiveConnectionSnapshotPage>(cacheKey, async () => {
340
+ if (!this.dcRouter.smartProxy) {
341
+ return { snapshots: [], hasMore: false, nextCursor: null };
342
+ }
343
+ return this.dcRouter.smartProxy.getActiveConnectionSnapshotPage(normalizedOptions);
344
+ }, ACTIVE_CONNECTION_SNAPSHOT_CACHE_TTL_MS);
305
345
  }
306
346
 
307
347
  // Get connection info from SmartProxy
@@ -560,10 +600,59 @@ export class MetricsManager {
560
600
  this.securityMetrics.incidents.shift();
561
601
  }
562
602
  }
603
+
604
+ /**
605
+ * Feed IP intelligence from maintained data-plane counters, independently of
606
+ * dashboard reads. Startup order is intentionally tolerated: until both
607
+ * SmartProxy and SecurityPolicyManager exist, the sample is a no-op.
608
+ */
609
+ private sampleObservedIps(): void {
610
+ const smartProxy = this.dcRouter.smartProxy;
611
+ const securityPolicyManager = this.dcRouter.securityPolicyManager;
612
+ if (!smartProxy || !securityPolicyManager) {
613
+ return;
614
+ }
615
+
616
+ try {
617
+ const proxyMetrics = smartProxy.getMetrics();
618
+ const connectionsByIP = proxyMetrics.connections.byIP();
619
+ const throughputByIP = proxyMetrics.throughput.byIP();
620
+ const candidates = new Map<string, { connections: number; throughput: number }>();
621
+
622
+ for (const [ip, connections] of connectionsByIP) {
623
+ candidates.set(ip, { connections, throughput: 0 });
624
+ }
625
+ for (const [ip, throughput] of throughputByIP) {
626
+ const candidate = candidates.get(ip) || { connections: 0, throughput: 0 };
627
+ candidate.throughput = throughput.in + throughput.out;
628
+ candidates.set(ip, candidate);
629
+ }
630
+ for (const ip of proxyMetrics.connections.domainRequestsByIP().keys()) {
631
+ if (!candidates.has(ip)) {
632
+ candidates.set(ip, { connections: 0, throughput: 0 });
633
+ }
634
+ }
635
+
636
+ const observedIps = [...candidates.entries()]
637
+ .filter(([ip]) => Boolean(ip))
638
+ .sort((a, b) => (
639
+ b[1].connections - a[1].connections
640
+ || b[1].throughput - a[1].throughput
641
+ || a[0].localeCompare(b[0])
642
+ ))
643
+ .slice(0, MAX_OBSERVED_IP_SAMPLE_SIZE)
644
+ .map(([ip]) => ip);
645
+
646
+ if (observedIps.length > 0) {
647
+ securityPolicyManager.queueObservedIps(observedIps);
648
+ }
649
+ } catch (error: unknown) {
650
+ logger.log('warn', `Observed-IP metrics sample failed: ${(error as Error).message}`);
651
+ }
652
+ }
563
653
 
564
654
  // Get network metrics from SmartProxy
565
655
  public async getNetworkStats() {
566
- // Use shorter cache TTL for network stats to ensure real-time updates
567
656
  return this.metricsCache.get('networkStats', async () => {
568
657
  const smartProxy = this.dcRouter.smartProxy;
569
658
  const proxyMetrics = smartProxy ? smartProxy.getMetrics() : null;
@@ -571,6 +660,7 @@ export class MetricsManager {
571
660
  if (!proxyMetrics) {
572
661
  return {
573
662
  connectionsByIP: new Map<string, number>(),
663
+ activeConnections: 0,
574
664
  throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
575
665
  topIPs: [] as Array<{ ip: string; count: number }>,
576
666
  topIPsByBandwidth: [] as Array<{ ip: string; count: number; bwIn: number; bwOut: number }>,
@@ -587,21 +677,9 @@ export class MetricsManager {
587
677
  };
588
678
  }
589
679
 
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
- }
680
+ const connectionsByIP = proxyMetrics.connections.byIP();
681
+ const connectionsByRoute = proxyMetrics.connections.byRoute();
682
+ const activeConnections = proxyMetrics.connections.active();
605
683
 
606
684
  const instantThroughput = proxyMetrics.throughput.instant();
607
685
 
@@ -769,7 +847,6 @@ export class MetricsManager {
769
847
  ...topIPs.map((item) => item.ip),
770
848
  ...topIPsByBandwidth.map((item) => item.ip),
771
849
  ])];
772
- this.dcRouter.securityPolicyManager?.queueObservedIps(observedIps);
773
850
 
774
851
  const topASNs = await this.buildTopASNs(observedIps, allIPData);
775
852
 
@@ -808,10 +885,6 @@ export class MetricsManager {
808
885
  for (const entry of protocolCache) {
809
886
  if (entry.domain) allKnownDomains.add(entry.domain);
810
887
  }
811
- for (const snapshot of activeConnectionSnapshots) {
812
- if (snapshot.domain) allKnownDomains.add(snapshot.domain);
813
- }
814
-
815
888
  // Build reverse map: concrete domain → canonical route key(s)
816
889
  const domainToRoutes = new Map<string, string[]>();
817
890
  for (const [routeKey, domains] of routeDomains) {
@@ -882,7 +955,7 @@ export class MetricsManager {
882
955
  }
883
956
 
884
957
  domainAgg.set(domain, {
885
- activeConnections: activeConnectionsByDomain.get(domain) ?? Math.round(totalConns),
958
+ activeConnections: Math.round(totalConns),
886
959
  bytesInPerSec: totalIn,
887
960
  bytesOutPerSec: totalOut,
888
961
  routeCount: routeKeys.length,
@@ -914,6 +987,7 @@ export class MetricsManager {
914
987
 
915
988
  return {
916
989
  connectionsByIP,
990
+ activeConnections,
917
991
  throughputRate,
918
992
  topIPs,
919
993
  topIPsByBandwidth,
@@ -928,7 +1002,7 @@ export class MetricsManager {
928
1002
  backendProtocols,
929
1003
  domainActivity,
930
1004
  };
931
- }, 1000); // 1s cache — matches typical dashboard poll interval
1005
+ }, NETWORK_STATS_CACHE_TTL_MS);
932
1006
  }
933
1007
 
934
1008
  private async buildTopASNs(
@@ -936,14 +1010,21 @@ export class MetricsManager {
936
1010
  allIPData: Map<string, { count: number; bwIn: number; bwOut: number }>,
937
1011
  ): Promise<IAsnActivity[]> {
938
1012
  const manager = this.dcRouter.securityPolicyManager;
939
- if (!manager || observedIps.length === 0) {
1013
+ const candidateIps = [...new Set(observedIps.filter(Boolean))]
1014
+ .sort()
1015
+ .slice(0, MAX_ASN_CANDIDATE_IPS);
1016
+ if (!manager || candidateIps.length === 0) {
940
1017
  return [];
941
1018
  }
942
1019
 
943
- const intelligenceRecords = await manager.listIpIntelligence({
944
- ipAddresses: observedIps,
945
- limit: Math.max(100, observedIps.length),
946
- });
1020
+ const intelligenceRecords = await this.metricsCache.get(
1021
+ 'networkAsnIntelligence',
1022
+ () => manager.listIpIntelligence({
1023
+ ipAddresses: candidateIps,
1024
+ limit: Math.max(100, candidateIps.length),
1025
+ }),
1026
+ ASN_INTELLIGENCE_CACHE_TTL_MS,
1027
+ );
947
1028
  const asnActivity = new Map<number, IAsnActivity>();
948
1029
 
949
1030
  for (const record of intelligenceRecords) {
@@ -45,7 +45,13 @@ export class SecurityHandler {
45
45
  'getActiveConnections',
46
46
  async (dataArg, toolsArg) => {
47
47
  await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'stats:read' });
48
- const connectionInfos = await this.getActiveConnections(dataArg.protocol, dataArg.state);
48
+ const page = await this.getActiveConnections(
49
+ dataArg.protocol,
50
+ dataArg.state,
51
+ dataArg.limit,
52
+ dataArg.cursor,
53
+ );
54
+ const connectionInfos = page.connections;
49
55
  const totalConnections = connectionInfos.reduce((sum, conn) => sum + (conn.connectionCount || 1), 0);
50
56
 
51
57
  const summary = {
@@ -63,6 +69,7 @@ export class SecurityHandler {
63
69
  return {
64
70
  connections: connectionInfos,
65
71
  summary,
72
+ pagination: page.pagination,
66
73
  };
67
74
  }
68
75
  )
@@ -349,14 +356,27 @@ export class SecurityHandler {
349
356
 
350
357
  private async getActiveConnections(
351
358
  protocol?: 'http' | 'https' | 'smtp' | 'smtps',
352
- state?: string
353
- ): Promise<interfaces.data.IConnectionInfo[]> {
359
+ state?: string,
360
+ requestedLimit?: number,
361
+ cursor?: string,
362
+ ): Promise<{
363
+ connections: interfaces.data.IConnectionInfo[];
364
+ pagination: { limit: number; hasMore: boolean; nextCursor?: string };
365
+ }> {
366
+ const limit = Math.min(Math.max(Math.floor(requestedLimit ?? 250), 1), 1_000);
354
367
  const metricsManager = this.opsServerRef.dcRouterRef.metricsManager;
355
368
  if (!metricsManager) {
356
- return [];
369
+ return {
370
+ connections: [],
371
+ pagination: { limit, hasMore: false },
372
+ };
357
373
  }
358
374
 
359
- const snapshots = await metricsManager.getActiveConnectionSnapshots({ limit: 10000 });
375
+ const page = await metricsManager.getActiveConnectionSnapshotPage({
376
+ limit,
377
+ afterId: cursor,
378
+ });
379
+ const snapshots = page.snapshots;
360
380
  const connections = snapshots.map((snapshot): interfaces.data.IConnectionInfo => ({
361
381
  id: String(snapshot.id),
362
382
  remoteAddress: snapshot.sourcePort === null
@@ -372,7 +392,7 @@ export class SecurityHandler {
372
392
  bytesSent: snapshot.bytesOut,
373
393
  }));
374
394
 
375
- return connections.filter((connection) => {
395
+ const filteredConnections = connections.filter((connection) => {
376
396
  if (protocol && connection.protocol !== protocol) {
377
397
  return false;
378
398
  }
@@ -381,6 +401,15 @@ export class SecurityHandler {
381
401
  }
382
402
  return true;
383
403
  });
404
+
405
+ return {
406
+ connections: filteredConnections,
407
+ pagination: {
408
+ limit,
409
+ hasMore: page.hasMore,
410
+ nextCursor: page.nextCursor ?? undefined,
411
+ },
412
+ };
384
413
  }
385
414
 
386
415
  private mapSnapshotProtocol(
@@ -288,7 +288,6 @@ export class StatsHandler {
288
288
  promises.push(
289
289
  (async () => {
290
290
  const stats = await this.opsServerRef.dcRouterRef.metricsManager!.getNetworkStats();
291
- const serverStats = await this.collectServerStats();
292
291
 
293
292
  // Build per-IP bandwidth lookup from throughputByIP
294
293
  const ipBandwidth = new Map<string, { in: number; out: number }>();
@@ -322,7 +321,7 @@ export class StatsHandler {
322
321
  in: stats.totalDataTransferred.bytesIn,
323
322
  out: stats.totalDataTransferred.bytesOut,
324
323
  },
325
- activeConnections: serverStats.activeConnections,
324
+ activeConnections: stats.activeConnections,
326
325
  connectionDetails,
327
326
  topEndpoints: stats.topIPs.map(ip => ({
328
327
  endpoint: ip.ip,
@@ -223,7 +223,9 @@ export class RemoteIngressHubLifecycle {
223
223
  tls: tlsConfig,
224
224
  performance: manager.getHubPerformanceConfig(),
225
225
  });
226
- tunnelManager.setOnTopologyChanged((reason) => this.dcRouterRef.mailDnsSync.requestSync(reason));
226
+ tunnelManager.setOnTopologyChanged((reason) => (
227
+ this.dcRouterRef.mailDnsSync.requestEdgeEligibilityCheck(reason)
228
+ ));
227
229
  try {
228
230
  await tunnelManager.start();
229
231
  } catch (err) {
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '17.1.5',
6
+ version: '17.1.6',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }
@@ -4,6 +4,9 @@ import { appState } from './shared.js';
4
4
  import { getActionContext } from './login.js';
5
5
  import { runBackgroundRefresh } from './shared.js';
6
6
 
7
+ const IP_INTELLIGENCE_REFRESH_INTERVAL_MS = 60_000;
8
+ let lastIpIntelligenceRefreshAt = 0;
9
+
7
10
  export interface INetworkState {
8
11
  connections: interfaces.data.IConnectionInfo[];
9
12
  connectionsByIP: { [ip: string]: number };
@@ -53,9 +56,17 @@ export const networkStatePart = await appState.getStatePart<INetworkState>(
53
56
  );
54
57
 
55
58
  export function refreshNetworkIpIntelligence(identity: interfaces.data.IIdentity, ipAddresses: string[]): void {
56
- const ips = [...new Set(ipAddresses.filter(Boolean))].slice(0, 100);
59
+ const ips = [...new Set(ipAddresses.map((ip) => ip.trim()).filter(Boolean))]
60
+ .sort()
61
+ .slice(0, 100);
57
62
  if (ips.length === 0) return;
58
63
 
64
+ const now = Date.now();
65
+ if (now - lastIpIntelligenceRefreshAt < IP_INTELLIGENCE_REFRESH_INTERVAL_MS) {
66
+ return;
67
+ }
68
+ lastIpIntelligenceRefreshAt = now;
69
+
59
70
  runBackgroundRefresh('networkIpIntelligence', 'IP intelligence refresh failed:', async () => {
60
71
  const intelligenceRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
61
72
  interfaces.requests.IReq_ListIpIntelligence
@@ -1,5 +1,4 @@
1
1
  import { appState } from './shared.js';
2
- import { networkStatePart, fetchNetworkStatsAction } from './network.js';
3
2
  import { certificateStatePart, fetchCertificateOverviewAction } from './certificates.js';
4
3
 
5
4
  export interface IUiState {
@@ -52,13 +51,6 @@ export const toggleAutoRefreshAction = uiStatePart.createAction(async (statePart
52
51
  export const setActiveViewAction = uiStatePart.createAction<string>(async (statePartArg, viewName): Promise<IUiState> => {
53
52
  const currentState = statePartArg.getState()!;
54
53
 
55
- // If switching to network view, ensure we fetch network data
56
- if (viewName === 'network' && currentState.activeView !== 'network') {
57
- setTimeout(() => {
58
- networkStatePart.dispatchAction(fetchNetworkStatsAction, null);
59
- }, 100);
60
- }
61
-
62
54
  // If switching to the Domains group, ensure we fetch certificate data
63
55
  // (Certificates is a subview of Domains).
64
56
  if (viewName === 'domains' && currentState.activeView !== 'domains') {
@@ -72,4 +64,3 @@ export const setActiveViewAction = uiStatePart.createAction<string>(async (state
72
64
  activeView: viewName,
73
65
  };
74
66
  });
75
-