@serve.zone/dcrouter 13.34.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.
@@ -5,7 +5,7 @@ import type { ITargetProfile, ITargetProfileTarget } from '../../ts_interfaces/d
5
5
  import type { IDcRouterRouteConfig } from '../../ts_interfaces/data/remoteingress.js';
6
6
  import type { IRoute } from '../../ts_interfaces/data/route-management.js';
7
7
 
8
- type TIpAllowEntry = string | { ip: string; domains?: string[] };
8
+ type TVpnClientAllowEntry = string | { clientId: string; domains: string[] };
9
9
 
10
10
  /**
11
11
  * Manages TargetProfiles (target-side: what can be accessed).
@@ -206,37 +206,35 @@ export class TargetProfileManager {
206
206
  }
207
207
 
208
208
  // =========================================================================
209
- // Core matching: route → client IPs
209
+ // Core matching: route → VPN client grants
210
210
  // =========================================================================
211
211
 
212
212
  /**
213
- * For a vpnOnly route, find all enabled VPN clients whose assigned TargetProfile
214
- * matches the route. Returns IP allow entries for injection into ipAllowList.
213
+ * Find all enabled VPN clients whose assigned TargetProfile matches the route.
214
+ * Returns SmartProxy VPN client allow entries for authenticated metadata checks.
215
215
  *
216
216
  * Entries are domain-scoped when a profile matches via specific domains that are
217
217
  * a subset of the route's wildcard. Plain IPs are returned for routeRef/target matches
218
218
  * or when profile domains exactly equal the route's domains. Profiles can also opt
219
- * into source-IP matching against non-vpnOnly route security.
219
+ * into source-policy routes; SmartProxy evaluates the real source IP per connection.
220
220
  */
