@serve.zone/dcrouter 13.34.0 → 13.36.0

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 (35) hide show
  1. package/dist_serve/bundle.js +22 -5
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/classes.dcrouter.d.ts +1 -1
  4. package/dist_ts/classes.dcrouter.js +11 -12
  5. package/dist_ts/config/classes.route-config-manager.d.ts +6 -7
  6. package/dist_ts/config/classes.route-config-manager.js +19 -28
  7. package/dist_ts/config/classes.target-profile-manager.d.ts +11 -19
  8. package/dist_ts/config/classes.target-profile-manager.js +20 -184
  9. package/dist_ts/monitoring/classes.metricsmanager.d.ts +4 -0
  10. package/dist_ts/monitoring/classes.metricsmanager.js +59 -6
  11. package/dist_ts/opsserver/handlers/security.handler.js +3 -1
  12. package/dist_ts/opsserver/handlers/stats.handler.js +2 -1
  13. package/dist_ts/vpn/classes.vpn-manager.js +3 -1
  14. package/dist_ts_interfaces/data/stats.d.ts +11 -0
  15. package/dist_ts_interfaces/requests/stats.d.ts +1 -0
  16. package/dist_ts_web/00_commitinfo_data.js +1 -1
  17. package/dist_ts_web/appstate.d.ts +1 -0
  18. package/dist_ts_web/appstate.js +4 -1
  19. package/dist_ts_web/elements/network/ops-view-network-activity.d.ts +2 -0
  20. package/dist_ts_web/elements/network/ops-view-network-activity.js +57 -1
  21. package/dist_ts_web/elements/network/ops-view-targetprofiles.js +4 -4
  22. package/package.json +6 -6
  23. package/readme.md +2 -2
  24. package/ts/00_commitinfo_data.ts +1 -1
  25. package/ts/classes.dcrouter.ts +11 -14
  26. package/ts/config/classes.route-config-manager.ts +25 -33
  27. package/ts/config/classes.target-profile-manager.ts +21 -211
  28. package/ts/monitoring/classes.metricsmanager.ts +65 -3
  29. package/ts/opsserver/handlers/security.handler.ts +2 -0
  30. package/ts/opsserver/handlers/stats.handler.ts +1 -0
  31. package/ts/vpn/classes.vpn-manager.ts +2 -0
  32. package/ts_web/00_commitinfo_data.ts +1 -1
  33. package/ts_web/appstate.ts +4 -0
  34. package/ts_web/elements/network/ops-view-network-activity.ts +59 -0
  35. package/ts_web/elements/network/ops-view-targetprofiles.ts +3 -3
@@ -11,8 +11,7 @@ import type { IDcRouterRouteConfig } from '../../ts_interfaces/data/remoteingres
11
11
  import { type IHttp3Config, augmentRouteWithHttp3 } from '../http3/index.js';
12
12
  import type { ReferenceResolver } from './classes.reference-resolver.js';
13
13
 
14
- /** An IP allow entry: plain IP/CIDR or domain-scoped. */
15
- export type TIpAllowEntry = string | { ip: string; domains: string[] };
14
+ export type TVpnClientAllowEntry = string | { clientId: string; domains: string[] };
16
15
 
