@serve.zone/dcrouter 13.32.1 → 13.34.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 +597 -590
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.js +9 -4
- package/dist_ts/config/classes.target-profile-manager.d.ts +16 -3
- package/dist_ts/config/classes.target-profile-manager.js +197 -6
- package/dist_ts/db/documents/classes.target-profile.doc.d.ts +1 -0
- package/dist_ts/db/documents/classes.target-profile.doc.js +8 -2
- package/dist_ts/monitoring/classes.metricsmanager.js +5 -2
- package/dist_ts/opsserver/handlers/security.handler.js +9 -2
- package/dist_ts/opsserver/handlers/target-profile.handler.js +3 -1
- package/dist_ts/opsserver/handlers/vpn.handler.js +3 -1
- package/dist_ts/security/classes.security-policy-manager.d.ts +15 -1
- package/dist_ts/security/classes.security-policy-manager.js +108 -9
- package/dist_ts/vpn/classes.vpn-manager.d.ts +15 -1
- package/dist_ts/vpn/classes.vpn-manager.js +138 -6
- package/dist_ts_interfaces/data/target-profile.d.ts +2 -0
- package/dist_ts_interfaces/data/vpn.d.ts +4 -0
- package/dist_ts_interfaces/requests/security-policy.d.ts +2 -0
- package/dist_ts_interfaces/requests/target-profiles.d.ts +2 -0
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/appstate.d.ts +2 -0
- package/dist_ts_web/appstate.js +77 -47
- package/dist_ts_web/elements/network/ops-view-targetprofiles.js +10 -1
- package/dist_ts_web/elements/network/ops-view-vpn.js +3 -1
- package/package.json +2 -2
- package/readme.md +13 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +10 -2
- package/ts/config/classes.target-profile-manager.ts +229 -5
- package/ts/db/documents/classes.target-profile.doc.ts +3 -0
- package/ts/monitoring/classes.metricsmanager.ts +4 -1
- package/ts/opsserver/handlers/security.handler.ts +8 -1
- package/ts/opsserver/handlers/target-profile.handler.ts +2 -0
- package/ts/opsserver/handlers/vpn.handler.ts +2 -0
- package/ts/security/classes.security-policy-manager.ts +115 -7
- package/ts/vpn/classes.vpn-manager.ts +158 -3
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/appstate.ts +86 -48
- package/ts_web/elements/network/ops-view-targetprofiles.ts +9 -0
- package/ts_web/elements/network/ops-view-vpn.ts +2 -0
|
@@ -19,6 +19,10 @@ export interface IVpnManagerConfig {
|
|
|
19
19
|
}>;
|
|
20
20
|
/** Called when clients are created/deleted/toggled — triggers route re-application */
|
|
21
21
|
onClientChanged?: () => void;
|
|
22
|
+
/** Called when a live VPN client's real source IP changes. */
|
|
23
|
+
onClientSourceIpsChanged?: () => void;
|
|
24
|
+
/** Poll interval for live VPN client real source IP updates. Default: 10 seconds. */
|
|
25
|
+
clientSourceIpPollIntervalMs?: number;
|
|
22
26
|
/** Destination routing policy override. Default: forceTarget to 127.0.0.1 */
|
|
23
27
|
destinationPolicy?: {
|
|
24
28
|
default: 'forceTarget' | 'block' | 'allow';
|
|
@@ -29,7 +33,7 @@ export interface IVpnManagerConfig {
|
|
|
29
33
|
/** Compute per-client AllowedIPs based on the client's target profile IDs.
|
|
30
34
|
* Called at config generation time (create/export). Returns CIDRs for WireGuard AllowedIPs.
|
|
31
35
|
* When not set, defaults to [subnet]. */
|
|
32
|
-
getClientAllowedIPs?: (targetProfileIds: string[]) => Promise<string[]>;
|
|
36
|
+
getClientAllowedIPs?: (targetProfileIds: string[], clientId?: string, sourceIp?: string) => Promise<string[]>;
|
|
33
37
|
/** Resolve per-client destination allow-list IPs from target profile IDs.
|
|
34
38
|
* Returns IP strings that should bypass forceTarget and go direct to the real destination. */
|
|
35
39
|
getClientDirectTargets?: (targetProfileIds: string[]) => string[];
|
|
@@ -57,6 +61,9 @@ export class VpnManager {
|
|
|
57
61
|
private serverKeys?: VpnServerKeysDoc;
|
|
58
62
|
private resolvedForwardingMode?: 'socket' | 'bridge' | 'hybrid';
|
|
59
63
|
private forwardingModeOverride?: 'socket' | 'bridge' | 'hybrid';
|
|
64
|
+
private clientSourceIps = new Map<string, string>();
|
|
65
|
+
private clientSourceIpPollTimer?: ReturnType<typeof setInterval>;
|
|
66
|
+
private clientSourceIpRefreshInFlight = false;
|
|
60
67
|
|
|
61
68
|
constructor(config: IVpnManagerConfig) {
|
|
62
69
|
this.config = config;
|
|
@@ -173,6 +180,9 @@ export class VpnManager {
|
|
|
173
180
|
}
|
|
174
181
|
}
|
|
175
182
|
|
|
183
|
+
await this.refreshClientSourceIps(false);
|
|
184
|
+
this.startClientSourceIpPolling();
|
|
185
|
+
|
|
176
186
|
logger.log('info', `VPN server started: subnet=${subnet}, wg=:${wgListenPort}, clients=${this.clients.size}`);
|
|
177
187
|
}
|
|
178
188
|
|
|
@@ -180,6 +190,7 @@ export class VpnManager {
|
|
|
180
190
|
* Stop the VPN server.
|
|
181
191
|
*/
|
|
182
192
|
public async stop(): Promise<void> {
|
|
193
|
+
this.stopClientSourceIpPolling();
|
|
183
194
|
if (this.vpnServer) {
|
|
184
195
|
try {
|
|
185
196
|
await this.vpnServer.stopServer();
|
|
@@ -189,6 +200,11 @@ export class VpnManager {
|
|
|
189
200
|
await this.vpnServer.stop();
|
|
190
201
|
this.vpnServer = undefined;
|
|
191
202
|
}
|
|
203
|
+
const hadClientSourceIps = this.clientSourceIps.size > 0;
|
|
204
|
+
this.clientSourceIps.clear();
|
|
205
|
+
if (hadClientSourceIps) {
|
|
206
|
+
this.config.onClientSourceIpsChanged?.();
|
|
207
|
+
}
|
|
192
208
|
this.resolvedForwardingMode = undefined;
|
|
193
209
|
logger.log('info', 'VPN server stopped');
|
|
194
210
|
}
|
|
@@ -246,6 +262,7 @@ export class VpnManager {
|
|
|
246
262
|
bundle.wireguardConfig = await this.rewriteWireGuardAllowedIPs(
|
|
247
263
|
bundle.wireguardConfig,
|
|
248
264
|
doc.targetProfileIds || [],
|
|
265
|
+
doc.clientId,
|
|
249
266
|
);
|
|
250
267
|
|
|
251
268
|
// Persist client entry (including WG private key for export/QR)
|
|
@@ -287,6 +304,7 @@ export class VpnManager {
|
|
|
287
304
|
await this.vpnServer.removeClient(clientId);
|
|
288
305
|
const doc = this.clients.get(clientId);
|
|
289
306
|
this.clients.delete(clientId);
|
|
307
|
+
this.clientSourceIps.delete(clientId);
|
|
290
308
|
if (doc) {
|
|
291
309
|
await doc.delete();
|
|
292
310
|
}
|
|
@@ -328,6 +346,7 @@ export class VpnManager {
|
|
|
328
346
|
client.updatedAt = Date.now();
|
|
329
347
|
await this.persistClient(client);
|
|
330
348
|
}
|
|
349
|
+
this.clientSourceIps.delete(clientId);
|
|
331
350
|
this.config.onClientChanged?.();
|
|
332
351
|
}
|
|
333
352
|
|
|
@@ -380,6 +399,7 @@ export class VpnManager {
|
|
|
380
399
|
bundle.wireguardConfig = await this.rewriteWireGuardAllowedIPs(
|
|
381
400
|
bundle.wireguardConfig,
|
|
382
401
|
client?.targetProfileIds || [],
|
|
402
|
+
clientId,
|
|
383
403
|
);
|
|
384
404
|
|
|
385
405
|
// Update persisted entry with new keys (including private key for export/QR)
|
|
@@ -413,7 +433,11 @@ export class VpnManager {
|
|
|
413
433
|
);
|
|
414
434
|
}
|
|
415
435
|
|
|
416
|
-
config = await this.rewriteWireGuardAllowedIPs(
|
|
436
|
+
config = await this.rewriteWireGuardAllowedIPs(
|
|
437
|
+
config,
|
|
438
|
+
persisted?.targetProfileIds || [],
|
|
439
|
+
clientId,
|
|
440
|
+
);
|
|
417
441
|
}
|
|
418
442
|
|
|
419
443
|
return config;
|
|
@@ -445,6 +469,107 @@ export class VpnManager {
|
|
|
445
469
|
return this.vpnServer.listClients();
|
|
446
470
|
}
|
|
447
471
|
|
|
472
|
+
public getClientSourceIp(clientId: string): string | undefined {
|
|
473
|
+
return this.clientSourceIps.get(clientId);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
public getClientSourceIpMap(): Map<string, string> {
|
|
477
|
+
return new Map(this.clientSourceIps);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
public async refreshClientSourceIps(notifyOnChange = true): Promise<boolean> {
|
|
481
|
+
if (!this.vpnServer || this.clientSourceIpRefreshInFlight) {
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
this.clientSourceIpRefreshInFlight = true;
|
|
486
|
+
try {
|
|
487
|
+
const connectedClients = await this.vpnServer.listClients();
|
|
488
|
+
const nextSourceIps = new Map<string, string>();
|
|
489
|
+
const wireguardClientIds = new Set<string>();
|
|
490
|
+
|
|
491
|
+
for (const connectedClient of connectedClients) {
|
|
492
|
+
const clientId = connectedClient.registeredClientId || connectedClient.clientId;
|
|
493
|
+
if (!clientId) continue;
|
|
494
|
+
if (connectedClient.transportType === 'wireguard') {
|
|
495
|
+
wireguardClientIds.add(clientId);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const sourceIp = VpnManager.normalizeRemoteAddress(connectedClient.remoteAddr);
|
|
499
|
+
if (sourceIp) {
|
|
500
|
+
nextSourceIps.set(clientId, sourceIp);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (wireguardClientIds.size > 0 && typeof (this.vpnServer as any).listWgPeers === 'function') {
|
|
505
|
+
try {
|
|
506
|
+
const wgPeers = await this.vpnServer.listWgPeers();
|
|
507
|
+
const endpointByPublicKey = new Map<string, string>();
|
|
508
|
+
for (const peer of wgPeers) {
|
|
509
|
+
const endpointIp = VpnManager.normalizeRemoteAddress(peer.endpoint);
|
|
510
|
+
if (peer.publicKey && endpointIp) {
|
|
511
|
+
endpointByPublicKey.set(peer.publicKey, endpointIp);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
for (const client of this.clients.values()) {
|
|
516
|
+
if (nextSourceIps.has(client.clientId)) continue;
|
|
517
|
+
if (!wireguardClientIds.has(client.clientId)) continue;
|
|
518
|
+
if (!client.wgPublicKey) continue;
|
|
519
|
+
const endpointIp = endpointByPublicKey.get(client.wgPublicKey);
|
|
520
|
+
if (endpointIp) {
|
|
521
|
+
nextSourceIps.set(client.clientId, endpointIp);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
} catch (err) {
|
|
525
|
+
logger.log('warn', `VPN: Failed to refresh WireGuard peer endpoints: ${(err as Error).message}`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (this.sameSourceIpMap(this.clientSourceIps, nextSourceIps)) {
|
|
530
|
+
return false;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
this.clientSourceIps = nextSourceIps;
|
|
534
|
+
if (notifyOnChange) {
|
|
535
|
+
this.config.onClientSourceIpsChanged?.();
|
|
536
|
+
}
|
|
537
|
+
return true;
|
|
538
|
+
} catch (err) {
|
|
539
|
+
logger.log('warn', `VPN: Failed to refresh client source IPs: ${(err as Error).message}`);
|
|
540
|
+
return false;
|
|
541
|
+
} finally {
|
|
542
|
+
this.clientSourceIpRefreshInFlight = false;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
public static normalizeRemoteAddress(remoteAddress?: string): string | undefined {
|
|
547
|
+
const remoteAddressString = remoteAddress?.trim();
|
|
548
|
+
if (!remoteAddressString) return undefined;
|
|
549
|
+
|
|
550
|
+
if (remoteAddressString.startsWith('[')) {
|
|
551
|
+
const closingBracketIndex = remoteAddressString.indexOf(']');
|
|
552
|
+
if (closingBracketIndex > 0) {
|
|
553
|
+
const bracketedIp = remoteAddressString.slice(1, closingBracketIndex);
|
|
554
|
+
return plugins.net.isIP(bracketedIp) ? bracketedIp : undefined;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (plugins.net.isIP(remoteAddressString)) {
|
|
559
|
+
return remoteAddressString;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const lastColonIndex = remoteAddressString.lastIndexOf(':');
|
|
563
|
+
if (lastColonIndex > -1 && remoteAddressString.indexOf(':') === lastColonIndex) {
|
|
564
|
+
const host = remoteAddressString.slice(0, lastColonIndex);
|
|
565
|
+
if (plugins.net.isIP(host)) {
|
|
566
|
+
return host;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return undefined;
|
|
571
|
+
}
|
|
572
|
+
|
|
448
573
|
/**
|
|
449
574
|
* Get telemetry for a specific client.
|
|
450
575
|
*/
|
|
@@ -533,10 +658,15 @@ export class VpnManager {
|
|
|
533
658
|
private async rewriteWireGuardAllowedIPs(
|
|
534
659
|
wireguardConfig: string,
|
|
535
660
|
targetProfileIds: string[],
|
|
661
|
+
clientId?: string,
|
|
536
662
|
): Promise<string> {
|
|
537
663
|
if (!this.config.getClientAllowedIPs) return wireguardConfig;
|
|
538
664
|
|
|
539
|
-
const allowedIPs = await this.config.getClientAllowedIPs(
|
|
665
|
+
const allowedIPs = await this.config.getClientAllowedIPs(
|
|
666
|
+
targetProfileIds,
|
|
667
|
+
clientId,
|
|
668
|
+
clientId ? this.getClientSourceIp(clientId) : undefined,
|
|
669
|
+
);
|
|
540
670
|
const effectiveAllowedIPs = allowedIPs.length ? allowedIPs : [this.getSubnet()];
|
|
541
671
|
const allowedLine = `AllowedIPs = ${effectiveAllowedIPs.join(', ')}`;
|
|
542
672
|
|
|
@@ -587,6 +717,31 @@ export class VpnManager {
|
|
|
587
717
|
}
|
|
588
718
|
}
|
|
589
719
|
|
|
720
|
+
private startClientSourceIpPolling(): void {
|
|
721
|
+
this.stopClientSourceIpPolling();
|
|
722
|
+
const pollIntervalMs = Math.max(1000, this.config.clientSourceIpPollIntervalMs ?? 10_000);
|
|
723
|
+
this.clientSourceIpPollTimer = setInterval(() => {
|
|
724
|
+
void this.refreshClientSourceIps().catch((err) => {
|
|
725
|
+
logger.log('warn', `VPN: Client source IP polling failed: ${err?.message || err}`);
|
|
726
|
+
});
|
|
727
|
+
}, pollIntervalMs);
|
|
728
|
+
this.clientSourceIpPollTimer.unref?.();
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
private stopClientSourceIpPolling(): void {
|
|
732
|
+
if (!this.clientSourceIpPollTimer) return;
|
|
733
|
+
clearInterval(this.clientSourceIpPollTimer);
|
|
734
|
+
this.clientSourceIpPollTimer = undefined;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
private sameSourceIpMap(left: Map<string, string>, right: Map<string, string>): boolean {
|
|
738
|
+
if (left.size !== right.size) return false;
|
|
739
|
+
for (const [clientId, sourceIp] of left) {
|
|
740
|
+
if (right.get(clientId) !== sourceIp) return false;
|
|
741
|
+
}
|
|
742
|
+
return true;
|
|
743
|
+
}
|
|
744
|
+
|
|
590
745
|
private getResolvedForwardingMode(): 'socket' | 'bridge' | 'hybrid' {
|
|
591
746
|
return this.resolvedForwardingMode
|
|
592
747
|
?? this.forwardingModeOverride
|
package/ts_web/appstate.ts
CHANGED
|
@@ -582,6 +582,52 @@ export const setActiveViewAction = uiStatePart.createAction<string>(async (state
|
|
|
582
582
|
};
|
|
583
583
|
});
|
|
584
584
|
|
|
585
|
+
const backgroundRefreshesInFlight = new Set<string>();
|
|
586
|
+
|
|
587
|
+
function runBackgroundRefresh(key: string, errorMessage: string, task: () => Promise<void>): void {
|
|
588
|
+
if (backgroundRefreshesInFlight.has(key)) return;
|
|
589
|
+
backgroundRefreshesInFlight.add(key);
|
|
590
|
+
void task()
|
|
591
|
+
.catch((error) => console.error(errorMessage, error))
|
|
592
|
+
.finally(() => backgroundRefreshesInFlight.delete(key));
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function refreshNetworkIpIntelligence(identity: interfaces.data.IIdentity, ipAddresses: string[]): void {
|
|
596
|
+
const ips = [...new Set(ipAddresses.filter(Boolean))].slice(0, 100);
|
|
597
|
+
if (ips.length === 0) return;
|
|
598
|
+
|
|
599
|
+
runBackgroundRefresh('networkIpIntelligence', 'IP intelligence refresh failed:', async () => {
|
|
600
|
+
const intelligenceRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
601
|
+
interfaces.requests.IReq_ListIpIntelligence
|
|
602
|
+
>('/typedrequest', 'listIpIntelligence');
|
|
603
|
+
const intelligenceResponse = await intelligenceRequest.fire({
|
|
604
|
+
identity,
|
|
605
|
+
ipAddresses: ips,
|
|
606
|
+
limit: Math.max(100, ips.length),
|
|
607
|
+
});
|
|
608
|
+
networkStatePart.setState({
|
|
609
|
+
...networkStatePart.getState()!,
|
|
610
|
+
ipIntelligence: intelligenceResponse.records || [],
|
|
611
|
+
});
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function refreshSecurityIpIntelligence(identity: interfaces.data.IIdentity): void {
|
|
616
|
+
runBackgroundRefresh('securityIpIntelligence', 'Security IP intelligence refresh failed:', async () => {
|
|
617
|
+
const intelligenceRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
618
|
+
interfaces.requests.IReq_ListIpIntelligence
|
|
619
|
+
>('/typedrequest', 'listIpIntelligence');
|
|
620
|
+
const intelligenceResponse = await intelligenceRequest.fire({
|
|
621
|
+
identity,
|
|
622
|
+
limit: 500,
|
|
623
|
+
});
|
|
624
|
+
securityPolicyStatePart.setState({
|
|
625
|
+
...securityPolicyStatePart.getState()!,
|
|
626
|
+
ipIntelligence: intelligenceResponse.records || [],
|
|
627
|
+
});
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
|
|
585
631
|
// Fetch Network Stats Action
|
|
586
632
|
export const fetchNetworkStatsAction = networkStatePart.createAction(async (statePartArg): Promise<INetworkState> => {
|
|
587
633
|
const context = getActionContext();
|
|
@@ -594,18 +640,9 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
|
|
|
594
640
|
interfaces.requests.IReq_GetNetworkStats
|
|
595
641
|
>('/typedrequest', 'getNetworkStats');
|
|
596
642
|
|
|
597
|
-
const
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
const [networkStatsResponse, ipIntelligenceResponse] = await Promise.all([
|
|
602
|
-
networkStatsRequest.fire({
|
|
603
|
-
identity: context.identity,
|
|
604
|
-
}),
|
|
605
|
-
ipIntelligenceRequest.fire({
|
|
606
|
-
identity: context.identity,
|
|
607
|
-
}),
|
|
608
|
-
]);
|
|
643
|
+
const networkStatsResponse = await networkStatsRequest.fire({
|
|
644
|
+
identity: context.identity,
|
|
645
|
+
});
|
|
609
646
|
|
|
610
647
|
// Use the connections data for the connection list
|
|
611
648
|
// and network stats for throughput and IP analytics
|
|
@@ -637,6 +674,12 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
|
|
|
637
674
|
};
|
|
638
675
|
});
|
|
639
676
|
|
|
677
|
+
refreshNetworkIpIntelligence(context.identity, [
|
|
678
|
+
...Object.keys(connectionsByIP),
|
|
679
|
+
...(networkStatsResponse.topIPs || []).map((item) => item.ip),
|
|
680
|
+
...(networkStatsResponse.topIPsByBandwidth || []).map((item) => item.ip),
|
|
681
|
+
]);
|
|
682
|
+
|
|
640
683
|
return {
|
|
641
684
|
connections,
|
|
642
685
|
connectionsByIP,
|
|
@@ -647,7 +690,7 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
|
|
|
647
690
|
topIPs: networkStatsResponse.topIPs || [],
|
|
648
691
|
topIPsByBandwidth: networkStatsResponse.topIPsByBandwidth || [],
|
|
649
692
|
throughputByIP: networkStatsResponse.throughputByIP || [],
|
|
650
|
-
ipIntelligence:
|
|
693
|
+
ipIntelligence: currentState.ipIntelligence,
|
|
651
694
|
domainActivity: networkStatsResponse.domainActivity || [],
|
|
652
695
|
throughputHistory: networkStatsResponse.throughputHistory || [],
|
|
653
696
|
requestsPerSecond: networkStatsResponse.requestsPerSecond || 0,
|
|
@@ -683,9 +726,6 @@ export const fetchSecurityPolicyAction = securityPolicyStatePart.createAction(
|
|
|
683
726
|
const rulesRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
684
727
|
interfaces.requests.IReq_ListSecurityBlockRules
|
|
685
728
|
>('/typedrequest', 'listSecurityBlockRules');
|
|
686
|
-
const intelligenceRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
687
|
-
interfaces.requests.IReq_ListIpIntelligence
|
|
688
|
-
>('/typedrequest', 'listIpIntelligence');
|
|
689
729
|
const compiledPolicyRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
690
730
|
interfaces.requests.IReq_GetCompiledSecurityPolicy
|
|
691
731
|
>('/typedrequest', 'getCompiledSecurityPolicy');
|
|
@@ -693,16 +733,17 @@ export const fetchSecurityPolicyAction = securityPolicyStatePart.createAction(
|
|
|
693
733
|
interfaces.requests.IReq_ListSecurityPolicyAudit
|
|
694
734
|
>('/typedrequest', 'listSecurityPolicyAudit');
|
|
695
735
|
|
|
696
|
-
const [rulesResponse,
|
|
736
|
+
const [rulesResponse, compiledPolicyResponse, auditResponse] = await Promise.all([
|
|
697
737
|
rulesRequest.fire({ identity: context.identity }),
|
|
698
|
-
intelligenceRequest.fire({ identity: context.identity }),
|
|
699
738
|
compiledPolicyRequest.fire({ identity: context.identity }),
|
|
700
739
|
auditRequest.fire({ identity: context.identity, limit: 100 }),
|
|
701
740
|
]);
|
|
702
741
|
|
|
742
|
+
refreshSecurityIpIntelligence(context.identity);
|
|
743
|
+
|
|
703
744
|
return {
|
|
704
745
|
rules: rulesResponse.rules || [],
|
|
705
|
-
ipIntelligence:
|
|
746
|
+
ipIntelligence: currentState.ipIntelligence,
|
|
706
747
|
compiledPolicy: compiledPolicyResponse.policy,
|
|
707
748
|
auditEvents: auditResponse.events || [],
|
|
708
749
|
isLoading: false,
|
|
@@ -835,7 +876,15 @@ export const refreshIpIntelligenceAction = securityPolicyStatePart.createAction<
|
|
|
835
876
|
if (!response.success) {
|
|
836
877
|
return { ...currentState, error: response.message || 'Failed to refresh IP intelligence' };
|
|
837
878
|
}
|
|
838
|
-
|
|
879
|
+
const refreshedState = await actionContext!.dispatch(fetchSecurityPolicyAction, null);
|
|
880
|
+
if (!response.record) return refreshedState;
|
|
881
|
+
return {
|
|
882
|
+
...refreshedState,
|
|
883
|
+
ipIntelligence: [
|
|
884
|
+
response.record,
|
|
885
|
+
...refreshedState.ipIntelligence.filter((record) => record.ipAddress !== response.record!.ipAddress),
|
|
886
|
+
],
|
|
887
|
+
};
|
|
839
888
|
} catch (error: unknown) {
|
|
840
889
|
return {
|
|
841
890
|
...currentState,
|
|
@@ -1520,6 +1569,7 @@ export const createTargetProfileAction = targetProfilesStatePart.createAction<{
|
|
|
1520
1569
|
domains?: string[];
|
|
1521
1570
|
targets?: Array<{ ip: string; port: number }>;
|
|
1522
1571
|
routeRefs?: string[];
|
|
1572
|
+
allowRoutesByClientSourceIp?: boolean;
|
|
1523
1573
|
}>(async (statePartArg, dataArg, actionContext): Promise<ITargetProfilesState> => {
|
|
1524
1574
|
const context = getActionContext();
|
|
1525
1575
|
try {
|
|
@@ -1533,6 +1583,7 @@ export const createTargetProfileAction = targetProfilesStatePart.createAction<{
|
|
|
1533
1583
|
domains: dataArg.domains,
|
|
1534
1584
|
targets: dataArg.targets,
|
|
1535
1585
|
routeRefs: dataArg.routeRefs,
|
|
1586
|
+
allowRoutesByClientSourceIp: dataArg.allowRoutesByClientSourceIp,
|
|
1536
1587
|
});
|
|
1537
1588
|
if (!response.success) {
|
|
1538
1589
|
return {
|
|
@@ -1556,6 +1607,7 @@ export const updateTargetProfileAction = targetProfilesStatePart.createAction<{
|
|
|
1556
1607
|
domains?: string[];
|
|
1557
1608
|
targets?: Array<{ ip: string; port: number }>;
|
|
1558
1609
|
routeRefs?: string[];
|
|
1610
|
+
allowRoutesByClientSourceIp?: boolean;
|
|
1559
1611
|
}>(async (statePartArg, dataArg, actionContext): Promise<ITargetProfilesState> => {
|
|
1560
1612
|
const context = getActionContext();
|
|
1561
1613
|
try {
|
|
@@ -1570,6 +1622,7 @@ export const updateTargetProfileAction = targetProfilesStatePart.createAction<{
|
|
|
1570
1622
|
domains: dataArg.domains,
|
|
1571
1623
|
targets: dataArg.targets,
|
|
1572
1624
|
routeRefs: dataArg.routeRefs,
|
|
1625
|
+
allowRoutesByClientSourceIp: dataArg.allowRoutesByClientSourceIp,
|
|
1573
1626
|
});
|
|
1574
1627
|
if (!response.success) {
|
|
1575
1628
|
return {
|
|
@@ -3112,53 +3165,38 @@ async function dispatchCombinedRefreshActionInner() {
|
|
|
3112
3165
|
error: null,
|
|
3113
3166
|
});
|
|
3114
3167
|
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
networkStatePart.setState({
|
|
3121
|
-
...networkStatePart.getState()!,
|
|
3122
|
-
ipIntelligence: intelligenceResponse.records || [],
|
|
3123
|
-
});
|
|
3124
|
-
} catch (error) {
|
|
3125
|
-
console.error('IP intelligence refresh failed:', error);
|
|
3126
|
-
}
|
|
3168
|
+
refreshNetworkIpIntelligence(context.identity, [
|
|
3169
|
+
...network.connectionDetails.map((conn) => conn.remoteAddress),
|
|
3170
|
+
...network.topEndpoints.map((endpoint) => endpoint.endpoint),
|
|
3171
|
+
...(network.topEndpointsByBandwidth || []).map((endpoint) => endpoint.endpoint),
|
|
3172
|
+
]);
|
|
3127
3173
|
}
|
|
3128
3174
|
|
|
3129
3175
|
if (currentView === 'security') {
|
|
3130
|
-
|
|
3176
|
+
runBackgroundRefresh('securityPolicy', 'Security policy refresh failed:', async () => {
|
|
3131
3177
|
await securityPolicyStatePart.dispatchAction(fetchSecurityPolicyAction, null);
|
|
3132
|
-
}
|
|
3133
|
-
console.error('Security policy refresh failed:', error);
|
|
3134
|
-
}
|
|
3178
|
+
});
|
|
3135
3179
|
}
|
|
3136
3180
|
|
|
3137
3181
|
// Refresh certificate data if on Domains > Certificates subview
|
|
3138
3182
|
if (currentView === 'domains' && currentSubview === 'certificates') {
|
|
3139
|
-
|
|
3183
|
+
runBackgroundRefresh('certificates', 'Certificate refresh failed:', async () => {
|
|
3140
3184
|
await certificateStatePart.dispatchAction(fetchCertificateOverviewAction, null);
|
|
3141
|
-
}
|
|
3142
|
-
console.error('Certificate refresh failed:', error);
|
|
3143
|
-
}
|
|
3185
|
+
});
|
|
3144
3186
|
}
|
|
3145
3187
|
|
|
3146
3188
|
// Refresh remote ingress data if on the Network → Remote Ingress subview
|
|
3147
3189
|
if (currentView === 'network' && currentSubview === 'remoteingress') {
|
|
3148
|
-
|
|
3190
|
+
runBackgroundRefresh('remoteIngress', 'Remote ingress refresh failed:', async () => {
|
|
3149
3191
|
await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
|
|
3150
|
-
}
|
|
3151
|
-
console.error('Remote ingress refresh failed:', error);
|
|
3152
|
-
}
|
|
3192
|
+
});
|
|
3153
3193
|
}
|
|
3154
3194
|
|
|
3155
3195
|
// Refresh VPN data if on the Network → VPN subview
|
|
3156
3196
|
if (currentView === 'network' && currentSubview === 'vpn') {
|
|
3157
|
-
|
|
3197
|
+
runBackgroundRefresh('vpn', 'VPN refresh failed:', async () => {
|
|
3158
3198
|
await vpnStatePart.dispatchAction(fetchVpnAction, null);
|
|
3159
|
-
}
|
|
3160
|
-
console.error('VPN refresh failed:', error);
|
|
3161
|
-
}
|
|
3199
|
+
});
|
|
3162
3200
|
}
|
|
3163
3201
|
} catch (error) {
|
|
3164
3202
|
console.error('Combined refresh failed:', error);
|
|
@@ -97,6 +97,7 @@ export class OpsViewTargetProfiles extends DeesElement {
|
|
|
97
97
|
'Route Refs': profile.routeRefs?.length
|
|
98
98
|
? html`${profile.routeRefs.map(r => html`<span class="tagBadge">${this.formatRouteRef(r)}</span>`)}`
|
|
99
99
|
: '-',
|
|
100
|
+
'Client Source IP Routes': profile.allowRoutesByClientSourceIp ? 'Yes' : 'No',
|
|
100
101
|
Created: new Date(profile.createdAt).toLocaleDateString(),
|
|
101
102
|
})}
|
|
102
103
|
.dataActions=${[
|
|
@@ -223,6 +224,7 @@ export class OpsViewTargetProfiles extends DeesElement {
|
|
|
223
224
|
<dees-input-list .key=${'domains'} .label=${'Domains'} .placeholder=${'e.g. *.example.com'} .allowFreeform=${true}></dees-input-list>
|
|
224
225
|
<dees-input-list .key=${'targets'} .label=${'Targets'} .description=${'Format: ip:port, e.g. 10.0.0.1:443'} .placeholder=${'e.g. 10.0.0.1:443'} .allowFreeform=${true}></dees-input-list>
|
|
225
226
|
<dees-input-list .key=${'routeRefs'} .label=${'Route Refs'} .placeholder=${'Type to search routes...'} .candidates=${routeCandidates} .allowFreeform=${true}></dees-input-list>
|
|
227
|
+
<dees-input-checkbox .key=${'allowRoutesByClientSourceIp'} .label=${'Allow routes by VPN client source IP'} .description=${'Also grant access to non-VPN-only routes that would allow the client\'s real connecting IP'} .value=${false}></dees-input-checkbox>
|
|
226
228
|
</dees-form>
|
|
227
229
|
`,
|
|
228
230
|
menuOptions: [
|
|
@@ -258,6 +260,7 @@ export class OpsViewTargetProfiles extends DeesElement {
|
|
|
258
260
|
domains: domains.length > 0 ? domains : undefined,
|
|
259
261
|
targets: targets.length > 0 ? targets : undefined,
|
|
260
262
|
routeRefs: routeRefs.length > 0 ? routeRefs : undefined,
|
|
263
|
+
allowRoutesByClientSourceIp: data.allowRoutesByClientSourceIp === true,
|
|
261
264
|
});
|
|
262
265
|
modalArg.destroy();
|
|
263
266
|
},
|
|
@@ -284,6 +287,7 @@ export class OpsViewTargetProfiles extends DeesElement {
|
|
|
284
287
|
<dees-input-list .key=${'domains'} .label=${'Domains'} .placeholder=${'e.g. *.example.com'} .allowFreeform=${true} .value=${currentDomains}></dees-input-list>
|
|
285
288
|
<dees-input-list .key=${'targets'} .label=${'Targets'} .description=${'Format: ip:port, e.g. 10.0.0.1:443'} .placeholder=${'e.g. 10.0.0.1:443'} .allowFreeform=${true} .value=${currentTargets}></dees-input-list>
|
|
286
289
|
<dees-input-list .key=${'routeRefs'} .label=${'Route Refs'} .placeholder=${'Type to search routes...'} .candidates=${routeCandidates} .allowFreeform=${true} .value=${currentRouteRefs}></dees-input-list>
|
|
290
|
+
<dees-input-checkbox .key=${'allowRoutesByClientSourceIp'} .label=${'Allow routes by VPN client source IP'} .description=${'Also grant access to non-VPN-only routes that would allow the client\'s real connecting IP'} .value=${profile.allowRoutesByClientSourceIp === true}></dees-input-checkbox>
|
|
287
291
|
</dees-form>
|
|
288
292
|
`,
|
|
289
293
|
menuOptions: [
|
|
@@ -319,6 +323,7 @@ export class OpsViewTargetProfiles extends DeesElement {
|
|
|
319
323
|
domains,
|
|
320
324
|
targets,
|
|
321
325
|
routeRefs,
|
|
326
|
+
allowRoutesByClientSourceIp: data.allowRoutesByClientSourceIp === true,
|
|
322
327
|
});
|
|
323
328
|
modalArg.destroy();
|
|
324
329
|
},
|
|
@@ -389,6 +394,10 @@ export class OpsViewTargetProfiles extends DeesElement {
|
|
|
389
394
|
: '-'}
|
|
390
395
|
</div>
|
|
391
396
|
</div>
|
|
397
|
+
<div>
|
|
398
|
+
<div style="font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};">Client Source IP Routes</div>
|
|
399
|
+
<div style="font-size: 14px; margin-top: 4px;">${profile.allowRoutesByClientSourceIp ? 'Enabled' : 'Disabled'}</div>
|
|
400
|
+
</div>
|
|
392
401
|
<div>
|
|
393
402
|
<div style="font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};">Created</div>
|
|
394
403
|
<div style="font-size: 14px; margin-top: 4px;">${new Date(profile.createdAt).toLocaleString()} by ${profile.createdBy}</div>
|
|
@@ -339,6 +339,7 @@ export class OpsViewVpn extends DeesElement {
|
|
|
339
339
|
'Status': statusHtml,
|
|
340
340
|
'Routing': routingHtml,
|
|
341
341
|
'VPN IP': client.assignedIp || '-',
|
|
342
|
+
'Source IP': conn?.sourceIp || '-',
|
|
342
343
|
'Target Profiles': this.renderTargetProfileBadges(client.targetProfileIds),
|
|
343
344
|
'Description': client.description || '-',
|
|
344
345
|
'Created': new Date(client.createdAt).toLocaleDateString(),
|
|
@@ -487,6 +488,7 @@ export class OpsViewVpn extends DeesElement {
|
|
|
487
488
|
${conn ? html`
|
|
488
489
|
<div class="infoItem"><span class="infoLabel">Connected Since</span><span class="infoValue">${new Date(conn.connectedSince).toLocaleString()}</span></div>
|
|
489
490
|
<div class="infoItem"><span class="infoLabel">Transport</span><span class="infoValue">${conn.transport}</span></div>
|
|
491
|
+
<div class="infoItem"><span class="infoLabel">Source IP</span><span class="infoValue">${conn.sourceIp || '-'}</span></div>
|
|
490
492
|
` : ''}
|
|
491
493
|
<div class="infoItem"><span class="infoLabel">Description</span><span class="infoValue">${client.description || '-'}</span></div>
|
|
492
494
|
<div class="infoItem"><span class="infoLabel">Target Profiles</span><span class="infoValue">${this.resolveProfileIdsToLabels(client.targetProfileIds)?.join(', ') || '-'}</span></div>
|