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