@serve.zone/dcrouter 13.33.0 → 13.35.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 (36) hide show
  1. package/dist_serve/bundle.js +12 -5
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/classes.dcrouter.d.ts +1 -1
  4. package/dist_ts/classes.dcrouter.js +12 -8
  5. package/dist_ts/config/classes.route-config-manager.d.ts +6 -7
  6. package/dist_ts/config/classes.route-config-manager.js +19 -28
  7. package/dist_ts/config/classes.target-profile-manager.d.ts +12 -7
  8. package/dist_ts/config/classes.target-profile-manager.js +38 -11
  9. package/dist_ts/db/documents/classes.target-profile.doc.d.ts +1 -0
  10. package/dist_ts/db/documents/classes.target-profile.doc.js +8 -2
  11. package/dist_ts/opsserver/handlers/target-profile.handler.js +3 -1
  12. package/dist_ts/opsserver/handlers/vpn.handler.js +3 -1
  13. package/dist_ts/vpn/classes.vpn-manager.d.ts +15 -1
  14. package/dist_ts/vpn/classes.vpn-manager.js +140 -6
  15. package/dist_ts_interfaces/data/target-profile.d.ts +2 -0
  16. package/dist_ts_interfaces/data/vpn.d.ts +4 -0
  17. package/dist_ts_interfaces/requests/target-profiles.d.ts +2 -0
  18. package/dist_ts_web/00_commitinfo_data.js +1 -1
  19. package/dist_ts_web/appstate.d.ts +2 -0
  20. package/dist_ts_web/appstate.js +3 -1
  21. package/dist_ts_web/elements/network/ops-view-targetprofiles.js +10 -1
  22. package/dist_ts_web/elements/network/ops-view-vpn.js +3 -1
  23. package/package.json +6 -6
  24. package/readme.md +13 -0
  25. package/ts/00_commitinfo_data.ts +1 -1
  26. package/ts/classes.dcrouter.ts +15 -10
  27. package/ts/config/classes.route-config-manager.ts +25 -33
  28. package/ts/config/classes.target-profile-manager.ts +48 -14
  29. package/ts/db/documents/classes.target-profile.doc.ts +3 -0
  30. package/ts/opsserver/handlers/target-profile.handler.ts +2 -0
  31. package/ts/opsserver/handlers/vpn.handler.ts +2 -0
  32. package/ts/vpn/classes.vpn-manager.ts +160 -3
  33. package/ts_web/00_commitinfo_data.ts +1 -1
  34. package/ts_web/appstate.ts +4 -0
  35. package/ts_web/elements/network/ops-view-targetprofiles.ts +9 -0
  36. package/ts_web/elements/network/ops-view-vpn.ts +2 -0
@@ -5,6 +5,8 @@ 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 TVpnClientAllowEntry = string | { clientId: string; domains: string[] };
9
+
8
10
  /**
9
11
  * Manages TargetProfiles (target-side: what can be accessed).
10
12
  * TargetProfiles define what resources a VPN client can reach:
@@ -35,6 +37,7 @@ export class TargetProfileManager {
35
37
  domains?: string[];
36
38
  targets?: ITargetProfileTarget[];
37
39
  routeRefs?: string[];
40
+ allowRoutesByClientSourceIp?: boolean;
38
41
  createdBy: string;
39
42
  }): Promise<string> {
40
43
  // Enforce unique profile names
@@ -55,6 +58,7 @@ export class TargetProfileManager {
55
58
  domains: data.domains,
56
59
  targets: data.targets,
57
60
  routeRefs,
61
+ allowRoutesByClientSourceIp: data.allowRoutesByClientSourceIp === true,
58
62
  createdAt: now,
59
63
  updatedAt: now,
60
64
  createdBy: data.createdBy,
@@ -88,6 +92,9 @@ export class TargetProfileManager {
88
92
  if (patch.domains !== undefined) profile.domains = patch.domains;
89
93
  if (patch.targets !== undefined) profile.targets = patch.targets;
90
94
  if (patch.routeRefs !== undefined) profile.routeRefs = this.normalizeRouteRefs(patch.routeRefs);
95
+ if (patch.allowRoutesByClientSourceIp !== undefined) {
96
+ profile.allowRoutesByClientSourceIp = patch.allowRoutesByClientSourceIp === true;
97
+ }
91
98
  profile.updatedAt = Date.now();
92
99
 
93
100
  await this.persistProfile(profile);
@@ -199,29 +206,30 @@ export class TargetProfileManager {
199
206
  }
200
207
 
201
208
  // =========================================================================
202
- // Core matching: route → client IPs
209
+ // Core matching: route → VPN client grants
203
210
  // =========================================================================
204
211
 
205
212
  /**
206
- * For a vpnOnly route, find all enabled VPN clients whose assigned TargetProfile
207
- * matches the route. Returns IP allow entries for injection into ipAllowList.
213
+ * Find all enabled VPN clients whose assigned TargetProfile matches the route.
214
+ * Returns SmartProxy VPN client allow entries for authenticated metadata checks.
208
215
  *
209
216
  * Entries are domain-scoped when a profile matches via specific domains that are
210
217
  * a subset of the route's wildcard. Plain IPs are returned for routeRef/target matches
211
- * or when profile domains exactly equal the route's domains.
218
+ * or when profile domains exactly equal the route's domains. Profiles can also opt
219
+ * into source-policy routes; SmartProxy evaluates the real source IP per connection.
212
220
  */