17
16
  export interface IRouteMutationResult {
18
17
  success: boolean;
@@ -57,7 +56,7 @@ export class RouteConfigManager {
57
56
  constructor(
58
57
  private getSmartProxy: () => plugins.smartproxy.SmartProxy | undefined,
59
58
  private getHttp3Config?: () => IHttp3Config | undefined,
60
- private getVpnClientIpsForRoute?: (route: IDcRouterRouteConfig, routeId?: string) => TIpAllowEntry[],
59
+ private getVpnClientAccessForRoute?: (route: IDcRouterRouteConfig, routeId?: string) => TVpnClientAllowEntry[],
61
60
  private referenceResolver?: ReferenceResolver,
62
61
  private onRoutesApplied?: (routes: plugins.smartproxy.IRouteConfig[]) => void | Promise<void>,
63
62
  private getRuntimeRoutes?: () => plugins.smartproxy.IRouteConfig[],
@@ -73,10 +72,10 @@ export class RouteConfigManager {
73
72
  return this.routes.get(id);
74
73
  }
75
74
 
76
- public setVpnClientIpsResolver(
77
- resolver?: (route: IDcRouterRouteConfig, routeId?: string) => TIpAllowEntry[],
75
+ public setVpnClientAccessResolver(
76
+ resolver?: (route: IDcRouterRouteConfig, routeId?: string) => TVpnClientAllowEntry[],
78
77
  ): void {
79
- this.getVpnClientIpsForRoute = resolver;
78
+ this.getVpnClientAccessForRoute = resolver;
80
79
  }
81
80
 
82
81
  /**
@@ -608,49 +607,42 @@ export class RouteConfigManager {
608
607
  routeId?: string,
609
608
  ): plugins.smartproxy.IRouteConfig {
610
609
  const dcRoute = route as IDcRouterRouteConfig;
611
- const vpnEntries = this.getVpnClientIpsForRoute?.(dcRoute, routeId) || [];
610
+ const vpnEntries = this.getVpnClientAccessForRoute?.(dcRoute, routeId) || [];
612
611
 
613
- if (!dcRoute.vpnOnly) {
614
- const existingAllowList = route.security?.ipAllowList;
615
- if (!Array.isArray(existingAllowList) || existingAllowList.length === 0 || vpnEntries.length === 0) {
616
- return route;
617
- }
618
-
619
- return {
620
- ...route,
621
- security: {
622
- ...route.security,
623
- ipAllowList: this.mergeIpAllowEntries(existingAllowList as TIpAllowEntry[], vpnEntries),
624
- },
625
- };
612
+ if (!dcRoute.vpnOnly && vpnEntries.length === 0) {
613
+ return route;
626
614
  }
627
615
 
628
- const existingBlockList = route.security?.ipBlockList || [];
629
- const ipBlockList = vpnEntries.length
630
- ? existingBlockList
631
- : [...new Set([...existingBlockList, '*'])];
616
+ const existingVpnSecurity = route.security?.vpn || {};
617
+ const mergedAllowedClients = this.mergeVpnClientAllowEntries(
618
+ existingVpnSecurity.allowedClients || [],
619
+ vpnEntries,
620
+ );
632
621
 
633
622
  return {
634
623
  ...route,
635
624
  security: {
636
625
  ...route.security,
637
- ipAllowList: vpnEntries,
638
- ipBlockList,
626
+ vpn: {
627
+ ...existingVpnSecurity,
628
+ required: dcRoute.vpnOnly ? true : existingVpnSecurity.required,
629
+ allowedClients: mergedAllowedClients,
630
+ },
639
631
  },
640
632
  };
641
633
  }
642
634
 
643
- private mergeIpAllowEntries(
644
- existingEntries: TIpAllowEntry[],
645
- vpnEntries: TIpAllowEntry[],
646
- ): TIpAllowEntry[] {
647
- const merged: TIpAllowEntry[] = [];
635
+ private mergeVpnClientAllowEntries(
636
+ existingEntries: TVpnClientAllowEntry[],
637
+ vpnEntries: TVpnClientAllowEntry[],
638
+ ): TVpnClientAllowEntry[] {
639
+ const merged: TVpnClientAllowEntry[] = [];
648
640
  const seen = new Set<string>();
649
641
 
650
642
  for (const entry of [...existingEntries, ...vpnEntries]) {
651
643
  const key = typeof entry === 'string'
652
- ? `ip:${entry}`
653
- : `domain:${entry.ip}:${[...entry.domains].sort().join(',')}`;
644
+ ? `client:${entry}`
645
+ : `domain:${entry.clientId}:${[...entry.domains].sort().join(',')}`;
654
646
  if (seen.has(key)) continue;
655
647
  seen.add(key);
656
648
  merged.push(entry);
@@ -5,7 +5,7 @@ import type { ITargetProfile, ITargetProfileTarget } from '../../ts_interfaces/d
5
5
  import type { IDcRouterRouteConfig } from '../../ts_interfaces/data/remoteingress.js';
6
6
  import type { IRoute } from '../../ts_interfaces/data/route-management.js';
7
7
 
8
- type TIpAllowEntry = string | { ip: string; domains?: string[] };
8
+ type TVpnClientAllowEntry = string | { clientId: string; domains: string[] };
9
9
 
10
10
  /**
11
11
  * Manages TargetProfiles (target-side: what can be accessed).
@@ -206,37 +206,35 @@ export class TargetProfileManager {
206
206
  }
207
207
 
208
208
  // =========================================================================
209
- // Core matching: route → client IPs
209
+ // Core matching: route → VPN client grants
210
210
  // =========================================================================
211
211
 
212
212
  /**
213
- * For a vpnOnly route, find all enabled VPN clients whose assigned TargetProfile
214
- * matches the route. Returns IP allow entries for injection into ipAllowList.
213
+ * Find all enabled VPN clients whose assigned TargetProfile matches the route.
214
+ * Returns SmartProxy VPN client allow entries for authenticated metadata checks.
215
215
  *
216
216
  * Entries are domain-scoped when a profile matches via specific domains that are
217
217
  * a subset of the route's wildcard. Plain IPs are returned for routeRef/target matches
218
218
  * or when profile domains exactly equal the route's domains. Profiles can also opt
219
- * into source-IP matching against non-vpnOnly route security.
219
+ * into source-policy routes; SmartProxy evaluates the real source IP per connection.
220
220
  */
221
- public getMatchingClientIps(
221
+ public getMatchingVpnClients(
222
222
  route: IDcRouterRouteConfig,
223
223
  routeId: string | undefined,
224
224
  clients: VpnClientDoc[],
225
225
  allRoutes: Map<string, IRoute> = new Map(),
226
- clientSourceIps: Map<string, string> = new Map(),
227
- ): Array<string | { ip: string; domains: string[] }> {
228
- const entries: Array<string | { ip: string; domains: string[] }> = [];
226
+ ): TVpnClientAllowEntry[] {
227
+ const entries: TVpnClientAllowEntry[] = [];
229
228
  const routeDomains = this.getRouteDomains(route);
230
229
  const routeNameIndex = this.buildRouteNameIndex(allRoutes);
231
230
 
232
231
  for (const client of clients) {
233
- if (!client.enabled || !client.assignedIp) continue;
232
+ if (!client.enabled || !client.clientId) continue;
234
233
  if (!client.targetProfileIds?.length) continue;
235
234
 
236
235
  // Collect scoped domains from all matching profiles for this client
237
236
  let fullAccess = false;
238
237
  const scopedDomains = new Set<string>();
239
- const clientSourceIp = clientSourceIps.get(client.clientId);
240
238
 
241
239
  for (const profileId of client.targetProfileIds) {
242
240
  const profile = this.profiles.get(profileId);
@@ -258,10 +256,8 @@ export class TargetProfileManager {
258
256
  }
259
257
 
260
258
  if (
261
- !route.vpnOnly
262
- && profile.allowRoutesByClientSourceIp === true
263
- && clientSourceIp
264
- && this.routeAllowsSourceIp(route, clientSourceIp, routeDomains)
259
+ profile.allowRoutesByClientSourceIp === true
260
+ && this.routeHasSourcePolicy(route)
265
261
  ) {
266
262
  fullAccess = true;
267
263
  break;
@@ -269,9 +265,9 @@ export class TargetProfileManager {
269
265
  }
270
266
 
271
267
  if (fullAccess) {
272
- entries.push(client.assignedIp);
268
+ entries.push(client.clientId);
273
269
  } else if (scopedDomains.size > 0) {
274
- entries.push({ ip: client.assignedIp, domains: [...scopedDomains] });
270
+ entries.push({ clientId: client.clientId, domains: [...scopedDomains] });
275
271
  }
276
272
  }
277
273
 
@@ -285,7 +281,6 @@ export class TargetProfileManager {
285
281
  public getClientAccessSpec(
286
282
  targetProfileIds: string[],
287
283
  allRoutes: Map<string, IRoute>,
288
- clientSourceIp?: string,
289
284
  ): { domains: string[]; targetIps: string[] } {
290
285
  const domains = new Set<string>();
291
286
  const targetIps = new Set<string>();
@@ -322,9 +317,7 @@ export class TargetProfileManager {
322
317
  routeNameIndex,
323
318
  );
324
319
  const sourceIpMatchesRoute = profile.allowRoutesByClientSourceIp === true
325
- && clientSourceIp
326
- && !dcRoute.vpnOnly
327
- && this.routeAllowsSourceIp(dcRoute, clientSourceIp, routeDomains);
320
+ && this.routeHasSourcePolicy(dcRoute);
328
321
  if (profileMatchesRoute || sourceIpMatchesRoute) {
329
322
  for (const d of routeDomains) {
330
323
  domains.add(d);
@@ -450,197 +443,14 @@ export class TargetProfileManager {
450
443
  return false;
451
444
  }
452
445
 
453
- private routeAllowsSourceIp(
454
- route: IDcRouterRouteConfig,
455
- sourceIp: string,
456
- routeDomains: string[],
457
- ): boolean {
446
+ private routeHasSourcePolicy(route: IDcRouterRouteConfig): boolean {
458
447
  const security = (route as any).security;
459
- const ipAllowList = this.normalizeIpEntries(security?.ipAllowList);
460
- const ipBlockList = this.normalizeIpEntries(security?.ipBlockList);
461
-
462
- if (this.ipEntriesMatchSource(ipBlockList, sourceIp, routeDomains)) {
463
- return false;
464
- }
465
-
466
- if (!ipAllowList.length) {
467
- return true;
468
- }
469
-
470
- return this.ipEntriesMatchSource(ipAllowList, sourceIp, routeDomains);
471
- }
472
-
473
- private normalizeIpEntries(entries: unknown): TIpAllowEntry[] {
474
- if (!entries) return [];
475
- if (Array.isArray(entries)) return entries as TIpAllowEntry[];
476
- return [entries as TIpAllowEntry];
477
- }
478
-
479
- private ipEntriesMatchSource(
480
- entries: TIpAllowEntry[],
481
- sourceIp: string,
482
- routeDomains: string[],
483
- ): boolean {
484
- return entries.some((entry) => this.ipEntryMatchesSource(entry, sourceIp, routeDomains));
485
- }
486
-
487
- private ipEntryMatchesSource(
488
- entry: TIpAllowEntry,
489
- sourceIp: string,
490
- routeDomains: string[],
491
- ): boolean {
492
- const ipPattern = typeof entry === 'string' ? entry : entry.ip;
493
- if (typeof ipPattern !== 'string') return false;
494
- if (!this.ipPatternMatchesSource(ipPattern, sourceIp)) {
495
- return false;
496
- }
497
-
498
- if (typeof entry === 'string' || !entry.domains?.length) {
499
- return true;
500
- }
501
-
502
- if (!routeDomains.length) {
503
- return false;
504
- }
505
-
506
- return routeDomains.some((routeDomain) =>
507
- entry.domains!.some((entryDomain) =>
508
- this.domainMatchesPattern(routeDomain, entryDomain)
509
- || this.domainMatchesPattern(entryDomain, routeDomain),
510
- ),
511
- );
512
- }
513
-
514
- private ipPatternMatchesSource(pattern: string, sourceIp: string): boolean {
515
- const trimmedPattern = pattern.trim();
516
- const trimmedSourceIp = sourceIp.trim();
517
- if (!trimmedPattern || !trimmedSourceIp) return false;
518
- if (trimmedPattern === '*') return true;
519
- if (trimmedPattern === trimmedSourceIp) return true;
520
-
521
- if (trimmedPattern.includes('/')) {
522
- return this.ipMatchesCidr(trimmedSourceIp, trimmedPattern);
523
- }
524
-
525
- if (trimmedPattern.includes('-')) {
526
- return this.ipMatchesRange(trimmedSourceIp, trimmedPattern);
527
- }
528
-
529
- if (trimmedPattern.includes('*')) {
530
- return this.ipMatchesWildcard(trimmedSourceIp, trimmedPattern);
531
- }
532
-
533
- return false;
534
- }
535
-
536
- private ipMatchesCidr(sourceIp: string, cidr: string): boolean {
537
- const [networkIp, prefixString] = cidr.split('/');
538
- if (!networkIp || !prefixString) return false;
539
- const source = this.ipToComparable(sourceIp);
540
- const network = this.ipToComparable(networkIp);
541
- const prefix = Number(prefixString);
542
- if (!source || !network || source.version !== network.version) return false;
543
-
544
- const bitCount = source.version === 4 ? 32 : 128;
545
- if (!Number.isInteger(prefix) || prefix < 0 || prefix > bitCount) return false;
546
- if (prefix === 0) return true;
547
-
548
- const shift = BigInt(bitCount - prefix);
549
- return (source.value >> shift) === (network.value >> shift);
550
- }
551
-
552
- private ipMatchesRange(sourceIp: string, range: string): boolean {
553
- const [startIp, endIp] = range.split('-').map((part) => part.trim());
554
- if (!startIp || !endIp) return false;
555
- const source = this.ipToComparable(sourceIp);
556
- const start = this.ipToComparable(startIp);
557
- const end = this.ipToComparable(endIp);
558
- if (!source || !start || !end) return false;
559
- if (source.version !== start.version || source.version !== end.version) return false;
560
- return source.value >= start.value && source.value <= end.value;
561
- }
562
-
563
- private ipMatchesWildcard(sourceIp: string, pattern: string): boolean {
564
- const sourceParts = sourceIp.split('.');
565
- const patternParts = pattern.split('.');
566
- if (sourceParts.length !== 4 || patternParts.length !== 4) return false;
567
-
568
- return patternParts.every((patternPart, index) => {
569
- if (patternPart === '*') return true;
570
- return patternPart === sourceParts[index];
571
- });
572
- }
573
-
574
- private ipToComparable(ip: string): { version: 4 | 6; value: bigint } | undefined {
575
- const normalizedIp = this.normalizeIpLiteral(ip);
576
- const ipVersion = plugins.net.isIP(normalizedIp);
577
- if (ipVersion === 4) {
578
- const parts = normalizedIp.split('.').map((part) => Number(part));
579
- if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
580
- return undefined;
581
- }
582
- return {
583
- version: 4,
584
- value: parts.reduce((value, part) => (value << 8n) + BigInt(part), 0n),
585
- };
586
- }
587
-
588
- if (ipVersion === 6) {
589
- const parts = this.expandIpv6(normalizedIp);
590
- if (!parts) return undefined;
591
- return {
592
- version: 6,
593
- value: parts.reduce((value, part) => (value << 16n) + BigInt(part), 0n),
594
- };
595
- }
596
-
597
- return undefined;
598
- }
599
-
600
- private normalizeIpLiteral(ip: string): string {
601
- const trimmed = ip.trim().replace(/^\[|\]$/g, '');
602
- const zoneIndex = trimmed.indexOf('%');
603
- const withoutZone = zoneIndex === -1 ? trimmed : trimmed.slice(0, zoneIndex);
604
- const ipv4MappedPrefix = '::ffff:';
605
- if (withoutZone.toLowerCase().startsWith(ipv4MappedPrefix)) {
606
- const mappedIpv4 = withoutZone.slice(ipv4MappedPrefix.length);
607
- if (plugins.net.isIP(mappedIpv4) === 4) return mappedIpv4;
608
- }
609
- return withoutZone;
610
- }
611
-
612
- private expandIpv6(ip: string): number[] | undefined {
613
- let normalizedIp = ip.toLowerCase();
614
- if (normalizedIp.includes('.')) {
615
- const lastColonIndex = normalizedIp.lastIndexOf(':');
616
- const ipv4Part = normalizedIp.slice(lastColonIndex + 1);
617
- const ipv4Comparable = this.ipToComparable(ipv4Part);
618
- if (!ipv4Comparable || ipv4Comparable.version !== 4) return undefined;
619
- const high = Number((ipv4Comparable.value >> 16n) & 0xffffn).toString(16);
620
- const low = Number(ipv4Comparable.value & 0xffffn).toString(16);
621
- normalizedIp = `${normalizedIp.slice(0, lastColonIndex)}:${high}:${low}`;
622
- }
623
-
624
- const doubleColonParts = normalizedIp.split('::');
625
- if (doubleColonParts.length > 2) return undefined;
626
-
627
- const head = doubleColonParts[0] ? doubleColonParts[0].split(':') : [];
628
- const tail = doubleColonParts[1] ? doubleColonParts[1].split(':') : [];
629
- const missingCount = 8 - head.length - tail.length;
630
- if (missingCount < 0 || (doubleColonParts.length === 1 && missingCount !== 0)) return undefined;
631
-
632
- const parts = [
633
- ...head,
634
- ...Array(missingCount).fill('0'),
635
- ...tail,
636
- ];
637
- if (parts.length !== 8) return undefined;
638
-
639
- const numbers = parts.map((part) => Number.parseInt(part || '0', 16));
640
- if (numbers.some((part) => !Number.isInteger(part) || part < 0 || part > 0xffff)) {
641
- return undefined;
642
- }
643
- return numbers;
448
+ const blockEntries = Array.isArray(security?.ipBlockList)
449
+ ? security.ipBlockList
450
+ : security?.ipBlockList
451
+ ? [security.ipBlockList]
452
+ : [];
453
+ return !blockEntries.some((entry: unknown) => typeof entry === 'string' && entry.trim() === '*');
644
454
  }
645
455
 
646
456
  private getRouteDomains(route: IDcRouterRouteConfig): string[] {
@@ -3,6 +3,7 @@ import { DcRouter } from '../classes.dcrouter.js';
3
3
  import { MetricsCache } from './classes.metricscache.js';
4
4
  import { SecurityLogger, SecurityEventType } from '../security/classes.securitylogger.js';
5
5
  import { logger } from '../logger.js';
6
+ import type { IAsnActivity } from '../../ts_interfaces/data/stats.js';
6
7
 
7
8
  export class MetricsManager {
8
9
  private metricsLogger: plugins.smartlog.Smartlog;
@@ -545,7 +546,7 @@ export class MetricsManager {
545
546
  // Get network metrics from SmartProxy
546
547
  public async getNetworkStats() {
547
548
  // Use shorter cache TTL for network stats to ensure real-time updates
548
- return this.metricsCache.get('networkStats', () => {
549
+ return this.metricsCache.get('networkStats', async () => {
549
550
  const proxyMetrics = this.dcRouter.smartProxy ? this.dcRouter.smartProxy.getMetrics() : null;
550
551
 
551
552
  if (!proxyMetrics) {
@@ -554,6 +555,7 @@ export class MetricsManager {
554
555
  throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
555
556
  topIPs: [] as Array<{ ip: string; count: number }>,
556
557
  topIPsByBandwidth: [] as Array<{ ip: string; count: number; bwIn: number; bwOut: number }>,
558
+ topASNs: [] as IAsnActivity[],
557
559
  totalDataTransferred: { bytesIn: 0, bytesOut: 0 },
558
560
  throughputHistory: [] as Array<{ timestamp: number; in: number; out: number }>,
559
561
  throughputByIP: new Map<string, { in: number; out: number }>(),
@@ -725,10 +727,15 @@ export class MetricsManager {
725
727
  .slice(0, 10)
726
728
  .map(([ip, data]) => ({ ip, count: data.count, bwIn: data.bwIn, bwOut: data.bwOut }));
727
729
 
728
- this.dcRouter.securityPolicyManager?.queueObservedIps([
730
+ const observedIps = [...new Set([
731
+ ...connectionsByIP.keys(),
732
+ ...throughputByIP.keys(),
729
733
  ...topIPs.map((item) => item.ip),
730
734
  ...topIPsByBandwidth.map((item) => item.ip),
731
- ]);
735
+ ])];
736
+ this.dcRouter.securityPolicyManager?.queueObservedIps(observedIps);
737
+
738
+ const topASNs = await this.buildTopASNs(observedIps, allIPData);
732
739
 
733
740
  // Build domain activity using per-IP domain request counts from Rust engine
734
741
  const connectionsByRoute = proxyMetrics.connections.byRoute();
@@ -872,6 +879,7 @@ export class MetricsManager {
872
879
  throughputRate,
873
880
  topIPs,
874
881
  topIPsByBandwidth,
882
+ topASNs,
875
883
  totalDataTransferred,
876
884
  throughputHistory,
877
885
  throughputByIP,
@@ -885,6 +893,60 @@ export class MetricsManager {
885
893
  }, 1000); // 1s cache — matches typical dashboard poll interval
886
894
  }
887
895
 
896
+ private async buildTopASNs(
897
+ observedIps: string[],
898
+ allIPData: Map<string, { count: number; bwIn: number; bwOut: number }>,
899
+ ): Promise<IAsnActivity[]> {
900
+ const manager = this.dcRouter.securityPolicyManager;
901
+ if (!manager || observedIps.length === 0) {
902
+ return [];
903
+ }
904
+
905
+ const intelligenceRecords = await manager.listIpIntelligence({
906
+ ipAddresses: observedIps,
907
+ limit: Math.max(100, observedIps.length),
908
+ });
909
+ const asnActivity = new Map<number, IAsnActivity>();
910
+
911
+ for (const record of intelligenceRecords) {
912
+ if (typeof record.asn !== 'number') continue;
913
+
914
+ const ipData = allIPData.get(record.ipAddress);
915
+ if (!ipData) continue;
916
+
917
+ const existing = asnActivity.get(record.asn);
918
+ const activity = existing || {
919
+ asn: record.asn,
920
+ organization: record.asnOrg || record.registrantOrg || `AS${record.asn}`,
921
+ country: record.countryCode || record.country || record.registrantCountry || null,
922
+ activeConnections: 0,
923
+ ipCount: 0,
924
+ bytesInPerSecond: 0,
925
+ bytesOutPerSecond: 0,
926
+ sampleIps: [],
927
+ };
928
+
929
+ activity.activeConnections += ipData.count;
930
+ activity.bytesInPerSecond += ipData.bwIn;
931
+ activity.bytesOutPerSecond += ipData.bwOut;
932
+ activity.ipCount++;
933
+ if (activity.sampleIps.length < 5) {
934
+ activity.sampleIps.push(record.ipAddress);
935
+ }
936
+ asnActivity.set(record.asn, activity);
937
+ }
938
+
939
+ return [...asnActivity.values()]
940
+ .sort((a, b) => {
941
+ const connectionDiff = b.activeConnections - a.activeConnections;
942
+ if (connectionDiff !== 0) return connectionDiff;
943
+ const bandwidthA = a.bytesInPerSecond + a.bytesOutPerSecond;
944
+ const bandwidthB = b.bytesInPerSecond + b.bytesOutPerSecond;
945
+ return bandwidthB - bandwidthA;
946
+ })
947
+ .slice(0, 10);
948
+ }
949
+
888
950
  // --- Time-series helpers ---
889
951
 
890
952
  private static minuteKey(ts: number = Date.now()): number {
@@ -103,6 +103,7 @@ export class SecurityHandler {
103
103
  throughputRate: networkStats.throughputRate,
104
104
  topIPs: networkStats.topIPs,
105
105
  topIPsByBandwidth: networkStats.topIPsByBandwidth,
106
+ topASNs: networkStats.topASNs,
106
107
  totalDataTransferred: networkStats.totalDataTransferred,
107
108
  throughputHistory: networkStats.throughputHistory || [],
108
109
  throughputByIP,
@@ -121,6 +122,7 @@ export class SecurityHandler {
121
122
  throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
122
123
  topIPs: [],
123
124
  topIPsByBandwidth: [],
125
+ topASNs: [],
124
126
  totalDataTransferred: { bytesIn: 0, bytesOut: 0 },
125
127
  throughputHistory: [],
126
128
  throughputByIP: [],
@@ -334,6 +334,7 @@ export class StatsHandler {
334
334
  connections: ip.count,
335
335
  bandwidth: { in: ip.bwIn, out: ip.bwOut },
336
336
  })),
337
+ topASNs: stats.topASNs || [],
337
338
  domainActivity: stats.domainActivity || [],
338
339
  throughputHistory: stats.throughputHistory || [],
339
340
  requestsPerSecond: stats.requestsPerSecond || 0,
@@ -152,6 +152,8 @@ export class VpnManager {
152
152
  wgListenPort,
153
153
  clients: clientEntries,
154
154
  socketForwardProxyProtocol: !isBridge,
155
+ socketForwardProxyProtocolSource: 'remoteIp',
156
+ socketForwardProxyProtocolVpnMetadata: true,
155
157
  destinationPolicy: this.getServerDestinationPolicy(forwardingMode, defaultDestinationPolicy),
156
158
  serverEndpoint,
157
159
  clientAllowedIPs: [subnet],
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '13.34.0',
6
+ version: '13.36.0',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }
@@ -55,6 +55,7 @@ export interface INetworkState {
55
55
  totalBytes: { in: number; out: number };
56
56
  topIPs: Array<{ ip: string; count: number }>;
57
57
  topIPsByBandwidth: Array<{ ip: string; count: number; bwIn: number; bwOut: number }>;
58
+ topASNs: interfaces.data.IAsnActivity[];
58
59
  throughputByIP: Array<{ ip: string; in: number; out: number }>;
59
60
  ipIntelligence: interfaces.data.IIpIntelligenceRecord[];
60
61
  domainActivity: interfaces.data.IDomainActivity[];
@@ -176,6 +177,7 @@ export const networkStatePart = await appState.getStatePart<INetworkState>(
176
177
  totalBytes: { in: 0, out: 0 },
177
178
  topIPs: [],
178
179
  topIPsByBandwidth: [],
180
+ topASNs: [],
179
181
  throughputByIP: [],
180
182
  ipIntelligence: [],
181
183
  domainActivity: [],
@@ -689,6 +691,7 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
689
691
  : { in: 0, out: 0 },
690
692
  topIPs: networkStatsResponse.topIPs || [],
691
693
  topIPsByBandwidth: networkStatsResponse.topIPsByBandwidth || [],
694
+ topASNs: networkStatsResponse.topASNs || [],
692
695
  throughputByIP: networkStatsResponse.throughputByIP || [],
693
696
  ipIntelligence: currentState.ipIntelligence,
694
697
  domainActivity: networkStatsResponse.domainActivity || [],
@@ -3152,6 +3155,7 @@ async function dispatchCombinedRefreshActionInner() {
3152
3155
  bwIn: e.bandwidth?.in || 0,
3153
3156
  bwOut: e.bandwidth?.out || 0,
3154
3157
  })),
3158
+ topASNs: network.topASNs || [],
3155
3159
  throughputByIP: network.topEndpoints.map(e => ({ ip: e.endpoint, in: e.bandwidth?.in || 0, out: e.bandwidth?.out || 0 })),
3156
3160
  domainActivity: network.domainActivity || [],
3157
3161
  throughputHistory: network.throughputHistory || [],
@@ -308,6 +308,9 @@ export class OpsViewNetworkActivity extends DeesElement {
308
308
  <!-- Top IPs by Connection Count -->
309
309
  ${this.renderTopIPs()}
310
310
 
311
+ <!-- Top ASNs by Connection Count -->
312
+ ${this.renderTopASNs()}
313
+
311
314
  <!-- Top IPs by Bandwidth -->
312
315
  ${this.renderTopIPsByBandwidth()}
313
316
 
@@ -450,6 +453,28 @@ export class OpsViewNetworkActivity extends DeesElement {
450
453
  ];
451
454
  }
452
455
 
456
+ private getAsnDataActions() {
457
+ return [
458
+ {
459
+ name: 'Block ASN',
460
+ iconName: 'lucide:radio-tower',
461
+ type: ['inRow', 'contextmenu'] as any,
462
+ actionFunc: async (actionData: any) => {
463
+ await this.createBlockRuleDialog('asn', String(actionData.item.asn), 'Blocked ASN from Network Activity');
464
+ },
465
+ },
466
+ {
467
+ name: 'Block Organization',
468
+ iconName: 'lucide:building-2',
469
+ type: ['contextmenu'] as any,
470
+ actionRelevancyCheckFunc: (actionData: any) => Boolean(actionData.item.organization),
471
+ actionFunc: async (actionData: any) => {
472
+ await this.createBlockRuleDialog('organization', actionData.item.organization, 'Blocked organization from Network Activity');
473
+ },
474
+ },
475
+ ];
476
+ }
477
+
453
478
  private calculateThroughput(): { in: number; out: number } {
454
479
  // Use real throughput data from network state
455
480
  return {
@@ -619,6 +644,40 @@ export class OpsViewNetworkActivity extends DeesElement {
619
644
  `;
620
645
  }
621
646
 
647
+ private renderTopASNs(): TemplateResult {
648
+ if (!this.networkState.topASNs || this.networkState.topASNs.length === 0) {
649
+ return html``;
650
+ }
651
+
652
+ return html`
653
+ <dees-table
654
+ .data=${this.networkState.topASNs}
655
+ .rowKey=${'asn'}
656
+ .highlightUpdates=${'flash'}
657
+ .displayFunction=${(asnData: appstate.INetworkState['topASNs'][number]) => {
658
+ return {
659
+ 'ASN': `AS${asnData.asn}`,
660
+ 'Organization': this.formatOptional(asnData.organization),
661
+ 'Connections': asnData.activeConnections,
662
+ 'IPs': asnData.ipCount,
663
+ 'Bandwidth In': this.formatBitsPerSecond(asnData.bytesInPerSecond),
664
+ 'Bandwidth Out': this.formatBitsPerSecond(asnData.bytesOutPerSecond),
665
+ 'Total Bandwidth': this.formatBitsPerSecond(asnData.bytesInPerSecond + asnData.bytesOutPerSecond),
666
+ 'Country': this.formatOptional(asnData.country),
667
+ 'Sample IPs': asnData.sampleIps.join(', '),
668
+ };
669
+ }}
670
+ .dataActions=${this.getAsnDataActions()}
671
+ heading1="Top Connected ASNs"
672
+ heading2="Organizations causing the most active connections across observed IPs"
673
+ searchable
674
+ .showColumnFilters=${true}
675
+ .pagination=${false}
676
+ dataName="ASN"
677
+ ></dees-table>
678
+ `;
679
+ }
680
+
622
681
  private renderTopIPsByBandwidth(): TemplateResult {
623
682
  if (!this.networkState.topIPsByBandwidth || this.networkState.topIPsByBandwidth.length === 0) {
624
683
  return html``;