Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6344c2deae | |||
| c1452131fa |
@@ -1,5 +1,13 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-04-01 - 12.1.0 - feat(vpn)
|
||||
add per-client routing controls and bridge forwarding support for VPN clients
|
||||
|
||||
- adds persisted per-client VPN settings for SmartProxy enforcement, destination allow/block lists, host IP assignment, DHCP/static IP selection, and VLAN options
|
||||
- passes new VPN routing and bridge configuration through request handlers, app state, and the ops UI for creating, editing, and viewing clients
|
||||
- supports bridge and hybrid forwarding modes in the VPN manager, including auto-upgrading to hybrid when clients request host IP access
|
||||
- updates smartvpn and dees-catalog dependencies to support the new VPN forwarding capabilities
|
||||
|
||||
## 2026-03-31 - 12.0.0 - BREAKING CHANGE(db)
|
||||
replace StorageManager and CacheDb with a unified smartdata-backed database layer
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@serve.zone/dcrouter",
|
||||
"private": false,
|
||||
"version": "12.0.0",
|
||||
"version": "12.1.0",
|
||||
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
@@ -35,7 +35,7 @@
|
||||
"@api.global/typedserver": "^8.4.6",
|
||||
"@api.global/typedsocket": "^4.1.2",
|
||||
"@apiclient.xyz/cloudflare": "^7.1.0",
|
||||
"@design.estate/dees-catalog": "^3.49.0",
|
||||
"@design.estate/dees-catalog": "^3.49.1",
|
||||
"@design.estate/dees-element": "^2.2.4",
|
||||
"@push.rocks/lik": "^6.4.0",
|
||||
"@push.rocks/projectinfo": "^5.1.0",
|
||||
@@ -59,7 +59,7 @@
|
||||
"@push.rocks/smartrx": "^3.0.10",
|
||||
"@push.rocks/smartstate": "^2.3.0",
|
||||
"@push.rocks/smartunique": "^3.0.9",
|
||||
"@push.rocks/smartvpn": "1.17.1",
|
||||
"@push.rocks/smartvpn": "1.19.1",
|
||||
"@push.rocks/taskbuffer": "^8.0.2",
|
||||
"@serve.zone/catalog": "^2.9.0",
|
||||
"@serve.zone/interfaces": "^5.3.0",
|
||||
|
||||
55
pnpm-lock.yaml
generated
55
pnpm-lock.yaml
generated
@@ -24,8 +24,8 @@ importers:
|
||||
specifier: ^7.1.0
|
||||
version: 7.1.0
|
||||
'@design.estate/dees-catalog':
|
||||
specifier: ^3.49.0
|
||||
version: 3.49.0(@tiptap/pm@2.27.2)
|
||||
specifier: ^3.49.1
|
||||
version: 3.49.1(@tiptap/pm@2.27.2)
|
||||
'@design.estate/dees-element':
|
||||
specifier: ^2.2.4
|
||||
version: 2.2.4
|
||||
@@ -96,8 +96,8 @@ importers:
|
||||
specifier: ^3.0.9
|
||||
version: 3.0.9
|
||||
'@push.rocks/smartvpn':
|
||||
specifier: 1.17.1
|
||||
version: 1.17.1
|
||||
specifier: 1.19.1
|
||||
version: 1.19.1
|
||||
'@push.rocks/taskbuffer':
|
||||
specifier: ^8.0.2
|
||||
version: 8.0.2
|
||||
@@ -350,8 +350,8 @@ packages:
|
||||
'@configvault.io/interfaces@1.0.17':
|
||||
resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==}
|
||||
|
||||
'@design.estate/dees-catalog@3.49.0':
|
||||
resolution: {integrity: sha512-ZtHroyBZekv+jVSDmtGOzoGVI+EA55kd5EcSsNmUByxN3UMcFFeg62QRNzm3RHpz01u1Zfynm0bN9E44pk6FDQ==}
|
||||
'@design.estate/dees-catalog@3.49.1':
|
||||
resolution: {integrity: sha512-YyaRu6uep5wiqx2wnQeeWXstNRkkEfTAH7uA9XiWwM+TwbWH83esu5PR8L+J4akz3VsSW26JlfRI+7GoWTs2mw==}
|
||||
|
||||
'@design.estate/dees-comms@1.0.30':
|
||||
resolution: {integrity: sha512-KchMlklJfKAjQiJiR0xmofXtQ27VgZtBIxcMwPE9d+h3jJRv+lPZxzBQVOM0eyM0uS44S5vJMZ11IeV4uDXSHg==}
|
||||
@@ -359,6 +359,9 @@ packages:
|
||||
'@design.estate/dees-domtools@2.5.3':
|
||||
resolution: {integrity: sha512-E30vu4Cl49nSQAFlazT2Eo9VVR3VG3RGc2NLmVe7i8NMC/Sm2HQisXlpKMZYBOoY8YwdG8W2MiXaD0lbGyibCw==}
|
||||
|
||||
'@design.estate/dees-domtools@2.5.4':
|
||||
resolution: {integrity: sha512-IGyVKl1XMXHVCpPQXX6wSnGbD4S2Q1XkJCuuXZotu4Q86HTiALyfyZi0RouCKv3zxCSMvZHpFWVoh2DgF/3R3g==}
|
||||
|
||||
'@design.estate/dees-element@2.2.4':
|
||||
resolution: {integrity: sha512-O9cA6flBMMd+pBwMQrZXwAWel9yVxgokolb+Em6gvkXxPJ0P/B5UDn4Vc2d4ts3ta55PTBm+l2dPeDVGx/bl7Q==}
|
||||
|
||||
@@ -1339,8 +1342,8 @@ packages:
|
||||
'@push.rocks/smartversion@3.0.5':
|
||||
resolution: {integrity: sha512-8MZSo1yqyaKxKq0Q5N188l4un++9GFWVbhCAX5mXJwewZHn97ujffTeL+eOQYpWFTEpUhaq1QhL4NhqObBCt1Q==}
|
||||
|
||||
'@push.rocks/smartvpn@1.17.1':
|
||||
resolution: {integrity: sha512-oTOxNUrh+doL9AocgPnMbcYZKrWJhCeuqNotu1RfiteIV9DDdznvA+cl3nOgxD/ImUYrFPz6PUp5BEMogWcS8Q==}
|
||||
'@push.rocks/smartvpn@1.19.1':
|
||||
resolution: {integrity: sha512-zvC/rrba1tZcXzzzrhX97BEUN6smo1KcqcULu6ZAGpDNhR7c5PU8oWwFxIy33UdDf5NLActkS0L3dq42sGB8nw==}
|
||||
|
||||
'@push.rocks/smartwatch@6.4.0':
|
||||
resolution: {integrity: sha512-KDswRgE/siBmZRCsRA07MtW5oF4c9uQEBkwTGPIWneHzksbCDsvs/7agKFEL7WnNifLNwo8w1K1qoiVWkX1fvw==}
|
||||
@@ -4336,7 +4339,7 @@ snapshots:
|
||||
'@api.global/typedrequest-interfaces': 3.0.19
|
||||
'@api.global/typedsocket': 4.1.2(@push.rocks/smartserve@2.0.3)
|
||||
'@cloudflare/workers-types': 4.20260317.1
|
||||
'@design.estate/dees-catalog': 3.49.0(@tiptap/pm@2.27.2)
|
||||
'@design.estate/dees-catalog': 3.49.1(@tiptap/pm@2.27.2)
|
||||
'@design.estate/dees-comms': 1.0.30
|
||||
'@push.rocks/lik': 6.4.0
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
@@ -4865,9 +4868,9 @@ snapshots:
|
||||
dependencies:
|
||||
'@api.global/typedrequest-interfaces': 3.0.19
|
||||
|
||||
'@design.estate/dees-catalog@3.49.0(@tiptap/pm@2.27.2)':
|
||||
'@design.estate/dees-catalog@3.49.1(@tiptap/pm@2.27.2)':
|
||||
dependencies:
|
||||
'@design.estate/dees-domtools': 2.5.3
|
||||
'@design.estate/dees-domtools': 2.5.4
|
||||
'@design.estate/dees-element': 2.2.4
|
||||
'@design.estate/dees-wcctools': 3.8.0
|
||||
'@fortawesome/fontawesome-svg-core': 7.2.0
|
||||
@@ -4933,6 +4936,32 @@ snapshots:
|
||||
- supports-color
|
||||
- vue
|
||||
|
||||
'@design.estate/dees-domtools@2.5.4':
|
||||
dependencies:
|
||||
'@api.global/typedrequest': 3.3.0
|
||||
'@design.estate/dees-comms': 1.0.30
|
||||
'@push.rocks/lik': 6.4.0
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
'@push.rocks/smartjson': 6.0.0
|
||||
'@push.rocks/smartmarkdown': 3.0.3
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
'@push.rocks/smartrouter': 1.3.3
|
||||
'@push.rocks/smartrx': 3.0.10
|
||||
'@push.rocks/smartstate': 2.3.0
|
||||
'@push.rocks/smartstring': 4.1.0
|
||||
'@push.rocks/smarturl': 3.1.0
|
||||
'@push.rocks/webrequest': 4.0.5
|
||||
'@push.rocks/websetup': 3.0.19
|
||||
'@push.rocks/webstore': 2.0.21
|
||||
'@tempfix/lenis': 1.3.20
|
||||
lit: 3.3.2
|
||||
sweet-scroll: 4.0.0
|
||||
transitivePeerDependencies:
|
||||
- '@nuxt/kit'
|
||||
- react
|
||||
- supports-color
|
||||
- vue
|
||||
|
||||
'@design.estate/dees-element@2.2.4':
|
||||
dependencies:
|
||||
'@design.estate/dees-domtools': 2.5.3
|
||||
@@ -6622,7 +6651,7 @@ snapshots:
|
||||
'@types/semver': 7.7.1
|
||||
semver: 7.7.4
|
||||
|
||||
'@push.rocks/smartvpn@1.17.1':
|
||||
'@push.rocks/smartvpn@1.19.1':
|
||||
dependencies:
|
||||
'@push.rocks/smartnftables': 1.1.0
|
||||
'@push.rocks/smartpath': 6.0.0
|
||||
@@ -6867,7 +6896,7 @@ snapshots:
|
||||
|
||||
'@serve.zone/catalog@2.9.0(@tiptap/pm@2.27.2)':
|
||||
dependencies:
|
||||
'@design.estate/dees-catalog': 3.49.0(@tiptap/pm@2.27.2)
|
||||
'@design.estate/dees-catalog': 3.49.1(@tiptap/pm@2.27.2)
|
||||
'@design.estate/dees-domtools': 2.5.3
|
||||
'@design.estate/dees-element': 2.2.4
|
||||
'@design.estate/dees-wcctools': 3.8.0
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/dcrouter',
|
||||
version: '12.0.0',
|
||||
version: '12.1.0',
|
||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||
}
|
||||
|
||||
@@ -205,6 +205,17 @@ export interface IDcRouterOptions {
|
||||
allowList?: string[];
|
||||
blockList?: string[];
|
||||
};
|
||||
/** Forwarding mode: 'socket' (default, userspace NAT), 'bridge' (L2 bridge to host LAN),
|
||||
* or 'hybrid' (socket default, bridge for clients with useHostIp=true) */
|
||||
forwardingMode?: 'socket' | 'bridge' | 'hybrid';
|
||||
/** LAN subnet CIDR for bridge mode (e.g., '192.168.1.0/24') */
|
||||
bridgeLanSubnet?: string;
|
||||
/** Physical network interface for bridge mode (auto-detected if omitted) */
|
||||
bridgePhysicalInterface?: string;
|
||||
/** Start of VPN client IP range in LAN subnet (host offset, default: 200) */
|
||||
bridgeIpRangeStart?: number;
|
||||
/** End of VPN client IP range in LAN subnet (host offset, default: 250) */
|
||||
bridgeIpRangeEnd?: number;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2085,6 +2096,11 @@ export class DcRouter {
|
||||
serverEndpoint: this.options.vpnConfig.serverEndpoint,
|
||||
initialClients: this.options.vpnConfig.clients,
|
||||
destinationPolicy: this.options.vpnConfig.destinationPolicy,
|
||||
forwardingMode: this.options.vpnConfig.forwardingMode,
|
||||
bridgeLanSubnet: this.options.vpnConfig.bridgeLanSubnet,
|
||||
bridgePhysicalInterface: this.options.vpnConfig.bridgePhysicalInterface,
|
||||
bridgeIpRangeStart: this.options.vpnConfig.bridgeIpRangeStart,
|
||||
bridgeIpRangeEnd: this.options.vpnConfig.bridgeIpRangeEnd,
|
||||
onClientChanged: () => {
|
||||
// Re-apply routes so tag-based ipAllowLists get updated
|
||||
this.routeConfigManager?.applyRoutes();
|
||||
|
||||
@@ -39,6 +39,30 @@ export class VpnClientDoc extends plugins.smartdata.SmartDataDbDoc<VpnClientDoc,
|
||||
@plugins.smartdata.svDb()
|
||||
public expiresAt?: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public forceDestinationSmartproxy: boolean = true;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public destinationAllowList?: string[];
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public destinationBlockList?: string[];
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public useHostIp?: boolean;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public useDhcp?: boolean;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public staticIp?: string;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public forceVlan?: boolean;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public vlanId?: number;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
@@ -31,6 +31,14 @@ export class VpnHandler {
|
||||
createdAt: c.createdAt,
|
||||
updatedAt: c.updatedAt,
|
||||
expiresAt: c.expiresAt,
|
||||
forceDestinationSmartproxy: c.forceDestinationSmartproxy ?? true,
|
||||
destinationAllowList: c.destinationAllowList,
|
||||
destinationBlockList: c.destinationBlockList,
|
||||
useHostIp: c.useHostIp,
|
||||
useDhcp: c.useDhcp,
|
||||
staticIp: c.staticIp,
|
||||
forceVlan: c.forceVlan,
|
||||
vlanId: c.vlanId,
|
||||
}));
|
||||
return { clients };
|
||||
},
|
||||
@@ -114,8 +122,21 @@ export class VpnHandler {
|
||||
clientId: dataArg.clientId,
|
||||
serverDefinedClientTags: dataArg.serverDefinedClientTags,
|
||||
description: dataArg.description,
|
||||
forceDestinationSmartproxy: dataArg.forceDestinationSmartproxy,
|
||||
destinationAllowList: dataArg.destinationAllowList,
|
||||
destinationBlockList: dataArg.destinationBlockList,
|
||||
useHostIp: dataArg.useHostIp,
|
||||
useDhcp: dataArg.useDhcp,
|
||||
staticIp: dataArg.staticIp,
|
||||
forceVlan: dataArg.forceVlan,
|
||||
vlanId: dataArg.vlanId,
|
||||
});
|
||||
|
||||
// Retrieve the persisted doc to get dcrouter-level fields
|
||||
const persistedClient = manager.listClients().find(
|
||||
(c) => c.clientId === bundle.entry.clientId,
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
client: {
|
||||
@@ -127,6 +148,14 @@ export class VpnHandler {
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
expiresAt: bundle.entry.expiresAt,
|
||||
forceDestinationSmartproxy: persistedClient?.forceDestinationSmartproxy ?? true,
|
||||
destinationAllowList: persistedClient?.destinationAllowList,
|
||||
destinationBlockList: persistedClient?.destinationBlockList,
|
||||
useHostIp: persistedClient?.useHostIp,
|
||||
useDhcp: persistedClient?.useDhcp,
|
||||
staticIp: persistedClient?.staticIp,
|
||||
forceVlan: persistedClient?.forceVlan,
|
||||
vlanId: persistedClient?.vlanId,
|
||||
},
|
||||
wireguardConfig: bundle.wireguardConfig,
|
||||
};
|
||||
@@ -151,6 +180,14 @@ export class VpnHandler {
|
||||
await manager.updateClient(dataArg.clientId, {
|
||||
description: dataArg.description,
|
||||
serverDefinedClientTags: dataArg.serverDefinedClientTags,
|
||||
forceDestinationSmartproxy: dataArg.forceDestinationSmartproxy,
|
||||
destinationAllowList: dataArg.destinationAllowList,
|
||||
destinationBlockList: dataArg.destinationBlockList,
|
||||
useHostIp: dataArg.useHostIp,
|
||||
useDhcp: dataArg.useDhcp,
|
||||
staticIp: dataArg.staticIp,
|
||||
forceVlan: dataArg.forceVlan,
|
||||
vlanId: dataArg.vlanId,
|
||||
});
|
||||
return { success: true };
|
||||
} catch (err: unknown) {
|
||||
|
||||
@@ -30,6 +30,17 @@ export interface IVpnManagerConfig {
|
||||
* Called at config generation time (create/export). Returns CIDRs for WireGuard AllowedIPs.
|
||||
* When not set, defaults to [subnet]. */
|
||||
getClientAllowedIPs?: (clientTags: string[]) => Promise<string[]>;
|
||||
/** Forwarding mode: 'socket' (default, userspace NAT), 'bridge' (L2 bridge to host LAN),
|
||||
* or 'hybrid' (socket default, bridge for clients with useHostIp=true) */
|
||||
forwardingMode?: 'socket' | 'bridge' | 'hybrid';
|
||||
/** LAN subnet CIDR for bridge mode (e.g., '192.168.1.0/24') */
|
||||
bridgeLanSubnet?: string;
|
||||
/** Physical network interface for bridge mode (auto-detected if omitted) */
|
||||
bridgePhysicalInterface?: string;
|
||||
/** Start of VPN client IP range in LAN subnet (host offset, default: 200) */
|
||||
bridgeIpRangeStart?: number;
|
||||
/** End of VPN client IP range in LAN subnet (host offset, default: 250) */
|
||||
bridgeIpRangeEnd?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,8 +80,12 @@ export class VpnManager {
|
||||
|
||||
// Build client entries for the daemon
|
||||
const clientEntries: plugins.smartvpn.IClientEntry[] = [];
|
||||
let anyClientUsesHostIp = false;
|
||||
for (const client of this.clients.values()) {
|
||||
clientEntries.push({
|
||||
if (client.useHostIp) {
|
||||
anyClientUsesHostIp = true;
|
||||
}
|
||||
const entry: plugins.smartvpn.IClientEntry = {
|
||||
clientId: client.clientId,
|
||||
publicKey: client.noisePublicKey,
|
||||
wgPublicKey: client.wgPublicKey,
|
||||
@@ -79,35 +94,65 @@ export class VpnManager {
|
||||
description: client.description,
|
||||
assignedIp: client.assignedIp,
|
||||
expiresAt: client.expiresAt,
|
||||
});
|
||||
security: this.buildClientSecurity(client),
|
||||
};
|
||||
// Pass per-client bridge fields if present (for hybrid/bridge mode)
|
||||
if (client.useHostIp !== undefined) (entry as any).useHostIp = client.useHostIp;
|
||||
if (client.useDhcp !== undefined) (entry as any).useDhcp = client.useDhcp;
|
||||
if (client.staticIp !== undefined) (entry as any).staticIp = client.staticIp;
|
||||
if (client.forceVlan !== undefined) (entry as any).forceVlan = client.forceVlan;
|
||||
if (client.vlanId !== undefined) (entry as any).vlanId = client.vlanId;
|
||||
clientEntries.push(entry);
|
||||
}
|
||||
|
||||
const subnet = this.getSubnet();
|
||||
const wgListenPort = this.config.wgListenPort ?? 51820;
|
||||
|
||||
// Auto-detect hybrid mode: if any persisted client uses host IP and mode is
|
||||
// 'socket' (or unset), upgrade to 'hybrid' so the daemon can handle both
|
||||
let configuredMode = this.config.forwardingMode ?? 'socket';
|
||||
if (anyClientUsesHostIp && configuredMode === 'socket') {
|
||||
configuredMode = 'hybrid';
|
||||
logger.log('info', 'VPN: Auto-upgrading forwarding mode to hybrid (client with useHostIp detected)');
|
||||
}
|
||||
const forwardingMode = configuredMode === 'hybrid' ? 'hybrid' : configuredMode;
|
||||
const isBridge = forwardingMode === 'bridge';
|
||||
|
||||
// Create and start VpnServer
|
||||
this.vpnServer = new plugins.smartvpn.VpnServer({
|
||||
transport: { transport: 'stdio' },
|
||||
});
|
||||
|
||||
// Default destination policy: bridge mode allows traffic through directly,
|
||||
// socket mode forces traffic to SmartProxy on 127.0.0.1
|
||||
const defaultDestinationPolicy: plugins.smartvpn.IDestinationPolicy = isBridge
|
||||
? { default: 'allow' as const }
|
||||
: { default: 'forceTarget' as const, target: '127.0.0.1' };
|
||||
|
||||
const serverConfig: plugins.smartvpn.IVpnServerConfig = {
|
||||
listenAddr: '0.0.0.0:0', // WS listener not strictly needed but required field
|
||||
privateKey: this.serverKeys.noisePrivateKey,
|
||||
publicKey: this.serverKeys.noisePublicKey,
|
||||
subnet,
|
||||
dns: this.config.dns,
|
||||
forwardingMode: 'socket',
|
||||
forwardingMode: forwardingMode as any,
|
||||
transportMode: 'all',
|
||||
wgPrivateKey: this.serverKeys.wgPrivateKey,
|
||||
wgListenPort,
|
||||
clients: clientEntries,
|
||||
socketForwardProxyProtocol: true,
|
||||
destinationPolicy: this.config.destinationPolicy
|
||||
?? { default: 'forceTarget' as const, target: '127.0.0.1' },
|
||||
socketForwardProxyProtocol: !isBridge,
|
||||
destinationPolicy: this.config.destinationPolicy ?? defaultDestinationPolicy,
|
||||
serverEndpoint: this.config.serverEndpoint
|
||||
? `${this.config.serverEndpoint}:${wgListenPort}`
|
||||
: undefined,
|
||||
clientAllowedIPs: [subnet],
|
||||
// Bridge-specific config
|
||||
...(isBridge ? {
|
||||
bridgeLanSubnet: this.config.bridgeLanSubnet,
|
||||
bridgePhysicalInterface: this.config.bridgePhysicalInterface,
|
||||
bridgeIpRangeStart: this.config.bridgeIpRangeStart,
|
||||
bridgeIpRangeEnd: this.config.bridgeIpRangeEnd,
|
||||
} : {}),
|
||||
};
|
||||
|
||||
await this.vpnServer.start(serverConfig);
|
||||
@@ -154,6 +199,14 @@ export class VpnManager {
|
||||
clientId: string;
|
||||
serverDefinedClientTags?: string[];
|
||||
description?: string;
|
||||
forceDestinationSmartproxy?: boolean;
|
||||
destinationAllowList?: string[];
|
||||
destinationBlockList?: string[];
|
||||
useHostIp?: boolean;
|
||||
useDhcp?: boolean;
|
||||
staticIp?: string;
|
||||
forceVlan?: boolean;
|
||||
vlanId?: number;
|
||||
}): Promise<plugins.smartvpn.IClientConfigBundle> {
|
||||
if (!this.vpnServer) {
|
||||
throw new Error('VPN server not running');
|
||||
@@ -188,9 +241,39 @@ export class VpnManager {
|
||||
doc.createdAt = Date.now();
|
||||
doc.updatedAt = Date.now();
|
||||
doc.expiresAt = bundle.entry.expiresAt;
|
||||
if (opts.forceDestinationSmartproxy !== undefined) {
|
||||
doc.forceDestinationSmartproxy = opts.forceDestinationSmartproxy;
|
||||
}
|
||||
if (opts.destinationAllowList !== undefined) {
|
||||
doc.destinationAllowList = opts.destinationAllowList;
|
||||
}
|
||||
if (opts.destinationBlockList !== undefined) {
|
||||
doc.destinationBlockList = opts.destinationBlockList;
|
||||
}
|
||||
if (opts.useHostIp !== undefined) {
|
||||
doc.useHostIp = opts.useHostIp;
|
||||
}
|
||||
if (opts.useDhcp !== undefined) {
|
||||
doc.useDhcp = opts.useDhcp;
|
||||
}
|
||||
if (opts.staticIp !== undefined) {
|
||||
doc.staticIp = opts.staticIp;
|
||||
}
|
||||
if (opts.forceVlan !== undefined) {
|
||||
doc.forceVlan = opts.forceVlan;
|
||||
}
|
||||
if (opts.vlanId !== undefined) {
|
||||
doc.vlanId = opts.vlanId;
|
||||
}
|
||||
this.clients.set(doc.clientId, doc);
|
||||
await this.persistClient(doc);
|
||||
|
||||
// Sync per-client security to the running daemon
|
||||
const security = this.buildClientSecurity(doc);
|
||||
if (security.destinationPolicy) {
|
||||
await this.vpnServer!.updateClient(doc.clientId, { security });
|
||||
}
|
||||
|
||||
this.config.onClientChanged?.();
|
||||
return bundle;
|
||||
}
|
||||
@@ -254,13 +337,36 @@ export class VpnManager {
|
||||
public async updateClient(clientId: string, update: {
|
||||
description?: string;
|
||||
serverDefinedClientTags?: string[];
|
||||
forceDestinationSmartproxy?: boolean;
|
||||
destinationAllowList?: string[];
|
||||
destinationBlockList?: string[];
|
||||
useHostIp?: boolean;
|
||||
useDhcp?: boolean;
|
||||
staticIp?: string;
|
||||
forceVlan?: boolean;
|
||||
vlanId?: number;
|
||||
}): Promise<void> {
|
||||
const client = this.clients.get(clientId);
|
||||
if (!client) throw new Error(`Client not found: ${clientId}`);
|
||||
if (update.description !== undefined) client.description = update.description;
|
||||
if (update.serverDefinedClientTags !== undefined) client.serverDefinedClientTags = update.serverDefinedClientTags;
|
||||
if (update.forceDestinationSmartproxy !== undefined) client.forceDestinationSmartproxy = update.forceDestinationSmartproxy;
|
||||
if (update.destinationAllowList !== undefined) client.destinationAllowList = update.destinationAllowList;
|
||||
if (update.destinationBlockList !== undefined) client.destinationBlockList = update.destinationBlockList;
|
||||
if (update.useHostIp !== undefined) client.useHostIp = update.useHostIp;
|
||||
if (update.useDhcp !== undefined) client.useDhcp = update.useDhcp;
|
||||
if (update.staticIp !== undefined) client.staticIp = update.staticIp;
|
||||
if (update.forceVlan !== undefined) client.forceVlan = update.forceVlan;
|
||||
if (update.vlanId !== undefined) client.vlanId = update.vlanId;
|
||||
client.updatedAt = Date.now();
|
||||
await this.persistClient(client);
|
||||
|
||||
// Sync per-client security to the running daemon
|
||||
if (this.vpnServer) {
|
||||
const security = this.buildClientSecurity(client);
|
||||
await this.vpnServer.updateClient(clientId, { security });
|
||||
}
|
||||
|
||||
this.config.onClientChanged?.();
|
||||
}
|
||||
|
||||
@@ -378,6 +484,37 @@ export class VpnManager {
|
||||
};
|
||||
}
|
||||
|
||||
// ── Per-client security ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Build per-client security settings for the smartvpn daemon.
|
||||
* Maps dcrouter-level fields (forceDestinationSmartproxy, allow/block lists)
|
||||
* to smartvpn's IClientSecurity with a destinationPolicy.
|
||||
*/
|
||||
private buildClientSecurity(client: VpnClientDoc): plugins.smartvpn.IClientSecurity {
|
||||
const security: plugins.smartvpn.IClientSecurity = {};
|
||||
const forceSmartproxy = client.forceDestinationSmartproxy ?? true;
|
||||
|
||||
if (!forceSmartproxy) {
|
||||
// Client traffic goes directly — not forced to SmartProxy
|
||||
security.destinationPolicy = {
|
||||
default: 'allow' as const,
|
||||
blockList: client.destinationBlockList,
|
||||
};
|
||||
} else if (client.destinationAllowList?.length || client.destinationBlockList?.length) {
|
||||
// Client is forced to SmartProxy, but with per-client allow/block overrides
|
||||
security.destinationPolicy = {
|
||||
default: 'forceTarget' as const,
|
||||
target: '127.0.0.1',
|
||||
allowList: client.destinationAllowList,
|
||||
blockList: client.destinationBlockList,
|
||||
};
|
||||
}
|
||||
// else: no per-client policy, server-wide applies
|
||||
|
||||
return security;
|
||||
}
|
||||
|
||||
// ── Private helpers ────────────────────────────────────────────────────
|
||||
|
||||
private async loadOrGenerateServerKeys(): Promise<VpnServerKeysDoc> {
|
||||
|
||||
@@ -10,6 +10,14 @@ export interface IVpnClient {
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
expiresAt?: string;
|
||||
forceDestinationSmartproxy: boolean;
|
||||
destinationAllowList?: string[];
|
||||
destinationBlockList?: string[];
|
||||
useHostIp?: boolean;
|
||||
useDhcp?: boolean;
|
||||
staticIp?: string;
|
||||
forceVlan?: boolean;
|
||||
vlanId?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -51,6 +51,14 @@ export interface IReq_CreateVpnClient extends plugins.typedrequestInterfaces.imp
|
||||
clientId: string;
|
||||
serverDefinedClientTags?: string[];
|
||||
description?: string;
|
||||
forceDestinationSmartproxy?: boolean;
|
||||
destinationAllowList?: string[];
|
||||
destinationBlockList?: string[];
|
||||
useHostIp?: boolean;
|
||||
useDhcp?: boolean;
|
||||
staticIp?: string;
|
||||
forceVlan?: boolean;
|
||||
vlanId?: number;
|
||||
};
|
||||
response: {
|
||||
success: boolean;
|
||||
@@ -74,6 +82,14 @@ export interface IReq_UpdateVpnClient extends plugins.typedrequestInterfaces.imp
|
||||
clientId: string;
|
||||
description?: string;
|
||||
serverDefinedClientTags?: string[];
|
||||
forceDestinationSmartproxy?: boolean;
|
||||
destinationAllowList?: string[];
|
||||
destinationBlockList?: string[];
|
||||
useHostIp?: boolean;
|
||||
useDhcp?: boolean;
|
||||
staticIp?: string;
|
||||
forceVlan?: boolean;
|
||||
vlanId?: number;
|
||||
};
|
||||
response: {
|
||||
success: boolean;
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/dcrouter',
|
||||
version: '12.0.0',
|
||||
version: '12.1.0',
|
||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||
}
|
||||
|
||||
@@ -984,6 +984,14 @@ export const createVpnClientAction = vpnStatePart.createAction<{
|
||||
clientId: string;
|
||||
serverDefinedClientTags?: string[];
|
||||
description?: string;
|
||||
forceDestinationSmartproxy?: boolean;
|
||||
destinationAllowList?: string[];
|
||||
destinationBlockList?: string[];
|
||||
useHostIp?: boolean;
|
||||
useDhcp?: boolean;
|
||||
staticIp?: string;
|
||||
forceVlan?: boolean;
|
||||
vlanId?: number;
|
||||
}>(async (statePartArg, dataArg, actionContext): Promise<IVpnState> => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState()!;
|
||||
@@ -998,6 +1006,14 @@ export const createVpnClientAction = vpnStatePart.createAction<{
|
||||
clientId: dataArg.clientId,
|
||||
serverDefinedClientTags: dataArg.serverDefinedClientTags,
|
||||
description: dataArg.description,
|
||||
forceDestinationSmartproxy: dataArg.forceDestinationSmartproxy,
|
||||
destinationAllowList: dataArg.destinationAllowList,
|
||||
destinationBlockList: dataArg.destinationBlockList,
|
||||
useHostIp: dataArg.useHostIp,
|
||||
useDhcp: dataArg.useDhcp,
|
||||
staticIp: dataArg.staticIp,
|
||||
forceVlan: dataArg.forceVlan,
|
||||
vlanId: dataArg.vlanId,
|
||||
});
|
||||
|
||||
if (!response.success) {
|
||||
@@ -1066,6 +1082,14 @@ export const updateVpnClientAction = vpnStatePart.createAction<{
|
||||
clientId: string;
|
||||
description?: string;
|
||||
serverDefinedClientTags?: string[];
|
||||
forceDestinationSmartproxy?: boolean;
|
||||
destinationAllowList?: string[];
|
||||
destinationBlockList?: string[];
|
||||
useHostIp?: boolean;
|
||||
useDhcp?: boolean;
|
||||
staticIp?: string;
|
||||
forceVlan?: boolean;
|
||||
vlanId?: number;
|
||||
}>(async (statePartArg, dataArg, actionContext): Promise<IVpnState> => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState()!;
|
||||
@@ -1080,6 +1104,14 @@ export const updateVpnClientAction = vpnStatePart.createAction<{
|
||||
clientId: dataArg.clientId,
|
||||
description: dataArg.description,
|
||||
serverDefinedClientTags: dataArg.serverDefinedClientTags,
|
||||
forceDestinationSmartproxy: dataArg.forceDestinationSmartproxy,
|
||||
destinationAllowList: dataArg.destinationAllowList,
|
||||
destinationBlockList: dataArg.destinationBlockList,
|
||||
useHostIp: dataArg.useHostIp,
|
||||
useDhcp: dataArg.useDhcp,
|
||||
staticIp: dataArg.staticIp,
|
||||
forceVlan: dataArg.forceVlan,
|
||||
vlanId: dataArg.vlanId,
|
||||
});
|
||||
|
||||
if (!response.success) {
|
||||
|
||||
@@ -13,6 +13,31 @@ import * as interfaces from '../../dist_ts_interfaces/index.js';
|
||||
import { viewHostCss } from './shared/css.js';
|
||||
import { type IStatsTile } from '@design.estate/dees-catalog';
|
||||
|
||||
/**
|
||||
* Toggle form field visibility based on checkbox states.
|
||||
* Used in Create and Edit VPN client dialogs.
|
||||
*/
|
||||
function setupFormVisibility(formEl: any) {
|
||||
const show = 'flex'; // match dees-form's flex layout
|
||||
const updateVisibility = async () => {
|
||||
const data = await formEl.collectFormData();
|
||||
const contentEl = formEl.closest('.content') || formEl.parentElement;
|
||||
if (!contentEl) return;
|
||||
const hostIpGroup = contentEl.querySelector('.hostIpGroup') as HTMLElement;
|
||||
const hostIpDetails = contentEl.querySelector('.hostIpDetails') as HTMLElement;
|
||||
const staticIpGroup = contentEl.querySelector('.staticIpGroup') as HTMLElement;
|
||||
const vlanIdGroup = contentEl.querySelector('.vlanIdGroup') as HTMLElement;
|
||||
const aclGroup = contentEl.querySelector('.aclGroup') as HTMLElement;
|
||||
if (hostIpGroup) hostIpGroup.style.display = data.forceDestinationSmartproxy ? 'none' : show;
|
||||
if (hostIpDetails) hostIpDetails.style.display = data.useHostIp ? show : 'none';
|
||||
if (staticIpGroup) staticIpGroup.style.display = data.useDhcp ? 'none' : show;
|
||||
if (vlanIdGroup) vlanIdGroup.style.display = data.forceVlan ? show : 'none';
|
||||
if (aclGroup) aclGroup.style.display = data.allowAdditionalAcls ? show : 'none';
|
||||
};
|
||||
formEl.changeSubject.subscribe(() => updateVisibility());
|
||||
updateVisibility();
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'ops-view-vpn': OpsViewVpn;
|
||||
@@ -289,9 +314,18 @@ export class OpsViewVpn extends DeesElement {
|
||||
} else {
|
||||
statusHtml = html`<span class="statusBadge enabled" style="background: ${cssManager.bdTheme('#eff6ff', '#172554')}; color: ${cssManager.bdTheme('#1e40af', '#60a5fa')};">offline</span>`;
|
||||
}
|
||||
let routingHtml;
|
||||
if (client.forceDestinationSmartproxy !== false) {
|
||||
routingHtml = html`<span class="statusBadge enabled">SmartProxy</span>`;
|
||||
} else if (client.useHostIp) {
|
||||
routingHtml = html`<span class="statusBadge" style="background: ${cssManager.bdTheme('#f3e8ff', '#3b0764')}; color: ${cssManager.bdTheme('#7c3aed', '#c084fc')};">Host IP</span>`;
|
||||
} else {
|
||||
routingHtml = html`<span class="statusBadge" style="background: ${cssManager.bdTheme('#eff6ff', '#172554')}; color: ${cssManager.bdTheme('#1e40af', '#60a5fa')};">Direct</span>`;
|
||||
}
|
||||
return {
|
||||
'Client ID': client.clientId,
|
||||
'Status': statusHtml,
|
||||
'Routing': routingHtml,
|
||||
'VPN IP': client.assignedIp || '-',
|
||||
'Tags': client.serverDefinedClientTags?.length
|
||||
? html`${client.serverDefinedClientTags.map(t => html`<span class="tagBadge">${t}</span>`)}`
|
||||
@@ -307,13 +341,32 @@ export class OpsViewVpn extends DeesElement {
|
||||
type: ['header'],
|
||||
actionFunc: async () => {
|
||||
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||
await DeesModal.createAndShow({
|
||||
const createModal = await DeesModal.createAndShow({
|
||||
heading: 'Create VPN Client',
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-text .key=${'clientId'} .label=${'Client ID'} .required=${true}></dees-input-text>
|
||||
<dees-input-text .key=${'description'} .label=${'Description'}></dees-input-text>
|
||||
<dees-input-text .key=${'tags'} .label=${'Server-Defined Tags (comma-separated)'}></dees-input-text>
|
||||
<dees-input-checkbox .key=${'forceDestinationSmartproxy'} .label=${'Force traffic through SmartProxy'} .value=${true}></dees-input-checkbox>
|
||||
<div class="hostIpGroup" style="display: none; flex-direction: column; gap: 16px;">
|
||||
<dees-input-checkbox .key=${'useHostIp'} .label=${'Get Host IP'} .value=${false}></dees-input-checkbox>
|
||||
<div class="hostIpDetails" style="display: none; flex-direction: column; gap: 16px;">
|
||||
<dees-input-checkbox .key=${'useDhcp'} .label=${'Get IP through DHCP'} .value=${false}></dees-input-checkbox>
|
||||
<div class="staticIpGroup" style="display: flex; flex-direction: column; gap: 16px;">
|
||||
<dees-input-text .key=${'staticIp'} .label=${'Static IP'}></dees-input-text>
|
||||
</div>
|
||||
<dees-input-checkbox .key=${'forceVlan'} .label=${'Force VLAN'} .value=${false}></dees-input-checkbox>
|
||||
<div class="vlanIdGroup" style="display: none; flex-direction: column; gap: 16px;">
|
||||
<dees-input-text .key=${'vlanId'} .label=${'VLAN ID'}></dees-input-text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<dees-input-checkbox .key=${'allowAdditionalAcls'} .label=${'Allow additional ACLs'} .value=${false}></dees-input-checkbox>
|
||||
<div class="aclGroup" style="display: none; flex-direction: column; gap: 16px;">
|
||||
<dees-input-text .key=${'destinationAllowList'} .label=${'Destination Allow List (comma-separated IPs/CIDRs)'}></dees-input-text>
|
||||
<dees-input-text .key=${'destinationBlockList'} .label=${'Destination Block List (comma-separated IPs/CIDRs)'}></dees-input-text>
|
||||
</div>
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
@@ -333,16 +386,47 @@ export class OpsViewVpn extends DeesElement {
|
||||
const serverDefinedClientTags = data.tags
|
||||
? data.tags.split(',').map((t: string) => t.trim()).filter(Boolean)
|
||||
: undefined;
|
||||
|
||||
// Apply conditional logic based on checkbox states
|
||||
const forceSmartproxy = data.forceDestinationSmartproxy ?? true;
|
||||
const useHostIp = !forceSmartproxy && (data.useHostIp ?? false);
|
||||
const useDhcp = useHostIp && (data.useDhcp ?? false);
|
||||
const staticIp = useHostIp && !useDhcp && data.staticIp ? data.staticIp : undefined;
|
||||
const forceVlan = useHostIp && (data.forceVlan ?? false);
|
||||
const vlanId = forceVlan && data.vlanId ? parseInt(data.vlanId, 10) : undefined;
|
||||
|
||||
const allowAcls = data.allowAdditionalAcls ?? false;
|
||||
const destinationAllowList = allowAcls && data.destinationAllowList
|
||||
? data.destinationAllowList.split(',').map((s: string) => s.trim()).filter(Boolean)
|
||||
: undefined;
|
||||
const destinationBlockList = allowAcls && data.destinationBlockList
|
||||
? data.destinationBlockList.split(',').map((s: string) => s.trim()).filter(Boolean)
|
||||
: undefined;
|
||||
|
||||
await appstate.vpnStatePart.dispatchAction(appstate.createVpnClientAction, {
|
||||
clientId: data.clientId,
|
||||
description: data.description || undefined,
|
||||
serverDefinedClientTags,
|
||||
forceDestinationSmartproxy: forceSmartproxy,
|
||||
useHostIp: useHostIp || undefined,
|
||||
useDhcp: useDhcp || undefined,
|
||||
staticIp,
|
||||
forceVlan: forceVlan || undefined,
|
||||
vlanId,
|
||||
destinationAllowList,
|
||||
destinationBlockList,
|
||||
});
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
// Setup conditional form visibility after modal renders
|
||||
const createForm = createModal?.shadowRoot?.querySelector('.content')?.querySelector('dees-form') as any;
|
||||
if (createForm) {
|
||||
await createForm.updateComplete;
|
||||
setupFormVisibility(createForm);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -396,6 +480,13 @@ export class OpsViewVpn extends DeesElement {
|
||||
` : ''}
|
||||
<div class="infoItem"><span class="infoLabel">Description</span><span class="infoValue">${client.description || '-'}</span></div>
|
||||
<div class="infoItem"><span class="infoLabel">Tags</span><span class="infoValue">${client.serverDefinedClientTags?.join(', ') || '-'}</span></div>
|
||||
<div class="infoItem"><span class="infoLabel">Routing</span><span class="infoValue">${client.forceDestinationSmartproxy !== false ? 'SmartProxy' : client.useHostIp ? 'Host IP' : 'Direct'}</span></div>
|
||||
${client.useHostIp ? html`
|
||||
<div class="infoItem"><span class="infoLabel">Host IP</span><span class="infoValue">${client.useDhcp ? 'DHCP' : client.staticIp ? `Static: ${client.staticIp}` : 'Not configured'}</span></div>
|
||||
<div class="infoItem"><span class="infoLabel">VLAN</span><span class="infoValue">${client.forceVlan && client.vlanId != null ? `VLAN ${client.vlanId}` : 'No VLAN'}</span></div>
|
||||
` : ''}
|
||||
<div class="infoItem"><span class="infoLabel">Allow List</span><span class="infoValue">${client.destinationAllowList?.length ? client.destinationAllowList.join(', ') : 'None'}</span></div>
|
||||
<div class="infoItem"><span class="infoLabel">Block List</span><span class="infoValue">${client.destinationBlockList?.length ? client.destinationBlockList.join(', ') : 'None'}</span></div>
|
||||
<div class="infoItem"><span class="infoLabel">Created</span><span class="infoValue">${new Date(client.createdAt).toLocaleString()}</span></div>
|
||||
<div class="infoItem"><span class="infoLabel">Updated</span><span class="infoValue">${new Date(client.updatedAt).toLocaleString()}</span></div>
|
||||
</div>
|
||||
@@ -553,12 +644,41 @@ export class OpsViewVpn extends DeesElement {
|
||||
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||
const currentDescription = client.description ?? '';
|
||||
const currentTags = client.serverDefinedClientTags?.join(', ') ?? '';
|
||||
DeesModal.createAndShow({
|
||||
const currentForceSmartproxy = client.forceDestinationSmartproxy ?? true;
|
||||
const currentUseHostIp = client.useHostIp ?? false;
|
||||
const currentUseDhcp = client.useDhcp ?? false;
|
||||
const currentStaticIp = client.staticIp ?? '';
|
||||
const currentForceVlan = client.forceVlan ?? false;
|
||||
const currentVlanId = client.vlanId != null ? String(client.vlanId) : '';
|
||||
const currentAllowList = client.destinationAllowList?.join(', ') ?? '';
|
||||
const currentBlockList = client.destinationBlockList?.join(', ') ?? '';
|
||||
const currentAllowAcls = (client.destinationAllowList?.length ?? 0) > 0
|
||||
|| (client.destinationBlockList?.length ?? 0) > 0;
|
||||
const editModal = await DeesModal.createAndShow({
|
||||
heading: `Edit: ${client.clientId}`,
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-text .key=${'description'} .label=${'Description'} .value=${currentDescription}></dees-input-text>
|
||||
<dees-input-text .key=${'tags'} .label=${'Server-Defined Tags (comma-separated)'} .value=${currentTags}></dees-input-text>
|
||||
<dees-input-checkbox .key=${'forceDestinationSmartproxy'} .label=${'Force traffic through SmartProxy'} .value=${currentForceSmartproxy}></dees-input-checkbox>
|
||||
<div class="hostIpGroup" style="display: ${currentForceSmartproxy ? 'none' : 'flex'}; flex-direction: column; gap: 16px;">
|
||||
<dees-input-checkbox .key=${'useHostIp'} .label=${'Get Host IP'} .value=${currentUseHostIp}></dees-input-checkbox>
|
||||
<div class="hostIpDetails" style="display: ${currentUseHostIp ? 'flex' : 'none'}; flex-direction: column; gap: 16px;">
|
||||
<dees-input-checkbox .key=${'useDhcp'} .label=${'Get IP through DHCP'} .value=${currentUseDhcp}></dees-input-checkbox>
|
||||
<div class="staticIpGroup" style="display: ${currentUseDhcp ? 'none' : 'flex'}; flex-direction: column; gap: 16px;">
|
||||
<dees-input-text .key=${'staticIp'} .label=${'Static IP'} .value=${currentStaticIp}></dees-input-text>
|
||||
</div>
|
||||
<dees-input-checkbox .key=${'forceVlan'} .label=${'Force VLAN'} .value=${currentForceVlan}></dees-input-checkbox>
|
||||
<div class="vlanIdGroup" style="display: ${currentForceVlan ? 'flex' : 'none'}; flex-direction: column; gap: 16px;">
|
||||
<dees-input-text .key=${'vlanId'} .label=${'VLAN ID'} .value=${currentVlanId}></dees-input-text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<dees-input-checkbox .key=${'allowAdditionalAcls'} .label=${'Allow additional ACLs'} .value=${currentAllowAcls}></dees-input-checkbox>
|
||||
<div class="aclGroup" style="display: ${currentAllowAcls ? 'flex' : 'none'}; flex-direction: column; gap: 16px;">
|
||||
<dees-input-text .key=${'destinationAllowList'} .label=${'Destination Allow List (comma-separated IPs/CIDRs)'} .value=${currentAllowList}></dees-input-text>
|
||||
<dees-input-text .key=${'destinationBlockList'} .label=${'Destination Block List (comma-separated IPs/CIDRs)'} .value=${currentBlockList}></dees-input-text>
|
||||
</div>
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
@@ -573,16 +693,47 @@ export class OpsViewVpn extends DeesElement {
|
||||
const serverDefinedClientTags = data.tags
|
||||
? data.tags.split(',').map((t: string) => t.trim()).filter(Boolean)
|
||||
: [];
|
||||
|
||||
// Apply conditional logic based on checkbox states
|
||||
const forceSmartproxy = data.forceDestinationSmartproxy ?? true;
|
||||
const useHostIp = !forceSmartproxy && (data.useHostIp ?? false);
|
||||
const useDhcp = useHostIp && (data.useDhcp ?? false);
|
||||
const staticIp = useHostIp && !useDhcp && data.staticIp ? data.staticIp : undefined;
|
||||
const forceVlan = useHostIp && (data.forceVlan ?? false);
|
||||
const vlanId = forceVlan && data.vlanId ? parseInt(data.vlanId, 10) : undefined;
|
||||
|
||||
const allowAcls = data.allowAdditionalAcls ?? false;
|
||||
const destinationAllowList = allowAcls && data.destinationAllowList
|
||||
? data.destinationAllowList.split(',').map((s: string) => s.trim()).filter(Boolean)
|
||||
: [];
|
||||
const destinationBlockList = allowAcls && data.destinationBlockList
|
||||
? data.destinationBlockList.split(',').map((s: string) => s.trim()).filter(Boolean)
|
||||
: [];
|
||||
|
||||
await appstate.vpnStatePart.dispatchAction(appstate.updateVpnClientAction, {
|
||||
clientId: client.clientId,
|
||||
description: data.description || undefined,
|
||||
serverDefinedClientTags,
|
||||
forceDestinationSmartproxy: forceSmartproxy,
|
||||
useHostIp: useHostIp || undefined,
|
||||
useDhcp: useDhcp || undefined,
|
||||
staticIp,
|
||||
forceVlan: forceVlan || undefined,
|
||||
vlanId,
|
||||
destinationAllowList,
|
||||
destinationBlockList,
|
||||
});
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
// Setup conditional form visibility for edit dialog
|
||||
const editForm = editModal?.shadowRoot?.querySelector('.content')?.querySelector('dees-form') as any;
|
||||
if (editForm) {
|
||||
await editForm.updateComplete;
|
||||
setupFormVisibility(editForm);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user