@serve.zone/dcrouter 12.0.0 → 12.1.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 +1052 -939
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.d.ts +11 -0
- package/dist_ts/classes.dcrouter.js +6 -1
- package/dist_ts/db/documents/classes.vpn-client.doc.d.ts +8 -0
- package/dist_ts/db/documents/classes.vpn-client.doc.js +50 -2
- package/dist_ts/opsserver/handlers/vpn.handler.js +35 -1
- package/dist_ts/vpn/classes.vpn-manager.d.ts +33 -0
- package/dist_ts/vpn/classes.vpn-manager.js +122 -7
- package/dist_ts_interfaces/data/vpn.d.ts +8 -0
- package/dist_ts_interfaces/requests/vpn.d.ts +16 -0
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/appstate.d.ts +16 -0
- package/dist_ts_web/appstate.js +17 -1
- package/dist_ts_web/elements/ops-view-vpn.js +155 -3
- package/package.json +3 -3
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +16 -0
- package/ts/db/documents/classes.vpn-client.doc.ts +24 -0
- package/ts/opsserver/handlers/vpn.handler.ts +37 -0
- package/ts/vpn/classes.vpn-manager.ts +143 -6
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/appstate.ts +32 -0
- package/ts_web/elements/ops-view-vpn.ts +153 -2
|
@@ -30,6 +30,17 @@ 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?: (clientTags: string[]) => Promise<string[]>;
|
|
33
|
+
/** Forwarding mode: 'socket' (default, userspace NAT), 'bridge' (L2 bridge to host LAN),
|
|
34
|
+
* or 'hybrid' (socket default, bridge for clients with useHostIp=true) */
|
|
35
|
+
forwardingMode?: 'socket' | 'bridge' | 'hybrid';
|
|
36
|
+
/** LAN subnet CIDR for bridge mode (e.g., '192.168.1.0/24') */
|
|
37
|
+
bridgeLanSubnet?: string;
|
|
38
|
+
/** Physical network interface for bridge mode (auto-detected if omitted) */
|
|
39
|
+
bridgePhysicalInterface?: string;
|
|
40
|
+
/** Start of VPN client IP range in LAN subnet (host offset, default: 200) */
|
|
41
|
+
bridgeIpRangeStart?: number;
|
|
42
|
+
/** End of VPN client IP range in LAN subnet (host offset, default: 250) */
|
|
43
|
+
bridgeIpRangeEnd?: number;
|
|
33
44
|
}
|
|
34
45
|
|
|
35
46
|
/**
|
|
@@ -69,8 +80,12 @@ export class VpnManager {
|
|
|
69
80
|
|
|
70
81
|
// Build client entries for the daemon
|
|
71
82
|
const clientEntries: plugins.smartvpn.IClientEntry[] = [];
|
|
83
|
+
let anyClientUsesHostIp = false;
|
|
72
84
|
for (const client of this.clients.values()) {
|
|
73
|
-
|
|
85
|
+
if (client.useHostIp) {
|
|
86
|
+
anyClientUsesHostIp = true;
|
|
87
|
+
}
|
|
88
|
+
const entry: plugins.smartvpn.IClientEntry = {
|
|
74
89
|
clientId: client.clientId,
|
|
75
90
|
publicKey: client.noisePublicKey,
|
|
76
91
|
wgPublicKey: client.wgPublicKey,
|
|
@@ -79,35 +94,65 @@ export class VpnManager {
|
|
|
79
94
|
description: client.description,
|
|
80
95
|
assignedIp: client.assignedIp,
|
|
81
96
|
expiresAt: client.expiresAt,
|
|
82
|
-
|
|
97
|
+
security: this.buildClientSecurity(client),
|
|
98
|
+
};
|
|
99
|
+
// Pass per-client bridge fields if present (for hybrid/bridge mode)
|
|
100
|
+
if (client.useHostIp !== undefined) (entry as any).useHostIp = client.useHostIp;
|
|
101
|
+
if (client.useDhcp !== undefined) (entry as any).useDhcp = client.useDhcp;
|
|
102
|
+
if (client.staticIp !== undefined) (entry as any).staticIp = client.staticIp;
|
|
103
|
+
if (client.forceVlan !== undefined) (entry as any).forceVlan = client.forceVlan;
|
|
104
|
+
if (client.vlanId !== undefined) (entry as any).vlanId = client.vlanId;
|
|
105
|
+
clientEntries.push(entry);
|
|
83
106
|
}
|
|
84
107
|
|
|
85
108
|
const subnet = this.getSubnet();
|
|
86
109
|
const wgListenPort = this.config.wgListenPort ?? 51820;
|
|
87
110
|
|
|
111
|
+
// Auto-detect hybrid mode: if any persisted client uses host IP and mode is
|
|
112
|
+
// 'socket' (or unset), upgrade to 'hybrid' so the daemon can handle both
|
|
113
|
+
let configuredMode = this.config.forwardingMode ?? 'socket';
|
|
114
|
+
if (anyClientUsesHostIp && configuredMode === 'socket') {
|
|
115
|
+
configuredMode = 'hybrid';
|
|
116
|
+
logger.log('info', 'VPN: Auto-upgrading forwarding mode to hybrid (client with useHostIp detected)');
|
|
117
|
+
}
|
|
118
|
+
const forwardingMode = configuredMode === 'hybrid' ? 'hybrid' : configuredMode;
|
|
119
|
+
const isBridge = forwardingMode === 'bridge';
|
|
120
|
+
|
|
88
121
|
// Create and start VpnServer
|
|
89
122
|
this.vpnServer = new plugins.smartvpn.VpnServer({
|
|
90
123
|
transport: { transport: 'stdio' },
|
|
91
124
|
});
|
|
92
125
|
|
|
126
|
+
// Default destination policy: bridge mode allows traffic through directly,
|
|
127
|
+
// socket mode forces traffic to SmartProxy on 127.0.0.1
|
|
128
|
+
const defaultDestinationPolicy: plugins.smartvpn.IDestinationPolicy = isBridge
|
|
129
|
+
? { default: 'allow' as const }
|
|
130
|
+
: { default: 'forceTarget' as const, target: '127.0.0.1' };
|
|
131
|
+
|
|
93
132
|
const serverConfig: plugins.smartvpn.IVpnServerConfig = {
|
|
94
133
|
listenAddr: '0.0.0.0:0', // WS listener not strictly needed but required field
|
|
95
134
|
privateKey: this.serverKeys.noisePrivateKey,
|
|
96
135
|
publicKey: this.serverKeys.noisePublicKey,
|
|
97
136
|
subnet,
|
|
98
137
|
dns: this.config.dns,
|
|
99
|
-
forwardingMode:
|
|
138
|
+
forwardingMode: forwardingMode as any,
|
|
100
139
|
transportMode: 'all',
|
|
101
140
|
wgPrivateKey: this.serverKeys.wgPrivateKey,
|
|
102
141
|
wgListenPort,
|
|
103
142
|
clients: clientEntries,
|
|
104
|
-
socketForwardProxyProtocol:
|
|
105
|
-
destinationPolicy: this.config.destinationPolicy
|
|
106
|
-
?? { default: 'forceTarget' as const, target: '127.0.0.1' },
|
|
143
|
+
socketForwardProxyProtocol: !isBridge,
|
|
144
|
+
destinationPolicy: this.config.destinationPolicy ?? defaultDestinationPolicy,
|
|
107
145
|
serverEndpoint: this.config.serverEndpoint
|
|
108
146
|
? `${this.config.serverEndpoint}:${wgListenPort}`
|
|
109
147
|
: undefined,
|
|
110
148
|
clientAllowedIPs: [subnet],
|
|
149
|
+
// Bridge-specific config
|
|
150
|
+
...(isBridge ? {
|
|
151
|
+
bridgeLanSubnet: this.config.bridgeLanSubnet,
|
|
152
|
+
bridgePhysicalInterface: this.config.bridgePhysicalInterface,
|
|
153
|
+
bridgeIpRangeStart: this.config.bridgeIpRangeStart,
|
|
154
|
+
bridgeIpRangeEnd: this.config.bridgeIpRangeEnd,
|
|
155
|
+
} : {}),
|
|
111
156
|
};
|
|
112
157
|
|
|
113
158
|
await this.vpnServer.start(serverConfig);
|
|
@@ -154,6 +199,14 @@ export class VpnManager {
|
|
|
154
199
|
clientId: string;
|
|
155
200
|
serverDefinedClientTags?: string[];
|
|
156
201
|
description?: string;
|
|
202
|
+
forceDestinationSmartproxy?: boolean;
|
|
203
|
+
destinationAllowList?: string[];
|
|
204
|
+
destinationBlockList?: string[];
|
|
205
|
+
useHostIp?: boolean;
|
|
206
|
+
useDhcp?: boolean;
|
|
207
|
+
staticIp?: string;
|
|
208
|
+
forceVlan?: boolean;
|
|
209
|
+
vlanId?: number;
|
|
157
210
|
}): Promise<plugins.smartvpn.IClientConfigBundle> {
|
|
158
211
|
if (!this.vpnServer) {
|
|
159
212
|
throw new Error('VPN server not running');
|
|
@@ -188,9 +241,39 @@ export class VpnManager {
|
|
|
188
241
|
doc.createdAt = Date.now();
|
|
189
242
|
doc.updatedAt = Date.now();
|
|
190
243
|
doc.expiresAt = bundle.entry.expiresAt;
|
|
244
|
+
if (opts.forceDestinationSmartproxy !== undefined) {
|
|
245
|
+
doc.forceDestinationSmartproxy = opts.forceDestinationSmartproxy;
|
|
246
|
+
}
|
|
247
|
+
if (opts.destinationAllowList !== undefined) {
|
|
248
|
+
doc.destinationAllowList = opts.destinationAllowList;
|
|
249
|
+
}
|
|
250
|
+
if (opts.destinationBlockList !== undefined) {
|
|
251
|
+
doc.destinationBlockList = opts.destinationBlockList;
|
|
252
|
+
}
|
|
253
|
+
if (opts.useHostIp !== undefined) {
|
|
254
|
+
doc.useHostIp = opts.useHostIp;
|
|
255
|
+
}
|
|
256
|
+
if (opts.useDhcp !== undefined) {
|
|
257
|
+
doc.useDhcp = opts.useDhcp;
|
|
258
|
+
}
|
|
259
|
+
if (opts.staticIp !== undefined) {
|
|
260
|
+
doc.staticIp = opts.staticIp;
|
|
261
|
+
}
|
|
262
|
+
if (opts.forceVlan !== undefined) {
|
|
263
|
+
doc.forceVlan = opts.forceVlan;
|
|
264
|
+
}
|
|
265
|
+
if (opts.vlanId !== undefined) {
|
|
266
|
+
doc.vlanId = opts.vlanId;
|
|
267
|
+
}
|
|
191
268
|
this.clients.set(doc.clientId, doc);
|
|
192
269
|
await this.persistClient(doc);
|
|
193
270
|
|
|
271
|
+
// Sync per-client security to the running daemon
|
|
272
|
+
const security = this.buildClientSecurity(doc);
|
|
273
|
+
if (security.destinationPolicy) {
|
|
274
|
+
await this.vpnServer!.updateClient(doc.clientId, { security });
|
|
275
|
+
}
|
|
276
|
+
|
|
194
277
|
this.config.onClientChanged?.();
|
|
195
278
|
return bundle;
|
|
196
279
|
}
|
|
@@ -254,13 +337,36 @@ export class VpnManager {
|
|
|
254
337
|
public async updateClient(clientId: string, update: {
|
|
255
338
|
description?: string;
|
|
256
339
|
serverDefinedClientTags?: string[];
|
|
340
|
+
forceDestinationSmartproxy?: boolean;
|
|
341
|
+
destinationAllowList?: string[];
|
|
342
|
+
destinationBlockList?: string[];
|
|
343
|
+
useHostIp?: boolean;
|
|
344
|
+
useDhcp?: boolean;
|
|
345
|
+
staticIp?: string;
|
|
346
|
+
forceVlan?: boolean;
|
|
347
|
+
vlanId?: number;
|
|
257
348
|
}): Promise<void> {
|
|
258
349
|
const client = this.clients.get(clientId);
|
|
259
350
|
if (!client) throw new Error(`Client not found: ${clientId}`);
|
|
260
351
|
if (update.description !== undefined) client.description = update.description;
|
|
261
352
|
if (update.serverDefinedClientTags !== undefined) client.serverDefinedClientTags = update.serverDefinedClientTags;
|
|
353
|
+
if (update.forceDestinationSmartproxy !== undefined) client.forceDestinationSmartproxy = update.forceDestinationSmartproxy;
|
|
354
|
+
if (update.destinationAllowList !== undefined) client.destinationAllowList = update.destinationAllowList;
|
|
355
|
+
if (update.destinationBlockList !== undefined) client.destinationBlockList = update.destinationBlockList;
|
|
356
|
+
if (update.useHostIp !== undefined) client.useHostIp = update.useHostIp;
|
|
357
|
+
if (update.useDhcp !== undefined) client.useDhcp = update.useDhcp;
|
|
358
|
+
if (update.staticIp !== undefined) client.staticIp = update.staticIp;
|
|
359
|
+
if (update.forceVlan !== undefined) client.forceVlan = update.forceVlan;
|
|
360
|
+
if (update.vlanId !== undefined) client.vlanId = update.vlanId;
|
|
262
361
|
client.updatedAt = Date.now();
|
|
263
362
|
await this.persistClient(client);
|
|
363
|
+
|
|
364
|
+
// Sync per-client security to the running daemon
|
|
365
|
+
if (this.vpnServer) {
|
|
366
|
+
const security = this.buildClientSecurity(client);
|
|
367
|
+
await this.vpnServer.updateClient(clientId, { security });
|
|
368
|
+
}
|
|
369
|
+
|
|
264
370
|
this.config.onClientChanged?.();
|
|
265
371
|
}
|
|
266
372
|
|
|
@@ -378,6 +484,37 @@ export class VpnManager {
|
|
|
378
484
|
};
|
|
379
485
|
}
|
|
380
486
|
|
|
487
|
+
// ── Per-client security ────────────────────────────────────────────────
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Build per-client security settings for the smartvpn daemon.
|
|
491
|
+
* Maps dcrouter-level fields (forceDestinationSmartproxy, allow/block lists)
|
|
492
|
+
* to smartvpn's IClientSecurity with a destinationPolicy.
|
|
493
|
+
*/
|
|
494
|
+
private buildClientSecurity(client: VpnClientDoc): plugins.smartvpn.IClientSecurity {
|
|
495
|
+
const security: plugins.smartvpn.IClientSecurity = {};
|
|
496
|
+
const forceSmartproxy = client.forceDestinationSmartproxy ?? true;
|
|
497
|
+
|
|
498
|
+
if (!forceSmartproxy) {
|
|
499
|
+
// Client traffic goes directly — not forced to SmartProxy
|
|
500
|
+
security.destinationPolicy = {
|
|
501
|
+
default: 'allow' as const,
|
|
502
|
+
blockList: client.destinationBlockList,
|
|
503
|
+
};
|
|
504
|
+
} else if (client.destinationAllowList?.length || client.destinationBlockList?.length) {
|
|
505
|
+
// Client is forced to SmartProxy, but with per-client allow/block overrides
|
|
506
|
+
security.destinationPolicy = {
|
|
507
|
+
default: 'forceTarget' as const,
|
|
508
|
+
target: '127.0.0.1',
|
|
509
|
+
allowList: client.destinationAllowList,
|
|
510
|
+
blockList: client.destinationBlockList,
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
// else: no per-client policy, server-wide applies
|
|
514
|
+
|
|
515
|
+
return security;
|
|
516
|
+
}
|
|
517
|
+
|
|
381
518
|
// ── Private helpers ────────────────────────────────────────────────────
|
|
382
519
|
|
|
383
520
|
private async loadOrGenerateServerKeys(): Promise<VpnServerKeysDoc> {
|
package/ts_web/appstate.ts
CHANGED
|
@@ -984,6 +984,14 @@ export const createVpnClientAction = vpnStatePart.createAction<{
|
|
|
984
984
|
clientId: string;
|
|
985
985
|
serverDefinedClientTags?: string[];
|
|
986
986
|
description?: string;
|
|
987
|
+
forceDestinationSmartproxy?: boolean;
|
|
988
|
+
destinationAllowList?: string[];
|
|
989
|
+
destinationBlockList?: string[];
|
|
990
|
+
useHostIp?: boolean;
|
|
991
|
+
useDhcp?: boolean;
|
|
992
|
+
staticIp?: string;
|
|
993
|
+
forceVlan?: boolean;
|
|
994
|
+
vlanId?: number;
|
|
987
995
|
}>(async (statePartArg, dataArg, actionContext): Promise<IVpnState> => {
|
|
988
996
|
const context = getActionContext();
|
|
989
997
|
const currentState = statePartArg.getState()!;
|
|
@@ -998,6 +1006,14 @@ export const createVpnClientAction = vpnStatePart.createAction<{
|
|
|
998
1006
|
clientId: dataArg.clientId,
|
|
999
1007
|
serverDefinedClientTags: dataArg.serverDefinedClientTags,
|
|
1000
1008
|
description: dataArg.description,
|
|
1009
|
+
forceDestinationSmartproxy: dataArg.forceDestinationSmartproxy,
|
|
1010
|
+
destinationAllowList: dataArg.destinationAllowList,
|
|
1011
|
+
destinationBlockList: dataArg.destinationBlockList,
|
|
1012
|
+
useHostIp: dataArg.useHostIp,
|
|
1013
|
+
useDhcp: dataArg.useDhcp,
|
|
1014
|
+
staticIp: dataArg.staticIp,
|
|
1015
|
+
forceVlan: dataArg.forceVlan,
|
|
1016
|
+
vlanId: dataArg.vlanId,
|
|
1001
1017
|
});
|
|
1002
1018
|
|
|
1003
1019
|
if (!response.success) {
|
|
@@ -1066,6 +1082,14 @@ export const updateVpnClientAction = vpnStatePart.createAction<{
|
|
|
1066
1082
|
clientId: string;
|
|
1067
1083
|
description?: string;
|
|
1068
1084
|
serverDefinedClientTags?: string[];
|
|
1085
|
+
forceDestinationSmartproxy?: boolean;
|
|
1086
|
+
destinationAllowList?: string[];
|
|
1087
|
+
destinationBlockList?: string[];
|
|
1088
|
+
useHostIp?: boolean;
|
|
1089
|
+
useDhcp?: boolean;
|
|
1090
|
+
staticIp?: string;
|
|
1091
|
+
forceVlan?: boolean;
|
|
1092
|
+
vlanId?: number;
|
|
1069
1093
|
}>(async (statePartArg, dataArg, actionContext): Promise<IVpnState> => {
|
|
1070
1094
|
const context = getActionContext();
|
|
1071
1095
|
const currentState = statePartArg.getState()!;
|
|
@@ -1080,6 +1104,14 @@ export const updateVpnClientAction = vpnStatePart.createAction<{
|
|
|
1080
1104
|
clientId: dataArg.clientId,
|
|
1081
1105
|
description: dataArg.description,
|
|
1082
1106
|
serverDefinedClientTags: dataArg.serverDefinedClientTags,
|
|
1107
|
+
forceDestinationSmartproxy: dataArg.forceDestinationSmartproxy,
|
|
1108
|
+
destinationAllowList: dataArg.destinationAllowList,
|
|
1109
|
+
destinationBlockList: dataArg.destinationBlockList,
|
|
1110
|
+
useHostIp: dataArg.useHostIp,
|
|
1111
|
+
useDhcp: dataArg.useDhcp,
|
|
1112
|
+
staticIp: dataArg.staticIp,
|
|
1113
|
+
forceVlan: dataArg.forceVlan,
|
|
1114
|
+
vlanId: dataArg.vlanId,
|
|
1083
1115
|
});
|
|
1084
1116
|
|
|
1085
1117
|
if (!response.success) {
|
|
@@ -13,6 +13,31 @@ import * as interfaces from '../../dist_ts_interfaces/index.js';
|
|
|
13
13
|
import { viewHostCss } from './shared/css.js';
|
|
14
14
|
import { type IStatsTile } from '@design.estate/dees-catalog';
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Toggle form field visibility based on checkbox states.
|
|
18
|
+
* Used in Create and Edit VPN client dialogs.
|
|
19
|
+
*/
|
|
20
|
+
function setupFormVisibility(formEl: any) {
|
|
21
|
+
const show = 'flex'; // match dees-form's flex layout
|
|
22
|
+
const updateVisibility = async () => {
|
|
23
|
+
const data = await formEl.collectFormData();
|
|
24
|
+
const contentEl = formEl.closest('.content') || formEl.parentElement;
|
|
25
|
+
if (!contentEl) return;
|
|
26
|
+
const hostIpGroup = contentEl.querySelector('.hostIpGroup') as HTMLElement;
|
|
27
|
+
const hostIpDetails = contentEl.querySelector('.hostIpDetails') as HTMLElement;
|
|
28
|
+
const staticIpGroup = contentEl.querySelector('.staticIpGroup') as HTMLElement;
|
|
29
|
+
const vlanIdGroup = contentEl.querySelector('.vlanIdGroup') as HTMLElement;
|
|
30
|
+
const aclGroup = contentEl.querySelector('.aclGroup') as HTMLElement;
|
|
31
|
+
if (hostIpGroup) hostIpGroup.style.display = data.forceDestinationSmartproxy ? 'none' : show;
|
|
32
|
+
if (hostIpDetails) hostIpDetails.style.display = data.useHostIp ? show : 'none';
|
|
33
|
+
if (staticIpGroup) staticIpGroup.style.display = data.useDhcp ? 'none' : show;
|
|
34
|
+
if (vlanIdGroup) vlanIdGroup.style.display = data.forceVlan ? show : 'none';
|
|
35
|
+
if (aclGroup) aclGroup.style.display = data.allowAdditionalAcls ? show : 'none';
|
|
36
|
+
};
|
|
37
|
+
formEl.changeSubject.subscribe(() => updateVisibility());
|
|
38
|
+
updateVisibility();
|
|
39
|
+
}
|
|
40
|
+
|
|
16
41
|
declare global {
|
|
17
42
|
interface HTMLElementTagNameMap {
|
|
18
43
|
'ops-view-vpn': OpsViewVpn;
|
|
@@ -289,9 +314,18 @@ export class OpsViewVpn extends DeesElement {
|
|
|
289
314
|
} else {
|
|
290
315
|
statusHtml = html`<span class="statusBadge enabled" style="background: ${cssManager.bdTheme('#eff6ff', '#172554')}; color: ${cssManager.bdTheme('#1e40af', '#60a5fa')};">offline</span>`;
|
|
291
316
|
}
|
|
317
|
+
let routingHtml;
|
|
318
|
+
if (client.forceDestinationSmartproxy !== false) {
|
|
319
|
+
routingHtml = html`<span class="statusBadge enabled">SmartProxy</span>`;
|
|
320
|
+
} else if (client.useHostIp) {
|
|
321
|
+
routingHtml = html`<span class="statusBadge" style="background: ${cssManager.bdTheme('#f3e8ff', '#3b0764')}; color: ${cssManager.bdTheme('#7c3aed', '#c084fc')};">Host IP</span>`;
|
|
322
|
+
} else {
|
|
323
|
+
routingHtml = html`<span class="statusBadge" style="background: ${cssManager.bdTheme('#eff6ff', '#172554')}; color: ${cssManager.bdTheme('#1e40af', '#60a5fa')};">Direct</span>`;
|
|
324
|
+
}
|
|
292
325
|
return {
|
|
293
326
|
'Client ID': client.clientId,
|
|
294
327
|
'Status': statusHtml,
|
|
328
|
+
'Routing': routingHtml,
|
|
295
329
|
'VPN IP': client.assignedIp || '-',
|
|
296
330
|
'Tags': client.serverDefinedClientTags?.length
|
|
297
331
|
? html`${client.serverDefinedClientTags.map(t => html`<span class="tagBadge">${t}</span>`)}`
|
|
@@ -307,13 +341,32 @@ export class OpsViewVpn extends DeesElement {
|
|
|
307
341
|
type: ['header'],
|
|
308
342
|
actionFunc: async () => {
|
|
309
343
|
const { DeesModal } = await import('@design.estate/dees-catalog');
|
|
310
|
-
await DeesModal.createAndShow({
|
|
344
|
+
const createModal = await DeesModal.createAndShow({
|
|
311
345
|
heading: 'Create VPN Client',
|
|
312
346
|
content: html`
|
|
313
347
|
<dees-form>
|
|
314
348
|
<dees-input-text .key=${'clientId'} .label=${'Client ID'} .required=${true}></dees-input-text>
|
|
315
349
|
<dees-input-text .key=${'description'} .label=${'Description'}></dees-input-text>
|
|
316
350
|
<dees-input-text .key=${'tags'} .label=${'Server-Defined Tags (comma-separated)'}></dees-input-text>
|
|
351
|
+
<dees-input-checkbox .key=${'forceDestinationSmartproxy'} .label=${'Force traffic through SmartProxy'} .value=${true}></dees-input-checkbox>
|
|
352
|
+
<div class="hostIpGroup" style="display: none; flex-direction: column; gap: 16px;">
|
|
353
|
+
<dees-input-checkbox .key=${'useHostIp'} .label=${'Get Host IP'} .value=${false}></dees-input-checkbox>
|
|
354
|
+
<div class="hostIpDetails" style="display: none; flex-direction: column; gap: 16px;">
|
|
355
|
+
<dees-input-checkbox .key=${'useDhcp'} .label=${'Get IP through DHCP'} .value=${false}></dees-input-checkbox>
|
|
356
|
+
<div class="staticIpGroup" style="display: flex; flex-direction: column; gap: 16px;">
|
|
357
|
+
<dees-input-text .key=${'staticIp'} .label=${'Static IP'}></dees-input-text>
|
|
358
|
+
</div>
|
|
359
|
+
<dees-input-checkbox .key=${'forceVlan'} .label=${'Force VLAN'} .value=${false}></dees-input-checkbox>
|
|
360
|
+
<div class="vlanIdGroup" style="display: none; flex-direction: column; gap: 16px;">
|
|
361
|
+
<dees-input-text .key=${'vlanId'} .label=${'VLAN ID'}></dees-input-text>
|
|
362
|
+
</div>
|
|
363
|
+
</div>
|
|
364
|
+
</div>
|
|
365
|
+
<dees-input-checkbox .key=${'allowAdditionalAcls'} .label=${'Allow additional ACLs'} .value=${false}></dees-input-checkbox>
|
|
366
|
+
<div class="aclGroup" style="display: none; flex-direction: column; gap: 16px;">
|
|
367
|
+
<dees-input-text .key=${'destinationAllowList'} .label=${'Destination Allow List (comma-separated IPs/CIDRs)'}></dees-input-text>
|
|
368
|
+
<dees-input-text .key=${'destinationBlockList'} .label=${'Destination Block List (comma-separated IPs/CIDRs)'}></dees-input-text>
|
|
369
|
+
</div>
|
|
317
370
|
</dees-form>
|
|
318
371
|
`,
|
|
319
372
|
menuOptions: [
|
|
@@ -333,16 +386,47 @@ export class OpsViewVpn extends DeesElement {
|
|
|
333
386
|
const serverDefinedClientTags = data.tags
|
|
334
387
|
? data.tags.split(',').map((t: string) => t.trim()).filter(Boolean)
|
|
335
388
|
: undefined;
|
|
389
|
+
|
|
390
|
+
// Apply conditional logic based on checkbox states
|
|
391
|
+
const forceSmartproxy = data.forceDestinationSmartproxy ?? true;
|
|
392
|
+
const useHostIp = !forceSmartproxy && (data.useHostIp ?? false);
|
|
393
|
+
const useDhcp = useHostIp && (data.useDhcp ?? false);
|
|
394
|
+
const staticIp = useHostIp && !useDhcp && data.staticIp ? data.staticIp : undefined;
|
|
395
|
+
const forceVlan = useHostIp && (data.forceVlan ?? false);
|
|
396
|
+
const vlanId = forceVlan && data.vlanId ? parseInt(data.vlanId, 10) : undefined;
|
|
397
|
+
|
|
398
|
+
const allowAcls = data.allowAdditionalAcls ?? false;
|
|
399
|
+
const destinationAllowList = allowAcls && data.destinationAllowList
|
|
400
|
+
? data.destinationAllowList.split(',').map((s: string) => s.trim()).filter(Boolean)
|
|
401
|
+
: undefined;
|
|
402
|
+
const destinationBlockList = allowAcls && data.destinationBlockList
|
|
403
|
+
? data.destinationBlockList.split(',').map((s: string) => s.trim()).filter(Boolean)
|
|
404
|
+
: undefined;
|
|
405
|
+
|
|
336
406
|
await appstate.vpnStatePart.dispatchAction(appstate.createVpnClientAction, {
|
|
337
407
|
clientId: data.clientId,
|
|
338
408
|
description: data.description || undefined,
|
|
339
409
|
serverDefinedClientTags,
|
|
410
|
+
forceDestinationSmartproxy: forceSmartproxy,
|
|
411
|
+
useHostIp: useHostIp || undefined,
|
|
412
|
+
useDhcp: useDhcp || undefined,
|
|
413
|
+
staticIp,
|
|
414
|
+
forceVlan: forceVlan || undefined,
|
|
415
|
+
vlanId,
|
|
416
|
+
destinationAllowList,
|
|
417
|
+
destinationBlockList,
|
|
340
418
|
});
|
|
341
419
|
await modalArg.destroy();
|
|
342
420
|
},
|
|
343
421
|
},
|
|
344
422
|
],
|
|
345
423
|
});
|
|
424
|
+
// Setup conditional form visibility after modal renders
|
|
425
|
+
const createForm = createModal?.shadowRoot?.querySelector('.content')?.querySelector('dees-form') as any;
|
|
426
|
+
if (createForm) {
|
|
427
|
+
await createForm.updateComplete;
|
|
428
|
+
setupFormVisibility(createForm);
|
|
429
|
+
}
|
|
346
430
|
},
|
|
347
431
|
},
|
|
348
432
|
{
|
|
@@ -396,6 +480,13 @@ export class OpsViewVpn extends DeesElement {
|
|
|
396
480
|
` : ''}
|
|
397
481
|
<div class="infoItem"><span class="infoLabel">Description</span><span class="infoValue">${client.description || '-'}</span></div>
|
|
398
482
|
<div class="infoItem"><span class="infoLabel">Tags</span><span class="infoValue">${client.serverDefinedClientTags?.join(', ') || '-'}</span></div>
|
|
483
|
+
<div class="infoItem"><span class="infoLabel">Routing</span><span class="infoValue">${client.forceDestinationSmartproxy !== false ? 'SmartProxy' : client.useHostIp ? 'Host IP' : 'Direct'}</span></div>
|
|
484
|
+
${client.useHostIp ? html`
|
|
485
|
+
<div class="infoItem"><span class="infoLabel">Host IP</span><span class="infoValue">${client.useDhcp ? 'DHCP' : client.staticIp ? `Static: ${client.staticIp}` : 'Not configured'}</span></div>
|
|
486
|
+
<div class="infoItem"><span class="infoLabel">VLAN</span><span class="infoValue">${client.forceVlan && client.vlanId != null ? `VLAN ${client.vlanId}` : 'No VLAN'}</span></div>
|
|
487
|
+
` : ''}
|
|
488
|
+
<div class="infoItem"><span class="infoLabel">Allow List</span><span class="infoValue">${client.destinationAllowList?.length ? client.destinationAllowList.join(', ') : 'None'}</span></div>
|
|
489
|
+
<div class="infoItem"><span class="infoLabel">Block List</span><span class="infoValue">${client.destinationBlockList?.length ? client.destinationBlockList.join(', ') : 'None'}</span></div>
|
|
399
490
|
<div class="infoItem"><span class="infoLabel">Created</span><span class="infoValue">${new Date(client.createdAt).toLocaleString()}</span></div>
|
|
400
491
|
<div class="infoItem"><span class="infoLabel">Updated</span><span class="infoValue">${new Date(client.updatedAt).toLocaleString()}</span></div>
|
|
401
492
|
</div>
|
|
@@ -553,12 +644,41 @@ export class OpsViewVpn extends DeesElement {
|
|
|
553
644
|
const { DeesModal } = await import('@design.estate/dees-catalog');
|
|
554
645
|
const currentDescription = client.description ?? '';
|
|
555
646
|
const currentTags = client.serverDefinedClientTags?.join(', ') ?? '';
|
|
556
|
-
|
|
647
|
+
const currentForceSmartproxy = client.forceDestinationSmartproxy ?? true;
|
|
648
|
+
const currentUseHostIp = client.useHostIp ?? false;
|
|
649
|
+
const currentUseDhcp = client.useDhcp ?? false;
|
|
650
|
+
const currentStaticIp = client.staticIp ?? '';
|
|
651
|
+
const currentForceVlan = client.forceVlan ?? false;
|
|
652
|
+
const currentVlanId = client.vlanId != null ? String(client.vlanId) : '';
|
|
653
|
+
const currentAllowList = client.destinationAllowList?.join(', ') ?? '';
|
|
654
|
+
const currentBlockList = client.destinationBlockList?.join(', ') ?? '';
|
|
655
|
+
const currentAllowAcls = (client.destinationAllowList?.length ?? 0) > 0
|
|
656
|
+
|| (client.destinationBlockList?.length ?? 0) > 0;
|
|
657
|
+
const editModal = await DeesModal.createAndShow({
|
|
557
658
|
heading: `Edit: ${client.clientId}`,
|
|
558
659
|
content: html`
|
|
559
660
|
<dees-form>
|
|
560
661
|
<dees-input-text .key=${'description'} .label=${'Description'} .value=${currentDescription}></dees-input-text>
|
|
561
662
|
<dees-input-text .key=${'tags'} .label=${'Server-Defined Tags (comma-separated)'} .value=${currentTags}></dees-input-text>
|
|
663
|
+
<dees-input-checkbox .key=${'forceDestinationSmartproxy'} .label=${'Force traffic through SmartProxy'} .value=${currentForceSmartproxy}></dees-input-checkbox>
|
|
664
|
+
<div class="hostIpGroup" style="display: ${currentForceSmartproxy ? 'none' : 'flex'}; flex-direction: column; gap: 16px;">
|
|
665
|
+
<dees-input-checkbox .key=${'useHostIp'} .label=${'Get Host IP'} .value=${currentUseHostIp}></dees-input-checkbox>
|
|
666
|
+
<div class="hostIpDetails" style="display: ${currentUseHostIp ? 'flex' : 'none'}; flex-direction: column; gap: 16px;">
|
|
667
|
+
<dees-input-checkbox .key=${'useDhcp'} .label=${'Get IP through DHCP'} .value=${currentUseDhcp}></dees-input-checkbox>
|
|
668
|
+
<div class="staticIpGroup" style="display: ${currentUseDhcp ? 'none' : 'flex'}; flex-direction: column; gap: 16px;">
|
|
669
|
+
<dees-input-text .key=${'staticIp'} .label=${'Static IP'} .value=${currentStaticIp}></dees-input-text>
|
|
670
|
+
</div>
|
|
671
|
+
<dees-input-checkbox .key=${'forceVlan'} .label=${'Force VLAN'} .value=${currentForceVlan}></dees-input-checkbox>
|
|
672
|
+
<div class="vlanIdGroup" style="display: ${currentForceVlan ? 'flex' : 'none'}; flex-direction: column; gap: 16px;">
|
|
673
|
+
<dees-input-text .key=${'vlanId'} .label=${'VLAN ID'} .value=${currentVlanId}></dees-input-text>
|
|
674
|
+
</div>
|
|
675
|
+
</div>
|
|
676
|
+
</div>
|
|
677
|
+
<dees-input-checkbox .key=${'allowAdditionalAcls'} .label=${'Allow additional ACLs'} .value=${currentAllowAcls}></dees-input-checkbox>
|
|
678
|
+
<div class="aclGroup" style="display: ${currentAllowAcls ? 'flex' : 'none'}; flex-direction: column; gap: 16px;">
|
|
679
|
+
<dees-input-text .key=${'destinationAllowList'} .label=${'Destination Allow List (comma-separated IPs/CIDRs)'} .value=${currentAllowList}></dees-input-text>
|
|
680
|
+
<dees-input-text .key=${'destinationBlockList'} .label=${'Destination Block List (comma-separated IPs/CIDRs)'} .value=${currentBlockList}></dees-input-text>
|
|
681
|
+
</div>
|
|
562
682
|
</dees-form>
|
|
563
683
|
`,
|
|
564
684
|
menuOptions: [
|
|
@@ -573,16 +693,47 @@ export class OpsViewVpn extends DeesElement {
|
|
|
573
693
|
const serverDefinedClientTags = data.tags
|
|
574
694
|
? data.tags.split(',').map((t: string) => t.trim()).filter(Boolean)
|
|
575
695
|
: [];
|
|
696
|
+
|
|
697
|
+
// Apply conditional logic based on checkbox states
|
|
698
|
+
const forceSmartproxy = data.forceDestinationSmartproxy ?? true;
|
|
699
|
+
const useHostIp = !forceSmartproxy && (data.useHostIp ?? false);
|
|
700
|
+
const useDhcp = useHostIp && (data.useDhcp ?? false);
|
|
701
|
+
const staticIp = useHostIp && !useDhcp && data.staticIp ? data.staticIp : undefined;
|
|
702
|
+
const forceVlan = useHostIp && (data.forceVlan ?? false);
|
|
703
|
+
const vlanId = forceVlan && data.vlanId ? parseInt(data.vlanId, 10) : undefined;
|
|
704
|
+
|
|
705
|
+
const allowAcls = data.allowAdditionalAcls ?? false;
|
|
706
|
+
const destinationAllowList = allowAcls && data.destinationAllowList
|
|
707
|
+
? data.destinationAllowList.split(',').map((s: string) => s.trim()).filter(Boolean)
|
|
708
|
+
: [];
|
|
709
|
+
const destinationBlockList = allowAcls && data.destinationBlockList
|
|
710
|
+
? data.destinationBlockList.split(',').map((s: string) => s.trim()).filter(Boolean)
|
|
711
|
+
: [];
|
|
712
|
+
|
|
576
713
|
await appstate.vpnStatePart.dispatchAction(appstate.updateVpnClientAction, {
|
|
577
714
|
clientId: client.clientId,
|
|
578
715
|
description: data.description || undefined,
|
|
579
716
|
serverDefinedClientTags,
|
|
717
|
+
forceDestinationSmartproxy: forceSmartproxy,
|
|
718
|
+
useHostIp: useHostIp || undefined,
|
|
719
|
+
useDhcp: useDhcp || undefined,
|
|
720
|
+
staticIp,
|
|
721
|
+
forceVlan: forceVlan || undefined,
|
|
722
|
+
vlanId,
|
|
723
|
+
destinationAllowList,
|
|
724
|
+
destinationBlockList,
|
|
580
725
|
});
|
|
581
726
|
await modalArg.destroy();
|
|
582
727
|
},
|
|
583
728
|
},
|
|
584
729
|
],
|
|
585
730
|
});
|
|
731
|
+
// Setup conditional form visibility for edit dialog
|
|
732
|
+
const editForm = editModal?.shadowRoot?.querySelector('.content')?.querySelector('dees-form') as any;
|
|
733
|
+
if (editForm) {
|
|
734
|
+
await editForm.updateComplete;
|
|
735
|
+
setupFormVisibility(editForm);
|
|
736
|
+
}
|
|
586
737
|
},
|
|
587
738
|
},
|
|
588
739
|
{
|