@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.
Files changed (40) hide show
  1. package/dist_serve/bundle.js +597 -590
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/classes.dcrouter.js +9 -4
  4. package/dist_ts/config/classes.target-profile-manager.d.ts +16 -3
  5. package/dist_ts/config/classes.target-profile-manager.js +197 -6
  6. package/dist_ts/db/documents/classes.target-profile.doc.d.ts +1 -0
  7. package/dist_ts/db/documents/classes.target-profile.doc.js +8 -2
  8. package/dist_ts/monitoring/classes.metricsmanager.js +5 -2
  9. package/dist_ts/opsserver/handlers/security.handler.js +9 -2
  10. package/dist_ts/opsserver/handlers/target-profile.handler.js +3 -1
  11. package/dist_ts/opsserver/handlers/vpn.handler.js +3 -1
  12. package/dist_ts/security/classes.security-policy-manager.d.ts +15 -1
  13. package/dist_ts/security/classes.security-policy-manager.js +108 -9
  14. package/dist_ts/vpn/classes.vpn-manager.d.ts +15 -1
  15. package/dist_ts/vpn/classes.vpn-manager.js +138 -6
  16. package/dist_ts_interfaces/data/target-profile.d.ts +2 -0
  17. package/dist_ts_interfaces/data/vpn.d.ts +4 -0
  18. package/dist_ts_interfaces/requests/security-policy.d.ts +2 -0
  19. package/dist_ts_interfaces/requests/target-profiles.d.ts +2 -0
  20. package/dist_ts_web/00_commitinfo_data.js +1 -1
  21. package/dist_ts_web/appstate.d.ts +2 -0
  22. package/dist_ts_web/appstate.js +77 -47
  23. package/dist_ts_web/elements/network/ops-view-targetprofiles.js +10 -1
  24. package/dist_ts_web/elements/network/ops-view-vpn.js +3 -1
  25. package/package.json +2 -2
  26. package/readme.md +13 -0
  27. package/ts/00_commitinfo_data.ts +1 -1
  28. package/ts/classes.dcrouter.ts +10 -2
  29. package/ts/config/classes.target-profile-manager.ts +229 -5
  30. package/ts/db/documents/classes.target-profile.doc.ts +3 -0
  31. package/ts/monitoring/classes.metricsmanager.ts +4 -1
  32. package/ts/opsserver/handlers/security.handler.ts +8 -1
  33. package/ts/opsserver/handlers/target-profile.handler.ts +2 -0
  34. package/ts/opsserver/handlers/vpn.handler.ts +2 -0
  35. package/ts/security/classes.security-policy-manager.ts +115 -7
  36. package/ts/vpn/classes.vpn-manager.ts +158 -3
  37. package/ts_web/00_commitinfo_data.ts +1 -1
  38. package/ts_web/appstate.ts +86 -48
  39. package/ts_web/elements/network/ops-view-targetprofiles.ts +9 -0
  40. 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(config, persisted?.targetProfileIds || []);
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(targetProfileIds);
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
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '13.32.1',
6
+ version: '13.34.0',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }
@@ -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 ipIntelligenceRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
598
- interfaces.requests.IReq_ListIpIntelligence
599
- >('/typedrequest', 'listIpIntelligence');
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: ipIntelligenceResponse.records || [],
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, intelligenceResponse, compiledPolicyResponse, auditResponse] = await Promise.all([
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: intelligenceResponse.records || [],
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
- return await actionContext!.dispatch(fetchSecurityPolicyAction, null);
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
- try {
3116
- const intelligenceRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
3117
- interfaces.requests.IReq_ListIpIntelligence
3118
- >('/typedrequest', 'listIpIntelligence');
3119
- const intelligenceResponse = await intelligenceRequest.fire({ identity: context.identity });
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
- try {
3176
+ runBackgroundRefresh('securityPolicy', 'Security policy refresh failed:', async () => {
3131
3177
  await securityPolicyStatePart.dispatchAction(fetchSecurityPolicyAction, null);
3132
- } catch (error) {
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
- try {
3183
+ runBackgroundRefresh('certificates', 'Certificate refresh failed:', async () => {
3140
3184
  await certificateStatePart.dispatchAction(fetchCertificateOverviewAction, null);
3141
- } catch (error) {
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
- try {
3190
+ runBackgroundRefresh('remoteIngress', 'Remote ingress refresh failed:', async () => {
3149
3191
  await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
3150
- } catch (error) {
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
- try {
3197
+ runBackgroundRefresh('vpn', 'VPN refresh failed:', async () => {
3158
3198
  await vpnStatePart.dispatchAction(fetchVpnAction, null);
3159
- } catch (error) {
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>