221
- public getMatchingClientIps(
221
+ public getMatchingVpnClients(
222
222
  route: IDcRouterRouteConfig,
223
223
  routeId: string | undefined,
224
224
  clients: VpnClientDoc[],
225
225
  allRoutes: Map<string, IRoute> = new Map(),
226
- clientSourceIps: Map<string, string> = new Map(),
227
- ): Array<string | { ip: string; domains: string[] }> {
228
- const entries: Array<string | { ip: string; domains: string[] }> = [];
226
+ ): TVpnClientAllowEntry[] {
227
+ const entries: TVpnClientAllowEntry[] = [];
229
228
  const routeDomains = this.getRouteDomains(route);
230
229
  const routeNameIndex = this.buildRouteNameIndex(allRoutes);
231
230
 
232
231
  for (const client of clients) {
233
- if (!client.enabled || !client.assignedIp) continue;
232
+ if (!client.enabled || !client.clientId) continue;
234
233
  if (!client.targetProfileIds?.length) continue;
235
234
 
236
235
  // Collect scoped domains from all matching profiles for this client
237
236
  let fullAccess = false;
238
237
  const scopedDomains = new Set<string>();
239
- const clientSourceIp = clientSourceIps.get(client.clientId);
240
238
 
241
239
  for (const profileId of client.targetProfileIds) {
242
240
  const profile = this.profiles.get(profileId);
@@ -258,10 +256,8 @@ export class TargetProfileManager {
258
256
  }
259
257
 
260
258
  if (
261
- !route.vpnOnly
262
- && profile.allowRoutesByClientSourceIp === true
263
- && clientSourceIp
264
- && this.routeAllowsSourceIp(route, clientSourceIp, routeDomains)
259
+ profile.allowRoutesByClientSourceIp === true
260
+ && this.routeHasSourcePolicy(route)
265
261
  ) {
266
262
  fullAccess = true;
267
263
  break;
@@ -269,9 +265,9 @@ export class TargetProfileManager {
269
265
  }
270
266
 
271
267
  if (fullAccess) {
272
- entries.push(client.assignedIp);
268
+ entries.push(client.clientId);
273
269
  } else if (scopedDomains.size > 0) {
274
- entries.push({ ip: client.assignedIp, domains: [...scopedDomains] });
270
+ entries.push({ clientId: client.clientId, domains: [...scopedDomains] });
275
271
  }
276
272
  }
277
273
 
@@ -285,7 +281,6 @@ export class TargetProfileManager {
285
281
  public getClientAccessSpec(
286
282
  targetProfileIds: string[],
287
283
  allRoutes: Map<string, IRoute>,
288
- clientSourceIp?: string,
289
284
  ): { domains: string[]; targetIps: string[] } {
290
285
  const domains = new Set<string>();
291
286
  const targetIps = new Set<string>();
@@ -322,9 +317,7 @@ export class TargetProfileManager {
322
317
  routeNameIndex,
323
318
  );
324
319
  const sourceIpMatchesRoute = profile.allowRoutesByClientSourceIp === true
325
- && clientSourceIp
326
- && !dcRoute.vpnOnly
327
- && this.routeAllowsSourceIp(dcRoute, clientSourceIp, routeDomains);
320
+ && this.routeHasSourcePolicy(dcRoute);
328
321
  if (profileMatchesRoute || sourceIpMatchesRoute) {
329
322
  for (const d of routeDomains) {
330
323
  domains.add(d);
@@ -450,197 +443,14 @@ export class TargetProfileManager {
450
443
  return false;
451
444
  }
452
445
 
453
- private routeAllowsSourceIp(
454
- route: IDcRouterRouteConfig,
455
- sourceIp: string,
456
- routeDomains: string[],
457
- ): boolean {
446
+ private routeHasSourcePolicy(route: IDcRouterRouteConfig): boolean {
458
447
  const security = (route as any).security;
459
- const ipAllowList = this.normalizeIpEntries(security?.ipAllowList);
460
- const ipBlockList = this.normalizeIpEntries(security?.ipBlockList);
461
-
462
- if (this.ipEntriesMatchSource(ipBlockList, sourceIp, routeDomains)) {
463
- return false;
464
- }
465
-
466
- if (!ipAllowList.length) {
467
- return true;
468
- }
469
-
470
- return this.ipEntriesMatchSource(ipAllowList, sourceIp, routeDomains);
471
- }
472
-
473
- private normalizeIpEntries(entries: unknown): TIpAllowEntry[] {
474
- if (!entries) return [];
475
- if (Array.isArray(entries)) return entries as TIpAllowEntry[];
476
- return [entries as TIpAllowEntry];
477
- }
478
-
479
- private ipEntriesMatchSource(
480
- entries: TIpAllowEntry[],
481
- sourceIp: string,
482
- routeDomains: string[],
483
- ): boolean {
484
- return entries.some((entry) => this.ipEntryMatchesSource(entry, sourceIp, routeDomains));
485
- }
486
-
487
- private ipEntryMatchesSource(
488
- entry: TIpAllowEntry,
489
- sourceIp: string,
490
- routeDomains: string[],
491
- ): boolean {
492
- const ipPattern = typeof entry === 'string' ? entry : entry.ip;
493
- if (typeof ipPattern !== 'string') return false;
494
- if (!this.ipPatternMatchesSource(ipPattern, sourceIp)) {
495
- return false;
496
- }
497
-
498
- if (typeof entry === 'string' || !entry.domains?.length) {
499
- return true;
500
- }
501
-
502
- if (!routeDomains.length) {
503
- return false;
504
- }
505
-
506
- return routeDomains.some((routeDomain) =>
507
- entry.domains!.some((entryDomain) =>
508
- this.domainMatchesPattern(routeDomain, entryDomain)
509
- || this.domainMatchesPattern(entryDomain, routeDomain),
510
- ),
511
- );
512
- }
513
-
514
- private ipPatternMatchesSource(pattern: string, sourceIp: string): boolean {
515
- const trimmedPattern = pattern.trim();
516
- const trimmedSourceIp = sourceIp.trim();
517
- if (!trimmedPattern || !trimmedSourceIp) return false;
518
- if (trimmedPattern === '*') return true;
519
- if (trimmedPattern === trimmedSourceIp) return true;
520
-
521
- if (trimmedPattern.includes('/')) {
522
- return this.ipMatchesCidr(trimmedSourceIp, trimmedPattern);
523
- }
524
-
525
- if (trimmedPattern.includes('-')) {
526
- return this.ipMatchesRange(trimmedSourceIp, trimmedPattern);
527
- }
528
-
529
- if (trimmedPattern.includes('*')) {
530
- return this.ipMatchesWildcard(trimmedSourceIp, trimmedPattern);
531
- }
532
-
533
- return false;
534
- }
535
-
536
- private ipMatchesCidr(sourceIp: string, cidr: string): boolean {
537
- const [networkIp, prefixString] = cidr.split('/');
538
- if (!networkIp || !prefixString) return false;
539
- const source = this.ipToComparable(sourceIp);
540
- const network = this.ipToComparable(networkIp);
541
- const prefix = Number(prefixString);
542
- if (!source || !network || source.version !== network.version) return false;
543
-
544
- const bitCount = source.version === 4 ? 32 : 128;
545
- if (!Number.isInteger(prefix) || prefix < 0 || prefix > bitCount) return false;
546
- if (prefix === 0) return true;
547
-
548
- const shift = BigInt(bitCount - prefix);
549
- return (source.value >> shift) === (network.value >> shift);
550
- }
551
-
552
- private ipMatchesRange(sourceIp: string, range: string): boolean {
553
- const [startIp, endIp] = range.split('-').map((part) => part.trim());
554
- if (!startIp || !endIp) return false;
555
- const source = this.ipToComparable(sourceIp);
556
- const start = this.ipToComparable(startIp);
557
- const end = this.ipToComparable(endIp);
558
- if (!source || !start || !end) return false;
559
- if (source.version !== start.version || source.version !== end.version) return false;
560
- return source.value >= start.value && source.value <= end.value;
561
- }
562
-
563
- private ipMatchesWildcard(sourceIp: string, pattern: string): boolean {
564
- const sourceParts = sourceIp.split('.');
565
- const patternParts = pattern.split('.');
566
- if (sourceParts.length !== 4 || patternParts.length !== 4) return false;
567
-
568
- return patternParts.every((patternPart, index) => {
569
- if (patternPart === '*') return true;
570
- return patternPart === sourceParts[index];
571
- });
572
- }
573
-
574
- private ipToComparable(ip: string): { version: 4 | 6; value: bigint } | undefined {
575
- const normalizedIp = this.normalizeIpLiteral(ip);
576
- const ipVersion = plugins.net.isIP(normalizedIp);
577
- if (ipVersion === 4) {
578
- const parts = normalizedIp.split('.').map((part) => Number(part));
579
- if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
580
- return undefined;
581
- }
582
- return {
583
- version: 4,
584
- value: parts.reduce((value, part) => (value << 8n) + BigInt(part), 0n),
585
- };
586
- }
587
-
588
- if (ipVersion === 6) {
589
- const parts = this.expandIpv6(normalizedIp);
590
- if (!parts) return undefined;
591
- return {
592
- version: 6,
593
- value: parts.reduce((value, part) => (value << 16n) + BigInt(part), 0n),
594
- };
595
- }
596
-
597
- return undefined;
598
- }
599
-
600
- private normalizeIpLiteral(ip: string): string {
601
- const trimmed = ip.trim().replace(/^\[|\]$/g, '');
602
- const zoneIndex = trimmed.indexOf('%');
603
- const withoutZone = zoneIndex === -1 ? trimmed : trimmed.slice(0, zoneIndex);
604
- const ipv4MappedPrefix = '::ffff:';
605
- if (withoutZone.toLowerCase().startsWith(ipv4MappedPrefix)) {
606
- const mappedIpv4 = withoutZone.slice(ipv4MappedPrefix.length);
607
- if (plugins.net.isIP(mappedIpv4) === 4) return mappedIpv4;
608
- }
609
- return withoutZone;
610
- }
611
-
612
- private expandIpv6(ip: string): number[] | undefined {
613
- let normalizedIp = ip.toLowerCase();
614
- if (normalizedIp.includes('.')) {
615
- const lastColonIndex = normalizedIp.lastIndexOf(':');
616
- const ipv4Part = normalizedIp.slice(lastColonIndex + 1);
617
- const ipv4Comparable = this.ipToComparable(ipv4Part);
618
- if (!ipv4Comparable || ipv4Comparable.version !== 4) return undefined;
619
- const high = Number((ipv4Comparable.value >> 16n) & 0xffffn).toString(16);
620
- const low = Number(ipv4Comparable.value & 0xffffn).toString(16);
621
- normalizedIp = `${normalizedIp.slice(0, lastColonIndex)}:${high}:${low}`;
622
- }
623
-
624
- const doubleColonParts = normalizedIp.split('::');
625
- if (doubleColonParts.length > 2) return undefined;
626
-
627
- const head = doubleColonParts[0] ? doubleColonParts[0].split(':') : [];
628
- const tail = doubleColonParts[1] ? doubleColonParts[1].split(':') : [];
629
- const missingCount = 8 - head.length - tail.length;
630
- if (missingCount < 0 || (doubleColonParts.length === 1 && missingCount !== 0)) return undefined;
631
-
632
- const parts = [
633
- ...head,
634
- ...Array(missingCount).fill('0'),
635
- ...tail,
636
- ];
637
- if (parts.length !== 8) return undefined;
638
-
639
- const numbers = parts.map((part) => Number.parseInt(part || '0', 16));
640
- if (numbers.some((part) => !Number.isInteger(part) || part < 0 || part > 0xffff)) {
641
- return undefined;
642
- }
643
- return numbers;
448
+ const blockEntries = Array.isArray(security?.ipBlockList)
449
+ ? security.ipBlockList
450
+ : security?.ipBlockList
451
+ ? [security.ipBlockList]
452
+ : [];
453
+ return !blockEntries.some((entry: unknown) => typeof entry === 'string' && entry.trim() === '*');
644
454
  }
645
455
 
646
456
  private getRouteDomains(route: IDcRouterRouteConfig): string[] {
@@ -152,6 +152,8 @@ export class VpnManager {
152
152
  wgListenPort,
153
153
  clients: clientEntries,
154
154
  socketForwardProxyProtocol: !isBridge,
155
+ socketForwardProxyProtocolSource: 'remoteIp',
156
+ socketForwardProxyProtocolVpnMetadata: true,
155
157
  destinationPolicy: this.getServerDestinationPolicy(forwardingMode, defaultDestinationPolicy),
156
158
  serverEndpoint,
157
159
  clientAllowedIPs: [subnet],
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '13.34.0',
6
+ version: '13.35.0',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }
@@ -97,7 +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
+ 'Source-Policy Route Grants': profile.allowRoutesByClientSourceIp ? 'Yes' : 'No',
101
101
  Created: new Date(profile.createdAt).toLocaleDateString(),
102
102
  })}
103
103
  .dataActions=${[
@@ -224,7 +224,7 @@ export class OpsViewTargetProfiles extends DeesElement {
224
224
  <dees-input-list .key=${'domains'} .label=${'Domains'} .placeholder=${'e.g. *.example.com'} .allowFreeform=${true}></dees-input-list>
225
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>
226
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>
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>
228
228
  </dees-form>
229
229
  `,
230
230
  menuOptions: [
@@ -287,7 +287,7 @@ export class OpsViewTargetProfiles extends DeesElement {
287
287
  <dees-input-list .key=${'domains'} .label=${'Domains'} .placeholder=${'e.g. *.example.com'} .allowFreeform=${true} .value=${currentDomains}></dees-input-list>
288
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>
289
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>
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>
291
291
  </dees-form>
292
292
  `,
293
293
  menuOptions: [