@serve.zone/dcrouter 13.0.6 → 13.0.8
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 +525 -525
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.js +6 -1
- package/dist_ts/config/classes.target-profile-manager.d.ts +6 -0
- package/dist_ts/config/classes.target-profile-manager.js +21 -1
- package/dist_ts/opsserver/handlers/target-profile.handler.js +5 -3
- package/dist_ts/vpn/classes.vpn-manager.d.ts +8 -0
- package/dist_ts/vpn/classes.vpn-manager.js +26 -4
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/elements/ops-view-targetprofiles.d.ts +1 -0
- package/dist_ts_web/elements/ops-view-targetprofiles.js +9 -1
- package/dist_ts_web/elements/ops-view-vpn.d.ts +14 -0
- package/dist_ts_web/elements/ops-view-vpn.js +54 -12
- package/package.json +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +4 -0
- package/ts/config/classes.target-profile-manager.ts +21 -0
- package/ts/opsserver/handlers/target-profile.handler.ts +4 -2
- package/ts/vpn/classes.vpn-manager.ts +30 -3
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/elements/ops-view-targetprofiles.ts +9 -0
- package/ts_web/elements/ops-view-vpn.ts +58 -11
|
@@ -134,6 +134,27 @@ export class TargetProfileManager {
|
|
|
134
134
|
.map((c) => ({ clientId: c.clientId, description: c.description }));
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
// =========================================================================
|
|
138
|
+
// Direct target IPs (bypass SmartProxy)
|
|
139
|
+
// =========================================================================
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* For a set of target profile IDs, collect all explicit target host IPs.
|
|
143
|
+
* These IPs bypass the SmartProxy forceTarget rewrite — VPN clients can
|
|
144
|
+
* connect to them directly through the tunnel.
|
|
145
|
+
*/
|
|
146
|
+
public getDirectTargetIps(targetProfileIds: string[]): string[] {
|
|
147
|
+
const ips = new Set<string>();
|
|
148
|
+
for (const profileId of targetProfileIds) {
|
|
149
|
+
const profile = this.profiles.get(profileId);
|
|
150
|
+
if (!profile?.targets?.length) continue;
|
|
151
|
+
for (const t of profile.targets) {
|
|
152
|
+
ips.add(t.host);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return [...ips];
|
|
156
|
+
}
|
|
157
|
+
|
|
137
158
|
// =========================================================================
|
|
138
159
|
// Core matching: route → client IPs
|
|
139
160
|
// =========================================================================
|
|
@@ -110,8 +110,9 @@ export class TargetProfileHandler {
|
|
|
110
110
|
targets: dataArg.targets,
|
|
111
111
|
routeRefs: dataArg.routeRefs,
|
|
112
112
|
});
|
|
113
|
-
// Re-apply routes to update
|
|
113
|
+
// Re-apply routes and refresh VPN client security to update access
|
|
114
114
|
this.opsServerRef.dcRouterRef.routeConfigManager?.applyRoutes();
|
|
115
|
+
this.opsServerRef.dcRouterRef.vpnManager?.refreshAllClientSecurity();
|
|
115
116
|
return { success: true };
|
|
116
117
|
},
|
|
117
118
|
),
|
|
@@ -129,8 +130,9 @@ export class TargetProfileHandler {
|
|
|
129
130
|
}
|
|
130
131
|
const result = await manager.deleteProfile(dataArg.id, dataArg.force);
|
|
131
132
|
if (result.success) {
|
|
132
|
-
// Re-apply routes to update
|
|
133
|
+
// Re-apply routes and refresh VPN client security to update access
|
|
133
134
|
this.opsServerRef.dcRouterRef.routeConfigManager?.applyRoutes();
|
|
135
|
+
this.opsServerRef.dcRouterRef.vpnManager?.refreshAllClientSecurity();
|
|
134
136
|
}
|
|
135
137
|
return result;
|
|
136
138
|
},
|
|
@@ -30,6 +30,9 @@ export interface IVpnManagerConfig {
|
|
|
30
30
|
* Called at config generation time (create/export). Returns CIDRs for WireGuard AllowedIPs.
|
|
31
31
|
* When not set, defaults to [subnet]. */
|
|
32
32
|
getClientAllowedIPs?: (targetProfileIds: string[]) => Promise<string[]>;
|
|
33
|
+
/** Resolve per-client destination allow-list IPs from target profile IDs.
|
|
34
|
+
* Returns IP strings that should bypass forceTarget and go direct to the real destination. */
|
|
35
|
+
getClientDirectTargets?: (targetProfileIds: string[]) => string[];
|
|
33
36
|
/** Forwarding mode: 'socket' (default, userspace NAT), 'bridge' (L2 bridge to host LAN),
|
|
34
37
|
* or 'hybrid' (socket default, bridge for clients with useHostIp=true) */
|
|
35
38
|
forwardingMode?: 'socket' | 'bridge' | 'hybrid';
|
|
@@ -477,18 +480,28 @@ export class VpnManager {
|
|
|
477
480
|
const security: plugins.smartvpn.IClientSecurity = {};
|
|
478
481
|
const forceSmartproxy = client.forceDestinationSmartproxy ?? true;
|
|
479
482
|
|
|
483
|
+
// Collect direct targets from assigned TargetProfiles (bypass forceTarget for these IPs)
|
|
484
|
+
const profileDirectTargets = this.config.getClientDirectTargets?.(client.targetProfileIds || []) || [];
|
|
485
|
+
|
|
486
|
+
// Merge with per-client explicit allow list
|
|
487
|
+
const mergedAllowList = [
|
|
488
|
+
...(client.destinationAllowList || []),
|
|
489
|
+
...profileDirectTargets,
|
|
490
|
+
];
|
|
491
|
+
|
|
480
492
|
if (!forceSmartproxy) {
|
|
481
493
|
// Client traffic goes directly — not forced to SmartProxy
|
|
482
494
|
security.destinationPolicy = {
|
|
483
495
|
default: 'allow' as const,
|
|
484
496
|
blockList: client.destinationBlockList,
|
|
485
497
|
};
|
|
486
|
-
} else if (
|
|
487
|
-
// Client is forced to SmartProxy, but with
|
|
498
|
+
} else if (mergedAllowList.length || client.destinationBlockList?.length) {
|
|
499
|
+
// Client is forced to SmartProxy, but with allow/block overrides
|
|
500
|
+
// (includes TargetProfile direct targets that bypass SmartProxy)
|
|
488
501
|
security.destinationPolicy = {
|
|
489
502
|
default: 'forceTarget' as const,
|
|
490
503
|
target: '127.0.0.1',
|
|
491
|
-
allowList:
|
|
504
|
+
allowList: mergedAllowList.length ? mergedAllowList : undefined,
|
|
492
505
|
blockList: client.destinationBlockList,
|
|
493
506
|
};
|
|
494
507
|
}
|
|
@@ -497,6 +510,20 @@ export class VpnManager {
|
|
|
497
510
|
return security;
|
|
498
511
|
}
|
|
499
512
|
|
|
513
|
+
/**
|
|
514
|
+
* Refresh all client security policies against the running daemon.
|
|
515
|
+
* Call this when TargetProfiles change so destination allow-lists stay in sync.
|
|
516
|
+
*/
|
|
517
|
+
public async refreshAllClientSecurity(): Promise<void> {
|
|
518
|
+
if (!this.vpnServer) return;
|
|
519
|
+
for (const client of this.clients.values()) {
|
|
520
|
+
const security = this.buildClientSecurity(client);
|
|
521
|
+
if (security.destinationPolicy) {
|
|
522
|
+
await this.vpnServer.updateClient(client.clientId, { security });
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
500
527
|
// ── Private helpers ────────────────────────────────────────────────────
|
|
501
528
|
|
|
502
529
|
private async loadOrGenerateServerKeys(): Promise<VpnServerKeysDoc> {
|
|
@@ -156,8 +156,16 @@ export class OpsViewTargetProfiles extends DeesElement {
|
|
|
156
156
|
.map((mr) => ({ viewKey: mr.route.name! }));
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
private async ensureRoutesLoaded() {
|
|
160
|
+
const routeState = appstate.routeManagementStatePart.getState();
|
|
161
|
+
if (!routeState?.mergedRoutes?.length) {
|
|
162
|
+
await appstate.routeManagementStatePart.dispatchAction(appstate.fetchMergedRoutesAction, null);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
159
166
|
private async showCreateProfileDialog() {
|
|
160
167
|
const { DeesModal } = await import('@design.estate/dees-catalog');
|
|
168
|
+
await this.ensureRoutesLoaded();
|
|
161
169
|
const routeCandidates = this.getRouteCandidates();
|
|
162
170
|
|
|
163
171
|
DeesModal.createAndShow({
|
|
@@ -216,6 +224,7 @@ export class OpsViewTargetProfiles extends DeesElement {
|
|
|
216
224
|
const currentRouteRefs = profile.routeRefs || [];
|
|
217
225
|
|
|
218
226
|
const { DeesModal } = await import('@design.estate/dees-catalog');
|
|
227
|
+
await this.ensureRoutesLoaded();
|
|
219
228
|
const routeCandidates = this.getRouteCandidates();
|
|
220
229
|
|
|
221
230
|
DeesModal.createAndShow({
|
|
@@ -60,6 +60,8 @@ export class OpsViewVpn extends DeesElement {
|
|
|
60
60
|
async connectedCallback() {
|
|
61
61
|
await super.connectedCallback();
|
|
62
62
|
await appstate.vpnStatePart.dispatchAction(appstate.fetchVpnAction, null);
|
|
63
|
+
// Ensure target profiles are loaded for autocomplete candidates
|
|
64
|
+
await appstate.targetProfilesStatePart.dispatchAction(appstate.fetchTargetProfilesAction, null);
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
public static styles = [
|
|
@@ -328,7 +330,11 @@ export class OpsViewVpn extends DeesElement {
|
|
|
328
330
|
'Routing': routingHtml,
|
|
329
331
|
'VPN IP': client.assignedIp || '-',
|
|
330
332
|
'Target Profiles': client.targetProfileIds?.length
|
|
331
|
-
? html`${client.targetProfileIds.map(
|
|
333
|
+
? html`${client.targetProfileIds.map(id => {
|
|
334
|
+
const profileState = appstate.targetProfilesStatePart.getState();
|
|
335
|
+
const profile = profileState?.profiles.find(p => p.id === id);
|
|
336
|
+
return html`<span class="tagBadge">${profile?.name || id}</span>`;
|
|
337
|
+
})}`
|
|
332
338
|
: '-',
|
|
333
339
|
'Description': client.description || '-',
|
|
334
340
|
'Created': new Date(client.createdAt).toLocaleDateString(),
|
|
@@ -341,13 +347,14 @@ export class OpsViewVpn extends DeesElement {
|
|
|
341
347
|
type: ['header'],
|
|
342
348
|
actionFunc: async () => {
|
|
343
349
|
const { DeesModal } = await import('@design.estate/dees-catalog');
|
|
350
|
+
const profileCandidates = this.getTargetProfileCandidates();
|
|
344
351
|
const createModal = await DeesModal.createAndShow({
|
|
345
352
|
heading: 'Create VPN Client',
|
|
346
353
|
content: html`
|
|
347
354
|
<dees-form>
|
|
348
355
|
<dees-input-text .key=${'clientId'} .label=${'Client ID'} .required=${true}></dees-input-text>
|
|
349
356
|
<dees-input-text .key=${'description'} .label=${'Description'}></dees-input-text>
|
|
350
|
-
<dees-input-
|
|
357
|
+
<dees-input-list .key=${'targetProfileNames'} .label=${'Target Profiles'} .placeholder=${'Type to search profiles...'} .candidates=${profileCandidates} .allowFreeform=${false}></dees-input-list>
|
|
351
358
|
<dees-input-checkbox .key=${'forceDestinationSmartproxy'} .label=${'Force traffic through SmartProxy'} .value=${true}></dees-input-checkbox>
|
|
352
359
|
<div class="hostIpGroup" style="display: none; flex-direction: column; gap: 16px;">
|
|
353
360
|
<dees-input-checkbox .key=${'useHostIp'} .label=${'Get Host IP'} .value=${false}></dees-input-checkbox>
|
|
@@ -383,9 +390,9 @@ export class OpsViewVpn extends DeesElement {
|
|
|
383
390
|
if (!form) return;
|
|
384
391
|
const data = await form.collectFormData();
|
|
385
392
|
if (!data.clientId) return;
|
|
386
|
-
const targetProfileIds =
|
|
387
|
-
|
|
388
|
-
|
|
393
|
+
const targetProfileIds = this.resolveProfileNamesToIds(
|
|
394
|
+
Array.isArray(data.targetProfileNames) ? data.targetProfileNames : [],
|
|
395
|
+
);
|
|
389
396
|
|
|
390
397
|
// Apply conditional logic based on checkbox states
|
|
391
398
|
const forceSmartproxy = data.forceDestinationSmartproxy ?? true;
|
|
@@ -479,7 +486,7 @@ export class OpsViewVpn extends DeesElement {
|
|
|
479
486
|
<div class="infoItem"><span class="infoLabel">Transport</span><span class="infoValue">${conn.transport}</span></div>
|
|
480
487
|
` : ''}
|
|
481
488
|
<div class="infoItem"><span class="infoLabel">Description</span><span class="infoValue">${client.description || '-'}</span></div>
|
|
482
|
-
<div class="infoItem"><span class="infoLabel">Target Profiles</span><span class="infoValue">${client.targetProfileIds?.join(', ') || '-'}</span></div>
|
|
489
|
+
<div class="infoItem"><span class="infoLabel">Target Profiles</span><span class="infoValue">${this.resolveProfileIdsToNames(client.targetProfileIds)?.join(', ') || '-'}</span></div>
|
|
483
490
|
<div class="infoItem"><span class="infoLabel">Routing</span><span class="infoValue">${client.forceDestinationSmartproxy !== false ? 'SmartProxy' : client.useHostIp ? 'Host IP' : 'Direct'}</span></div>
|
|
484
491
|
${client.useHostIp ? html`
|
|
485
492
|
<div class="infoItem"><span class="infoLabel">Host IP</span><span class="infoValue">${client.useDhcp ? 'DHCP' : client.staticIp ? `Static: ${client.staticIp}` : 'Not configured'}</span></div>
|
|
@@ -643,7 +650,8 @@ export class OpsViewVpn extends DeesElement {
|
|
|
643
650
|
const client = actionData.item as interfaces.data.IVpnClient;
|
|
644
651
|
const { DeesModal } = await import('@design.estate/dees-catalog');
|
|
645
652
|
const currentDescription = client.description ?? '';
|
|
646
|
-
const
|
|
653
|
+
const currentTargetProfileNames = this.resolveProfileIdsToNames(client.targetProfileIds) || [];
|
|
654
|
+
const profileCandidates = this.getTargetProfileCandidates();
|
|
647
655
|
const currentForceSmartproxy = client.forceDestinationSmartproxy ?? true;
|
|
648
656
|
const currentUseHostIp = client.useHostIp ?? false;
|
|
649
657
|
const currentUseDhcp = client.useDhcp ?? false;
|
|
@@ -659,7 +667,7 @@ export class OpsViewVpn extends DeesElement {
|
|
|
659
667
|
content: html`
|
|
660
668
|
<dees-form>
|
|
661
669
|
<dees-input-text .key=${'description'} .label=${'Description'} .value=${currentDescription}></dees-input-text>
|
|
662
|
-
<dees-input-
|
|
670
|
+
<dees-input-list .key=${'targetProfileNames'} .label=${'Target Profiles'} .placeholder=${'Type to search profiles...'} .candidates=${profileCandidates} .allowFreeform=${false} .value=${currentTargetProfileNames}></dees-input-list>
|
|
663
671
|
<dees-input-checkbox .key=${'forceDestinationSmartproxy'} .label=${'Force traffic through SmartProxy'} .value=${currentForceSmartproxy}></dees-input-checkbox>
|
|
664
672
|
<div class="hostIpGroup" style="display: ${currentForceSmartproxy ? 'none' : 'flex'}; flex-direction: column; gap: 16px;">
|
|
665
673
|
<dees-input-checkbox .key=${'useHostIp'} .label=${'Get Host IP'} .value=${currentUseHostIp}></dees-input-checkbox>
|
|
@@ -690,9 +698,9 @@ export class OpsViewVpn extends DeesElement {
|
|
|
690
698
|
const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
|
|
691
699
|
if (!form) return;
|
|
692
700
|
const data = await form.collectFormData();
|
|
693
|
-
const targetProfileIds =
|
|
694
|
-
|
|
695
|
-
|
|
701
|
+
const targetProfileIds = this.resolveProfileNamesToIds(
|
|
702
|
+
Array.isArray(data.targetProfileNames) ? data.targetProfileNames : [],
|
|
703
|
+
);
|
|
696
704
|
|
|
697
705
|
// Apply conditional logic based on checkbox states
|
|
698
706
|
const forceSmartproxy = data.forceDestinationSmartproxy ?? true;
|
|
@@ -805,4 +813,43 @@ export class OpsViewVpn extends DeesElement {
|
|
|
805
813
|
</div>
|
|
806
814
|
`;
|
|
807
815
|
}
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* Build autocomplete candidates from loaded target profiles.
|
|
819
|
+
* viewKey = profile name (displayed), payload = { id } (carried for resolution).
|
|
820
|
+
*/
|
|
821
|
+
private getTargetProfileCandidates() {
|
|
822
|
+
const profileState = appstate.targetProfilesStatePart.getState();
|
|
823
|
+
const profiles = profileState?.profiles || [];
|
|
824
|
+
return profiles.map((p) => ({ viewKey: p.name, payload: { id: p.id } }));
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* Convert profile IDs to profile names (for populating edit form values).
|
|
829
|
+
*/
|
|
830
|
+
private resolveProfileIdsToNames(ids?: string[]): string[] | undefined {
|
|
831
|
+
if (!ids?.length) return undefined;
|
|
832
|
+
const profileState = appstate.targetProfilesStatePart.getState();
|
|
833
|
+
const profiles = profileState?.profiles || [];
|
|
834
|
+
return ids.map((id) => {
|
|
835
|
+
const profile = profiles.find((p) => p.id === id);
|
|
836
|
+
return profile?.name || id;
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Convert profile names back to IDs (for saving form data).
|
|
842
|
+
* Uses the dees-input-list candidates' payload when available.
|
|
843
|
+
*/
|
|
844
|
+
private resolveProfileNamesToIds(names: string[]): string[] | undefined {
|
|
845
|
+
if (!names.length) return undefined;
|
|
846
|
+
const profileState = appstate.targetProfilesStatePart.getState();
|
|
847
|
+
const profiles = profileState?.profiles || [];
|
|
848
|
+
return names
|
|
849
|
+
.map((name) => {
|
|
850
|
+
const profile = profiles.find((p) => p.name === name);
|
|
851
|
+
return profile?.id;
|
|
852
|
+
})
|
|
853
|
+
.filter((id): id is string => !!id);
|
|
854
|
+
}
|
|
808
855
|
}
|