@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.
- package/dist_serve/bundle.js +22 -5
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.d.ts +1 -1
- package/dist_ts/classes.dcrouter.js +11 -12
- package/dist_ts/config/classes.route-config-manager.d.ts +6 -7
- package/dist_ts/config/classes.route-config-manager.js +19 -28
- package/dist_ts/config/classes.target-profile-manager.d.ts +11 -19
- package/dist_ts/config/classes.target-profile-manager.js +20 -184
- package/dist_ts/monitoring/classes.metricsmanager.d.ts +4 -0
- package/dist_ts/monitoring/classes.metricsmanager.js +59 -6
- package/dist_ts/opsserver/handlers/security.handler.js +3 -1
- package/dist_ts/opsserver/handlers/stats.handler.js +2 -1
- package/dist_ts/vpn/classes.vpn-manager.js +3 -1
- package/dist_ts_interfaces/data/stats.d.ts +11 -0
- package/dist_ts_interfaces/requests/stats.d.ts +1 -0
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/appstate.d.ts +1 -0
- package/dist_ts_web/appstate.js +4 -1
- package/dist_ts_web/elements/network/ops-view-network-activity.d.ts +2 -0
- package/dist_ts_web/elements/network/ops-view-network-activity.js +57 -1
- package/dist_ts_web/elements/network/ops-view-targetprofiles.js +4 -4
- package/package.json +6 -6
- package/readme.md +2 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +11 -14
- package/ts/config/classes.route-config-manager.ts +25 -33
- package/ts/config/classes.target-profile-manager.ts +21 -211
- package/ts/monitoring/classes.metricsmanager.ts +65 -3
- package/ts/opsserver/handlers/security.handler.ts +2 -0
- package/ts/opsserver/handlers/stats.handler.ts +1 -0
- package/ts/vpn/classes.vpn-manager.ts +2 -0
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/appstate.ts +4 -0
- package/ts_web/elements/network/ops-view-network-activity.ts +59 -0
- 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
|
-
|
|
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
|
|
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
|
|
77
|
-
resolver?: (route: IDcRouterRouteConfig, routeId?: string) =>
|
|
75
|
+
public setVpnClientAccessResolver(
|
|
76
|
+
resolver?: (route: IDcRouterRouteConfig, routeId?: string) => TVpnClientAllowEntry[],
|
|
78
77
|
): void {
|
|
79
|
-
this.
|
|
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.
|
|
610
|
+
const vpnEntries = this.getVpnClientAccessForRoute?.(dcRoute, routeId) || [];
|
|
612
611
|
|
|
613
|
-
if (!dcRoute.vpnOnly) {
|
|
614
|
-
|
|
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
|
|
629
|
-
const
|
|
630
|
-
|
|
631
|
-
|
|
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
|
-
|
|
638
|
-
|
|
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
|
|
644
|
-
existingEntries:
|
|
645
|
-
vpnEntries:
|
|
646
|
-
):
|
|
647
|
-
const merged:
|
|
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
|
-
? `
|
|
653
|
-
: `domain:${entry.
|
|
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
|
|
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
|
|
209
|
+
// Core matching: route → VPN client grants
|
|
210
210
|
// =========================================================================
|
|
211
211
|
|
|
212
212
|
/**
|
|
213
|
-
*
|
|
214
|
-
*
|
|
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-
|
|
219
|
+
* into source-policy routes; SmartProxy evaluates the real source IP per connection.
|
|
220
220
|
*/
|
|
221
|
-
public
|
|
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
|
-
|
|
227
|
-
|
|
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.
|
|
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
|
-
|
|
262
|
-
&&
|
|
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.
|
|
268
|
+
entries.push(client.clientId);
|
|
273
269
|
} else if (scopedDomains.size > 0) {
|
|
274
|
-
entries.push({
|
|
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
|
-
&&
|
|
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
|
|
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
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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
|
-
|
|
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],
|
package/ts_web/appstate.ts
CHANGED
|
@@ -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``;
|