213
- public getMatchingClientIps(
221
+ public getMatchingVpnClients(
214
222
  route: IDcRouterRouteConfig,
215
223
  routeId: string | undefined,
216
224
  clients: VpnClientDoc[],
217
225
  allRoutes: Map<string, IRoute> = new Map(),
218
- ): Array<string | { ip: string; domains: string[] }> {
219
- const entries: Array<string | { ip: string; domains: string[] }> = [];
226
+ ): TVpnClientAllowEntry[] {
227
+ const entries: TVpnClientAllowEntry[] = [];
220
228
  const routeDomains = this.getRouteDomains(route);
221
229
  const routeNameIndex = this.buildRouteNameIndex(allRoutes);
222
230
 
223
231
  for (const client of clients) {
224
- if (!client.enabled || !client.assignedIp) continue;
232
+ if (!client.enabled || !client.clientId) continue;
225
233
  if (!client.targetProfileIds?.length) continue;
226
234
 
227
235
  // Collect scoped domains from all matching profiles for this client
@@ -246,12 +254,20 @@ export class TargetProfileManager {
246
254
  if (matchResult !== 'none') {
247
255
  for (const d of matchResult.domains) scopedDomains.add(d);
248
256
  }
257
+
258
+ if (
259
+ profile.allowRoutesByClientSourceIp === true
260
+ && this.routeHasSourcePolicy(route)
261
+ ) {
262
+ fullAccess = true;
263
+ break;
264
+ }
249
265
  }
250
266
 
251
267
  if (fullAccess) {
252
- entries.push(client.assignedIp);
268
+ entries.push(client.clientId);
253
269
  } else if (scopedDomains.size > 0) {
254
- entries.push({ ip: client.assignedIp, domains: [...scopedDomains] });
270
+ entries.push({ clientId: client.clientId, domains: [...scopedDomains] });
255
271
  }
256
272
  }
257
273
 
@@ -292,13 +308,18 @@ export class TargetProfileManager {
292
308
  // Route references: scan all routes
293
309
  for (const [routeId, route] of allRoutes) {
294
310
  if (!route.enabled) continue;
295
- if (this.routeMatchesProfile(
296
- route.route as IDcRouterRouteConfig,
311
+ const dcRoute = route.route as IDcRouterRouteConfig;
312
+ const routeDomains = this.getRouteDomains(dcRoute);
313
+ const profileMatchesRoute = this.routeMatchesProfile(
314
+ dcRoute,
297
315
  routeId,
298
316
  profile,
299
317
  routeNameIndex,
300
- )) {
301
- for (const d of this.getRouteDomains(route.route as IDcRouterRouteConfig)) {
318
+ );
319
+ const sourceIpMatchesRoute = profile.allowRoutesByClientSourceIp === true
320
+ && this.routeHasSourcePolicy(dcRoute);
321
+ if (profileMatchesRoute || sourceIpMatchesRoute) {
322
+ for (const d of routeDomains) {
302
323
  domains.add(d);
303
324
  }
304
325
  }
@@ -422,6 +443,16 @@ export class TargetProfileManager {
422
443
  return false;
423
444
  }
424
445
 
446
+ private routeHasSourcePolicy(route: IDcRouterRouteConfig): boolean {
447
+ const security = (route as any).security;
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() === '*');
454
+ }
455
+
425
456
  private getRouteDomains(route: IDcRouterRouteConfig): string[] {
426
457
  const domains = (route.match as any)?.domains;
427
458
  if (!domains) return [];
@@ -503,6 +534,7 @@ export class TargetProfileManager {
503
534
  domains: doc.domains,
504
535
  targets: doc.targets,
505
536
  routeRefs: doc.routeRefs,
537
+ allowRoutesByClientSourceIp: doc.allowRoutesByClientSourceIp === true,
506
538
  createdAt: doc.createdAt,
507
539
  updatedAt: doc.updatedAt,
508
540
  createdBy: doc.createdBy,
@@ -522,6 +554,7 @@ export class TargetProfileManager {
522
554
  existingDoc.domains = profile.domains;
523
555
  existingDoc.targets = profile.targets;
524
556
  existingDoc.routeRefs = profile.routeRefs;
557
+ existingDoc.allowRoutesByClientSourceIp = profile.allowRoutesByClientSourceIp === true;
525
558
  existingDoc.updatedAt = profile.updatedAt;
526
559
  await existingDoc.save();
527
560
  } else {
@@ -532,6 +565,7 @@ export class TargetProfileManager {
532
565
  doc.domains = profile.domains;
533
566
  doc.targets = profile.targets;
534
567
  doc.routeRefs = profile.routeRefs;
568
+ doc.allowRoutesByClientSourceIp = profile.allowRoutesByClientSourceIp === true;
535
569
  doc.createdAt = profile.createdAt;
536
570
  doc.updatedAt = profile.updatedAt;
537
571
  doc.createdBy = profile.createdBy;
@@ -25,6 +25,9 @@ export class TargetProfileDoc extends plugins.smartdata.SmartDataDbDoc<TargetPro
25
25
  @plugins.smartdata.svDb()
26
26
  public routeRefs?: string[];
27
27
 
28
+ @plugins.smartdata.svDb()
29
+ public allowRoutesByClientSourceIp?: boolean;
30
+
28
31
  @plugins.smartdata.svDb()
29
32
  public createdAt!: number;
30
33
 
@@ -69,6 +69,7 @@ export class TargetProfileHandler {
69
69
  domains: dataArg.domains,
70
70
  targets: dataArg.targets,
71
71
  routeRefs: dataArg.routeRefs,
72
+ allowRoutesByClientSourceIp: dataArg.allowRoutesByClientSourceIp,
72
73
  createdBy: userId,
73
74
  });
74
75
  await this.opsServerRef.dcRouterRef.routeConfigManager?.applyRoutes();
@@ -94,6 +95,7 @@ export class TargetProfileHandler {
94
95
  domains: dataArg.domains,
95
96
  targets: dataArg.targets,
96
97
  routeRefs: dataArg.routeRefs,
98
+ allowRoutesByClientSourceIp: dataArg.allowRoutesByClientSourceIp,
97
99
  });
98
100
  // Re-apply routes and refresh VPN client security to update access
99
101
  await this.opsServerRef.dcRouterRef.routeConfigManager?.applyRoutes();
@@ -102,6 +102,8 @@ export class VpnHandler {
102
102
  bytesSent: c.bytesSent,
103
103
  bytesReceived: c.bytesReceived,
104
104
  transport: c.transportType,
105
+ remoteAddr: c.remoteAddr,
106
+ sourceIp: manager.getClientSourceIp(c.registeredClientId || c.clientId),
105
107
  })),
106
108
  };
107
109
  },
@@ -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;
@@ -145,6 +152,8 @@ export class VpnManager {
145
152
  wgListenPort,
146
153
  clients: clientEntries,
147
154
  socketForwardProxyProtocol: !isBridge,
155
+ socketForwardProxyProtocolSource: 'remoteIp',
156
+ socketForwardProxyProtocolVpnMetadata: true,
148
157
  destinationPolicy: this.getServerDestinationPolicy(forwardingMode, defaultDestinationPolicy),
149
158
  serverEndpoint,
150
159
  clientAllowedIPs: [subnet],
@@ -173,6 +182,9 @@ export class VpnManager {
173
182
  }
174
183
  }
175
184
 
185
+ await this.refreshClientSourceIps(false);
186
+ this.startClientSourceIpPolling();
187
+
176
188
  logger.log('info', `VPN server started: subnet=${subnet}, wg=:${wgListenPort}, clients=${this.clients.size}`);
177
189
  }
178
190
 
@@ -180,6 +192,7 @@ export class VpnManager {
180
192
  * Stop the VPN server.
181
193
  */
182
194
  public async stop(): Promise<void> {
195
+ this.stopClientSourceIpPolling();
183
196
  if (this.vpnServer) {
184
197
  try {
185
198
  await this.vpnServer.stopServer();
@@ -189,6 +202,11 @@ export class VpnManager {
189
202
  await this.vpnServer.stop();
190
203
  this.vpnServer = undefined;
191
204
  }
205
+ const hadClientSourceIps = this.clientSourceIps.size > 0;
206
+ this.clientSourceIps.clear();
207
+ if (hadClientSourceIps) {
208
+ this.config.onClientSourceIpsChanged?.();
209
+ }
192
210
  this.resolvedForwardingMode = undefined;
193
211
  logger.log('info', 'VPN server stopped');
194
212
  }
@@ -246,6 +264,7 @@ export class VpnManager {
246
264
  bundle.wireguardConfig = await this.rewriteWireGuardAllowedIPs(
247
265
  bundle.wireguardConfig,
248
266
  doc.targetProfileIds || [],
267
+ doc.clientId,
249
268
  );
250
269
 
251
270
  // Persist client entry (including WG private key for export/QR)
@@ -287,6 +306,7 @@ export class VpnManager {
287
306
  await this.vpnServer.removeClient(clientId);
288
307
  const doc = this.clients.get(clientId);
289
308
  this.clients.delete(clientId);
309
+ this.clientSourceIps.delete(clientId);
290
310
  if (doc) {
291
311
  await doc.delete();
292
312
  }
@@ -328,6 +348,7 @@ export class VpnManager {
328
348
  client.updatedAt = Date.now();
329
349
  await this.persistClient(client);
330
350
  }
351
+ this.clientSourceIps.delete(clientId);
331
352
  this.config.onClientChanged?.();
332
353
  }
333
354
 
@@ -380,6 +401,7 @@ export class VpnManager {
380
401
  bundle.wireguardConfig = await this.rewriteWireGuardAllowedIPs(
381
402
  bundle.wireguardConfig,
382
403
  client?.targetProfileIds || [],
404
+ clientId,
383
405
  );
384
406
 
385
407
  // Update persisted entry with new keys (including private key for export/QR)
@@ -413,7 +435,11 @@ export class VpnManager {
413
435
  );
414
436
  }
415
437
 
416
- config = await this.rewriteWireGuardAllowedIPs(config, persisted?.targetProfileIds || []);
438
+ config = await this.rewriteWireGuardAllowedIPs(
439
+ config,
440
+ persisted?.targetProfileIds || [],
441
+ clientId,
442
+ );
417
443
  }
418
444
 
419
445
  return config;
@@ -445,6 +471,107 @@ export class VpnManager {
445
471
  return this.vpnServer.listClients();
446
472
  }
447
473
 
474
+ public getClientSourceIp(clientId: string): string | undefined {
475
+ return this.clientSourceIps.get(clientId);
476
+ }
477
+
478
+ public getClientSourceIpMap(): Map<string, string> {
479
+ return new Map(this.clientSourceIps);
480
+ }
481
+
482
+ public async refreshClientSourceIps(notifyOnChange = true): Promise<boolean> {
483
+ if (!this.vpnServer || this.clientSourceIpRefreshInFlight) {
484
+ return false;
485
+ }
486
+
487
+ this.clientSourceIpRefreshInFlight = true;
488
+ try {
489
+ const connectedClients = await this.vpnServer.listClients();
490
+ const nextSourceIps = new Map<string, string>();
491
+ const wireguardClientIds = new Set<string>();
492
+
493
+ for (const connectedClient of connectedClients) {
494
+ const clientId = connectedClient.registeredClientId || connectedClient.clientId;
495
+ if (!clientId) continue;
496
+ if (connectedClient.transportType === 'wireguard') {
497
+ wireguardClientIds.add(clientId);
498
+ }
499
+
500
+ const sourceIp = VpnManager.normalizeRemoteAddress(connectedClient.remoteAddr);
501
+ if (sourceIp) {
502
+ nextSourceIps.set(clientId, sourceIp);
503
+ }
504
+ }
505
+
506
+ if (wireguardClientIds.size > 0 && typeof (this.vpnServer as any).listWgPeers === 'function') {
507
+ try {
508
+ const wgPeers = await this.vpnServer.listWgPeers();
509
+ const endpointByPublicKey = new Map<string, string>();
510
+ for (const peer of wgPeers) {
511
+ const endpointIp = VpnManager.normalizeRemoteAddress(peer.endpoint);
512
+ if (peer.publicKey && endpointIp) {
513
+ endpointByPublicKey.set(peer.publicKey, endpointIp);
514
+ }
515
+ }
516
+
517
+ for (const client of this.clients.values()) {
518
+ if (nextSourceIps.has(client.clientId)) continue;
519
+ if (!wireguardClientIds.has(client.clientId)) continue;
520
+ if (!client.wgPublicKey) continue;
521
+ const endpointIp = endpointByPublicKey.get(client.wgPublicKey);
522
+ if (endpointIp) {
523
+ nextSourceIps.set(client.clientId, endpointIp);
524
+ }
525
+ }
526
+ } catch (err) {
527
+ logger.log('warn', `VPN: Failed to refresh WireGuard peer endpoints: ${(err as Error).message}`);
528
+ }
529
+ }
530
+
531
+ if (this.sameSourceIpMap(this.clientSourceIps, nextSourceIps)) {
532
+ return false;
533
+ }
534
+
535
+ this.clientSourceIps = nextSourceIps;
536
+ if (notifyOnChange) {
537
+ this.config.onClientSourceIpsChanged?.();
538
+ }
539
+ return true;
540
+ } catch (err) {
541
+ logger.log('warn', `VPN: Failed to refresh client source IPs: ${(err as Error).message}`);
542
+ return false;
543
+ } finally {
544
+ this.clientSourceIpRefreshInFlight = false;
545
+ }
546
+ }
547
+
548
+ public static normalizeRemoteAddress(remoteAddress?: string): string | undefined {
549
+ const remoteAddressString = remoteAddress?.trim();
550
+ if (!remoteAddressString) return undefined;
551
+
552
+ if (remoteAddressString.startsWith('[')) {
553
+ const closingBracketIndex = remoteAddressString.indexOf(']');
554
+ if (closingBracketIndex > 0) {
555
+ const bracketedIp = remoteAddressString.slice(1, closingBracketIndex);
556
+ return plugins.net.isIP(bracketedIp) ? bracketedIp : undefined;
557
+ }
558
+ }
559
+
560
+ if (plugins.net.isIP(remoteAddressString)) {
561
+ return remoteAddressString;
562
+ }
563
+
564
+ const lastColonIndex = remoteAddressString.lastIndexOf(':');
565
+ if (lastColonIndex > -1 && remoteAddressString.indexOf(':') === lastColonIndex) {
566
+ const host = remoteAddressString.slice(0, lastColonIndex);
567
+ if (plugins.net.isIP(host)) {
568
+ return host;
569
+ }
570
+ }
571
+
572
+ return undefined;
573
+ }
574
+
448
575
  /**
449
576
  * Get telemetry for a specific client.
450
577
  */
@@ -533,10 +660,15 @@ export class VpnManager {
533
660
  private async rewriteWireGuardAllowedIPs(
534
661
  wireguardConfig: string,
535
662
  targetProfileIds: string[],
663
+ clientId?: string,
536
664
  ): Promise<string> {
537
665
  if (!this.config.getClientAllowedIPs) return wireguardConfig;
538
666
 
539
- const allowedIPs = await this.config.getClientAllowedIPs(targetProfileIds);
667
+ const allowedIPs = await this.config.getClientAllowedIPs(
668
+ targetProfileIds,
669
+ clientId,
670
+ clientId ? this.getClientSourceIp(clientId) : undefined,
671
+ );
540
672
  const effectiveAllowedIPs = allowedIPs.length ? allowedIPs : [this.getSubnet()];
541
673
  const allowedLine = `AllowedIPs = ${effectiveAllowedIPs.join(', ')}`;
542
674
 
@@ -587,6 +719,31 @@ export class VpnManager {
587
719
  }
588
720
  }
589
721
 
722
+ private startClientSourceIpPolling(): void {
723
+ this.stopClientSourceIpPolling();
724
+ const pollIntervalMs = Math.max(1000, this.config.clientSourceIpPollIntervalMs ?? 10_000);
725
+ this.clientSourceIpPollTimer = setInterval(() => {
726
+ void this.refreshClientSourceIps().catch((err) => {
727
+ logger.log('warn', `VPN: Client source IP polling failed: ${err?.message || err}`);
728
+ });
729
+ }, pollIntervalMs);
730
+ this.clientSourceIpPollTimer.unref?.();
731
+ }
732
+
733
+ private stopClientSourceIpPolling(): void {
734
+ if (!this.clientSourceIpPollTimer) return;
735
+ clearInterval(this.clientSourceIpPollTimer);
736
+ this.clientSourceIpPollTimer = undefined;
737
+ }
738
+
739
+ private sameSourceIpMap(left: Map<string, string>, right: Map<string, string>): boolean {
740
+ if (left.size !== right.size) return false;
741
+ for (const [clientId, sourceIp] of left) {
742
+ if (right.get(clientId) !== sourceIp) return false;
743
+ }
744
+ return true;
745
+ }
746
+
590
747
  private getResolvedForwardingMode(): 'socket' | 'bridge' | 'hybrid' {
591
748
  return this.resolvedForwardingMode
592
749
  ?? this.forwardingModeOverride
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '13.33.0',
6
+ version: '13.35.0',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }
@@ -1569,6 +1569,7 @@ export const createTargetProfileAction = targetProfilesStatePart.createAction<{
1569
1569
  domains?: string[];
1570
1570
  targets?: Array<{ ip: string; port: number }>;
1571
1571
  routeRefs?: string[];
1572
+ allowRoutesByClientSourceIp?: boolean;
1572
1573
  }>(async (statePartArg, dataArg, actionContext): Promise<ITargetProfilesState> => {
1573
1574
  const context = getActionContext();
1574
1575
  try {
@@ -1582,6 +1583,7 @@ export const createTargetProfileAction = targetProfilesStatePart.createAction<{
1582
1583
  domains: dataArg.domains,
1583
1584
  targets: dataArg.targets,
1584
1585
  routeRefs: dataArg.routeRefs,
1586
+ allowRoutesByClientSourceIp: dataArg.allowRoutesByClientSourceIp,
1585
1587
  });
1586
1588
  if (!response.success) {
1587
1589
  return {
@@ -1605,6 +1607,7 @@ export const updateTargetProfileAction = targetProfilesStatePart.createAction<{
1605
1607
  domains?: string[];
1606
1608
  targets?: Array<{ ip: string; port: number }>;
1607
1609
  routeRefs?: string[];
1610
+ allowRoutesByClientSourceIp?: boolean;
1608
1611
  }>(async (statePartArg, dataArg, actionContext): Promise<ITargetProfilesState> => {
1609
1612
  const context = getActionContext();
1610
1613
  try {
@@ -1619,6 +1622,7 @@ export const updateTargetProfileAction = targetProfilesStatePart.createAction<{
1619
1622
  domains: dataArg.domains,
1620
1623
  targets: dataArg.targets,
1621
1624
  routeRefs: dataArg.routeRefs,
1625
+ allowRoutesByClientSourceIp: dataArg.allowRoutesByClientSourceIp,
1622
1626
  });
1623
1627
  if (!response.success) {
1624
1628
  return {
@@ -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
+ 'Source-Policy Route Grants': 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 source-policy route grants'} .description=${'Grant these VPN clients to source-policy routes; SmartProxy still checks their real connecting IP per connection'} .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 source-policy route grants'} .description=${'Grant these VPN clients to source-policy routes; SmartProxy still checks their real connecting IP per connection'} .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>