feat(vpn): add tag-based VPN route access control and support configured initial VPN clients
This commit is contained in:
@@ -1,5 +1,13 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-03-30 - 11.15.0 - feat(vpn)
|
||||||
|
add tag-based VPN route access control and support configured initial VPN clients
|
||||||
|
|
||||||
|
- allow VPN-protected routes to restrict access to clients with matching server-defined tags instead of always permitting the full VPN subnet
|
||||||
|
- create configured VPN clients automatically on startup and re-apply routes when VPN clients change
|
||||||
|
- rename VPN client tag fields to serverDefinedClientTags across APIs, interfaces, handlers, and UI with legacy tag migration on load
|
||||||
|
- upgrade @push.rocks/smartvpn from 1.12.0 to 1.13.0
|
||||||
|
|
||||||
## 2026-03-30 - 11.14.0 - feat(docs)
|
## 2026-03-30 - 11.14.0 - feat(docs)
|
||||||
document VPN access control and add OpsServer VPN navigation
|
document VPN access control and add OpsServer VPN navigation
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,7 @@
|
|||||||
"@push.rocks/smartrx": "^3.0.10",
|
"@push.rocks/smartrx": "^3.0.10",
|
||||||
"@push.rocks/smartstate": "^2.3.0",
|
"@push.rocks/smartstate": "^2.3.0",
|
||||||
"@push.rocks/smartunique": "^3.0.9",
|
"@push.rocks/smartunique": "^3.0.9",
|
||||||
"@push.rocks/smartvpn": "1.12.0",
|
"@push.rocks/smartvpn": "1.13.0",
|
||||||
"@push.rocks/taskbuffer": "^8.0.2",
|
"@push.rocks/taskbuffer": "^8.0.2",
|
||||||
"@serve.zone/catalog": "^2.9.0",
|
"@serve.zone/catalog": "^2.9.0",
|
||||||
"@serve.zone/interfaces": "^5.3.0",
|
"@serve.zone/interfaces": "^5.3.0",
|
||||||
|
|||||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@@ -96,8 +96,8 @@ importers:
|
|||||||
specifier: ^3.0.9
|
specifier: ^3.0.9
|
||||||
version: 3.0.9
|
version: 3.0.9
|
||||||
'@push.rocks/smartvpn':
|
'@push.rocks/smartvpn':
|
||||||
specifier: 1.12.0
|
specifier: 1.13.0
|
||||||
version: 1.12.0
|
version: 1.13.0
|
||||||
'@push.rocks/taskbuffer':
|
'@push.rocks/taskbuffer':
|
||||||
specifier: ^8.0.2
|
specifier: ^8.0.2
|
||||||
version: 8.0.2
|
version: 8.0.2
|
||||||
@@ -1330,8 +1330,8 @@ packages:
|
|||||||
'@push.rocks/smartversion@3.0.5':
|
'@push.rocks/smartversion@3.0.5':
|
||||||
resolution: {integrity: sha512-8MZSo1yqyaKxKq0Q5N188l4un++9GFWVbhCAX5mXJwewZHn97ujffTeL+eOQYpWFTEpUhaq1QhL4NhqObBCt1Q==}
|
resolution: {integrity: sha512-8MZSo1yqyaKxKq0Q5N188l4un++9GFWVbhCAX5mXJwewZHn97ujffTeL+eOQYpWFTEpUhaq1QhL4NhqObBCt1Q==}
|
||||||
|
|
||||||
'@push.rocks/smartvpn@1.12.0':
|
'@push.rocks/smartvpn@1.13.0':
|
||||||
resolution: {integrity: sha512-lwZCK8fopkms3c6ZSrUghuVNFi7xOXMSkGDSptQM2K3tu2UbajhpdxlAVMODY8n6caQr5ZXp0kHdtwVU9WKi5Q==}
|
resolution: {integrity: sha512-oQY+GIvB9OZQMFEI/f4zwKwaUWPgG8Fsz8AGhPDedvH32jYNYEb9B957yRAROf7ndyQM/LThm7mN/5cx8ALyLw==}
|
||||||
|
|
||||||
'@push.rocks/smartwatch@6.4.0':
|
'@push.rocks/smartwatch@6.4.0':
|
||||||
resolution: {integrity: sha512-KDswRgE/siBmZRCsRA07MtW5oF4c9uQEBkwTGPIWneHzksbCDsvs/7agKFEL7WnNifLNwo8w1K1qoiVWkX1fvw==}
|
resolution: {integrity: sha512-KDswRgE/siBmZRCsRA07MtW5oF4c9uQEBkwTGPIWneHzksbCDsvs/7agKFEL7WnNifLNwo8w1K1qoiVWkX1fvw==}
|
||||||
@@ -6562,7 +6562,7 @@ snapshots:
|
|||||||
'@types/semver': 7.7.1
|
'@types/semver': 7.7.1
|
||||||
semver: 7.7.4
|
semver: 7.7.4
|
||||||
|
|
||||||
'@push.rocks/smartvpn@1.12.0':
|
'@push.rocks/smartvpn@1.13.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartpath': 6.0.0
|
'@push.rocks/smartpath': 6.0.0
|
||||||
'@push.rocks/smartrust': 1.3.2
|
'@push.rocks/smartrust': 1.3.2
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '11.14.0',
|
version: '11.15.0',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -208,6 +208,12 @@ export interface IDcRouterOptions {
|
|||||||
serverEndpoint?: string;
|
serverEndpoint?: string;
|
||||||
/** Override forwarding mode. Default: auto-detect (tun if root, socket otherwise) */
|
/** Override forwarding mode. Default: auto-detect (tun if root, socket otherwise) */
|
||||||
forwardingMode?: 'tun' | 'socket';
|
forwardingMode?: 'tun' | 'socket';
|
||||||
|
/** Pre-defined VPN clients created on startup */
|
||||||
|
clients?: Array<{
|
||||||
|
clientId: string;
|
||||||
|
serverDefinedClientTags?: string[];
|
||||||
|
description?: string;
|
||||||
|
}>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -453,7 +459,14 @@ export class DcRouter {
|
|||||||
() => this.getConstructorRoutes(),
|
() => this.getConstructorRoutes(),
|
||||||
() => this.smartProxy,
|
() => this.smartProxy,
|
||||||
() => this.options.http3,
|
() => this.options.http3,
|
||||||
() => this.options.vpnConfig?.enabled ? (this.options.vpnConfig.subnet || '10.8.0.0/24') : undefined,
|
this.options.vpnConfig?.enabled
|
||||||
|
? (tags?: string[]) => {
|
||||||
|
if (tags?.length && this.vpnManager) {
|
||||||
|
return this.vpnManager.getClientIpsForServerDefinedTags(tags);
|
||||||
|
}
|
||||||
|
return [this.options.vpnConfig?.subnet || '10.8.0.0/24'];
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
);
|
);
|
||||||
this.apiTokenManager = new ApiTokenManager(this.storageManager);
|
this.apiTokenManager = new ApiTokenManager(this.storageManager);
|
||||||
await this.apiTokenManager.initialize();
|
await this.apiTokenManager.initialize();
|
||||||
@@ -2086,6 +2099,11 @@ export class DcRouter {
|
|||||||
dns: this.options.vpnConfig.dns,
|
dns: this.options.vpnConfig.dns,
|
||||||
serverEndpoint: this.options.vpnConfig.serverEndpoint,
|
serverEndpoint: this.options.vpnConfig.serverEndpoint,
|
||||||
forwardingMode: this.options.vpnConfig.forwardingMode,
|
forwardingMode: this.options.vpnConfig.forwardingMode,
|
||||||
|
initialClients: this.options.vpnConfig.clients,
|
||||||
|
onClientChanged: () => {
|
||||||
|
// Re-apply routes so tag-based ipAllowLists get updated
|
||||||
|
this.routeConfigManager?.applyRoutes();
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.vpnManager.start();
|
await this.vpnManager.start();
|
||||||
@@ -2104,11 +2122,23 @@ export class DcRouter {
|
|||||||
if (dcrouterRoute.vpn?.required) {
|
if (dcrouterRoute.vpn?.required) {
|
||||||
injectedCount++;
|
injectedCount++;
|
||||||
const existing = route.security?.ipAllowList || [];
|
const existing = route.security?.ipAllowList || [];
|
||||||
|
|
||||||
|
let vpnAllowList: string[];
|
||||||
|
if (dcrouterRoute.vpn.allowedServerDefinedClientTags?.length && this.vpnManager) {
|
||||||
|
// Tag-based: only specific client IPs
|
||||||
|
vpnAllowList = this.vpnManager.getClientIpsForServerDefinedTags(
|
||||||
|
dcrouterRoute.vpn.allowedServerDefinedClientTags,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// No tags specified: entire VPN subnet
|
||||||
|
vpnAllowList = [vpnSubnet];
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...route,
|
...route,
|
||||||
security: {
|
security: {
|
||||||
...route.security,
|
...route.security,
|
||||||
ipAllowList: [...existing, vpnSubnet],
|
ipAllowList: [...existing, ...vpnAllowList],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -2116,7 +2146,7 @@ export class DcRouter {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (injectedCount > 0) {
|
if (injectedCount > 0) {
|
||||||
logger.log('info', `VPN: Injected ipAllowList (${vpnSubnet}) into ${injectedCount} VPN-protected route(s)`);
|
logger.log('info', `VPN: Injected ipAllowList into ${injectedCount} VPN-protected route(s)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export class RouteConfigManager {
|
|||||||
private getHardcodedRoutes: () => plugins.smartproxy.IRouteConfig[],
|
private getHardcodedRoutes: () => plugins.smartproxy.IRouteConfig[],
|
||||||
private getSmartProxy: () => plugins.smartproxy.SmartProxy | undefined,
|
private getSmartProxy: () => plugins.smartproxy.SmartProxy | undefined,
|
||||||
private getHttp3Config?: () => IHttp3Config | undefined,
|
private getHttp3Config?: () => IHttp3Config | undefined,
|
||||||
private getVpnSubnet?: () => string | undefined,
|
private getVpnAllowList?: (tags?: string[]) => string[],
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -246,7 +246,7 @@ export class RouteConfigManager {
|
|||||||
// Private: apply merged routes to SmartProxy
|
// Private: apply merged routes to SmartProxy
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
private async applyRoutes(): Promise<void> {
|
public async applyRoutes(): Promise<void> {
|
||||||
const smartProxy = this.getSmartProxy();
|
const smartProxy = this.getSmartProxy();
|
||||||
if (!smartProxy) return;
|
if (!smartProxy) return;
|
||||||
|
|
||||||
@@ -262,9 +262,9 @@ export class RouteConfigManager {
|
|||||||
enabledRoutes.push(route);
|
enabledRoutes.push(route);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add enabled programmatic routes (with HTTP/3 augmentation if enabled)
|
// Add enabled programmatic routes (with HTTP/3 and VPN augmentation)
|
||||||
const http3Config = this.getHttp3Config?.();
|
const http3Config = this.getHttp3Config?.();
|
||||||
const vpnSubnet = this.getVpnSubnet?.();
|
const vpnAllowList = this.getVpnAllowList;
|
||||||
for (const stored of this.storedRoutes.values()) {
|
for (const stored of this.storedRoutes.values()) {
|
||||||
if (stored.enabled) {
|
if (stored.enabled) {
|
||||||
let route = stored.route;
|
let route = stored.route;
|
||||||
@@ -272,15 +272,16 @@ export class RouteConfigManager {
|
|||||||
route = augmentRouteWithHttp3(route, { enabled: true, ...http3Config });
|
route = augmentRouteWithHttp3(route, { enabled: true, ...http3Config });
|
||||||
}
|
}
|
||||||
// Inject VPN security for programmatic routes with vpn.required
|
// Inject VPN security for programmatic routes with vpn.required
|
||||||
if (vpnSubnet) {
|
if (vpnAllowList) {
|
||||||
const dcRoute = route as IDcRouterRouteConfig;
|
const dcRoute = route as IDcRouterRouteConfig;
|
||||||
if (dcRoute.vpn?.required) {
|
if (dcRoute.vpn?.required) {
|
||||||
const existing = route.security?.ipAllowList || [];
|
const existing = route.security?.ipAllowList || [];
|
||||||
|
const allowList = vpnAllowList(dcRoute.vpn.allowedServerDefinedClientTags);
|
||||||
route = {
|
route = {
|
||||||
...route,
|
...route,
|
||||||
security: {
|
security: {
|
||||||
...route.security,
|
...route.security,
|
||||||
ipAllowList: [...existing, vpnSubnet],
|
ipAllowList: [...existing, ...allowList],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export class VpnHandler {
|
|||||||
const clients = manager.listClients().map((c) => ({
|
const clients = manager.listClients().map((c) => ({
|
||||||
clientId: c.clientId,
|
clientId: c.clientId,
|
||||||
enabled: c.enabled,
|
enabled: c.enabled,
|
||||||
tags: c.tags,
|
serverDefinedClientTags: c.serverDefinedClientTags,
|
||||||
description: c.description,
|
description: c.description,
|
||||||
assignedIp: c.assignedIp,
|
assignedIp: c.assignedIp,
|
||||||
createdAt: c.createdAt,
|
createdAt: c.createdAt,
|
||||||
@@ -89,7 +89,7 @@ export class VpnHandler {
|
|||||||
try {
|
try {
|
||||||
const bundle = await manager.createClient({
|
const bundle = await manager.createClient({
|
||||||
clientId: dataArg.clientId,
|
clientId: dataArg.clientId,
|
||||||
tags: dataArg.tags,
|
serverDefinedClientTags: dataArg.serverDefinedClientTags,
|
||||||
description: dataArg.description,
|
description: dataArg.description,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ export class VpnHandler {
|
|||||||
client: {
|
client: {
|
||||||
clientId: bundle.entry.clientId,
|
clientId: bundle.entry.clientId,
|
||||||
enabled: bundle.entry.enabled ?? true,
|
enabled: bundle.entry.enabled ?? true,
|
||||||
tags: bundle.entry.tags,
|
serverDefinedClientTags: bundle.entry.serverDefinedClientTags,
|
||||||
description: bundle.entry.description,
|
description: bundle.entry.description,
|
||||||
assignedIp: bundle.entry.assignedIp,
|
assignedIp: bundle.entry.assignedIp,
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
|
|||||||
@@ -16,6 +16,14 @@ export interface IVpnManagerConfig {
|
|||||||
serverEndpoint?: string;
|
serverEndpoint?: string;
|
||||||
/** Override forwarding mode. Default: auto-detect (tun if root, socket otherwise) */
|
/** Override forwarding mode. Default: auto-detect (tun if root, socket otherwise) */
|
||||||
forwardingMode?: 'tun' | 'socket';
|
forwardingMode?: 'tun' | 'socket';
|
||||||
|
/** Pre-defined VPN clients created on startup (idempotent — skips already-persisted clients) */
|
||||||
|
initialClients?: Array<{
|
||||||
|
clientId: string;
|
||||||
|
serverDefinedClientTags?: string[];
|
||||||
|
description?: string;
|
||||||
|
}>;
|
||||||
|
/** Called when clients are created/deleted/toggled — triggers route re-application */
|
||||||
|
onClientChanged?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IPersistedServerKeys {
|
interface IPersistedServerKeys {
|
||||||
@@ -28,7 +36,7 @@ interface IPersistedServerKeys {
|
|||||||
interface IPersistedClient {
|
interface IPersistedClient {
|
||||||
clientId: string;
|
clientId: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
tags?: string[];
|
serverDefinedClientTags?: string[];
|
||||||
description?: string;
|
description?: string;
|
||||||
assignedIp?: string;
|
assignedIp?: string;
|
||||||
noisePublicKey: string;
|
noisePublicKey: string;
|
||||||
@@ -36,6 +44,8 @@ interface IPersistedClient {
|
|||||||
createdAt: number;
|
createdAt: number;
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
expiresAt?: string;
|
expiresAt?: string;
|
||||||
|
/** @deprecated Legacy field — migrated to serverDefinedClientTags on load */
|
||||||
|
tags?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -92,7 +102,7 @@ export class VpnManager {
|
|||||||
publicKey: client.noisePublicKey,
|
publicKey: client.noisePublicKey,
|
||||||
wgPublicKey: client.wgPublicKey,
|
wgPublicKey: client.wgPublicKey,
|
||||||
enabled: client.enabled,
|
enabled: client.enabled,
|
||||||
tags: client.tags,
|
serverDefinedClientTags: client.serverDefinedClientTags,
|
||||||
description: client.description,
|
description: client.description,
|
||||||
assignedIp: client.assignedIp,
|
assignedIp: client.assignedIp,
|
||||||
expiresAt: client.expiresAt,
|
expiresAt: client.expiresAt,
|
||||||
@@ -122,6 +132,21 @@ export class VpnManager {
|
|||||||
};
|
};
|
||||||
|
|
||||||
await this.vpnServer.start(serverConfig);
|
await this.vpnServer.start(serverConfig);
|
||||||
|
|
||||||
|
// Create initial clients from config (idempotent — skip already-persisted)
|
||||||
|
if (this.config.initialClients) {
|
||||||
|
for (const initial of this.config.initialClients) {
|
||||||
|
if (!this.clients.has(initial.clientId)) {
|
||||||
|
const bundle = await this.createClient({
|
||||||
|
clientId: initial.clientId,
|
||||||
|
serverDefinedClientTags: initial.serverDefinedClientTags,
|
||||||
|
description: initial.description,
|
||||||
|
});
|
||||||
|
logger.log('info', `VPN: Created initial client '${initial.clientId}' (IP: ${bundle.entry.assignedIp})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
logger.log('info', `VPN server started: mode=${this._forwardingMode}, subnet=${subnet}, wg=:${wgListenPort}, clients=${this.clients.size}`);
|
logger.log('info', `VPN server started: mode=${this._forwardingMode}, subnet=${subnet}, wg=:${wgListenPort}, clients=${this.clients.size}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,7 +173,7 @@ export class VpnManager {
|
|||||||
*/
|
*/
|
||||||
public async createClient(opts: {
|
public async createClient(opts: {
|
||||||
clientId: string;
|
clientId: string;
|
||||||
tags?: string[];
|
serverDefinedClientTags?: string[];
|
||||||
description?: string;
|
description?: string;
|
||||||
}): Promise<plugins.smartvpn.IClientConfigBundle> {
|
}): Promise<plugins.smartvpn.IClientConfigBundle> {
|
||||||
if (!this.vpnServer) {
|
if (!this.vpnServer) {
|
||||||
@@ -157,7 +182,7 @@ export class VpnManager {
|
|||||||
|
|
||||||
const bundle = await this.vpnServer.createClient({
|
const bundle = await this.vpnServer.createClient({
|
||||||
clientId: opts.clientId,
|
clientId: opts.clientId,
|
||||||
tags: opts.tags,
|
serverDefinedClientTags: opts.serverDefinedClientTags,
|
||||||
description: opts.description,
|
description: opts.description,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -174,7 +199,7 @@ export class VpnManager {
|
|||||||
const persisted: IPersistedClient = {
|
const persisted: IPersistedClient = {
|
||||||
clientId: bundle.entry.clientId,
|
clientId: bundle.entry.clientId,
|
||||||
enabled: bundle.entry.enabled ?? true,
|
enabled: bundle.entry.enabled ?? true,
|
||||||
tags: bundle.entry.tags,
|
serverDefinedClientTags: bundle.entry.serverDefinedClientTags,
|
||||||
description: bundle.entry.description,
|
description: bundle.entry.description,
|
||||||
assignedIp: bundle.entry.assignedIp,
|
assignedIp: bundle.entry.assignedIp,
|
||||||
noisePublicKey: bundle.entry.publicKey,
|
noisePublicKey: bundle.entry.publicKey,
|
||||||
@@ -186,6 +211,7 @@ export class VpnManager {
|
|||||||
this.clients.set(persisted.clientId, persisted);
|
this.clients.set(persisted.clientId, persisted);
|
||||||
await this.persistClient(persisted);
|
await this.persistClient(persisted);
|
||||||
|
|
||||||
|
this.config.onClientChanged?.();
|
||||||
return bundle;
|
return bundle;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,6 +225,7 @@ export class VpnManager {
|
|||||||
await this.vpnServer.removeClient(clientId);
|
await this.vpnServer.removeClient(clientId);
|
||||||
this.clients.delete(clientId);
|
this.clients.delete(clientId);
|
||||||
await this.storageManager.delete(`${STORAGE_PREFIX_CLIENTS}${clientId}`);
|
await this.storageManager.delete(`${STORAGE_PREFIX_CLIENTS}${clientId}`);
|
||||||
|
this.config.onClientChanged?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -220,6 +247,7 @@ export class VpnManager {
|
|||||||
client.updatedAt = Date.now();
|
client.updatedAt = Date.now();
|
||||||
await this.persistClient(client);
|
await this.persistClient(client);
|
||||||
}
|
}
|
||||||
|
this.config.onClientChanged?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -234,6 +262,7 @@ export class VpnManager {
|
|||||||
client.updatedAt = Date.now();
|
client.updatedAt = Date.now();
|
||||||
await this.persistClient(client);
|
await this.persistClient(client);
|
||||||
}
|
}
|
||||||
|
this.config.onClientChanged?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -283,6 +312,22 @@ export class VpnManager {
|
|||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Tag-based access control ───────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get assigned IPs for all enabled clients matching any of the given server-defined tags.
|
||||||
|
*/
|
||||||
|
public getClientIpsForServerDefinedTags(tags: string[]): string[] {
|
||||||
|
const ips: string[] = [];
|
||||||
|
for (const client of this.clients.values()) {
|
||||||
|
if (!client.enabled || !client.assignedIp) continue;
|
||||||
|
if (client.serverDefinedClientTags?.some(t => tags.includes(t))) {
|
||||||
|
ips.push(client.assignedIp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ips;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Status and telemetry ───────────────────────────────────────────────
|
// ── Status and telemetry ───────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -364,6 +409,12 @@ export class VpnManager {
|
|||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
const client = await this.storageManager.getJSON<IPersistedClient>(key);
|
const client = await this.storageManager.getJSON<IPersistedClient>(key);
|
||||||
if (client) {
|
if (client) {
|
||||||
|
// Migrate legacy `tags` → `serverDefinedClientTags`
|
||||||
|
if (!client.serverDefinedClientTags && client.tags) {
|
||||||
|
client.serverDefinedClientTags = client.tags;
|
||||||
|
delete client.tags;
|
||||||
|
await this.persistClient(client);
|
||||||
|
}
|
||||||
this.clients.set(client.clientId, client);
|
this.clients.set(client.clientId, client);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ export interface IRouteRemoteIngress {
|
|||||||
export interface IRouteVpn {
|
export interface IRouteVpn {
|
||||||
/** Whether this route requires VPN access */
|
/** Whether this route requires VPN access */
|
||||||
required: boolean;
|
required: boolean;
|
||||||
|
/** Only allow VPN clients with these server-defined tags. Omitted = all VPN clients. */
|
||||||
|
allowedServerDefinedClientTags?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
export interface IVpnClient {
|
export interface IVpnClient {
|
||||||
clientId: string;
|
clientId: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
tags?: string[];
|
serverDefinedClientTags?: string[];
|
||||||
description?: string;
|
description?: string;
|
||||||
assignedIp?: string;
|
assignedIp?: string;
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export interface IReq_CreateVpnClient extends plugins.typedrequestInterfaces.imp
|
|||||||
request: {
|
request: {
|
||||||
identity: authInterfaces.IIdentity;
|
identity: authInterfaces.IIdentity;
|
||||||
clientId: string;
|
clientId: string;
|
||||||
tags?: string[];
|
serverDefinedClientTags?: string[];
|
||||||
description?: string;
|
description?: string;
|
||||||
};
|
};
|
||||||
response: {
|
response: {
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '11.14.0',
|
version: '11.15.0',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -974,7 +974,7 @@ export const fetchVpnAction = vpnStatePart.createAction(async (statePartArg): Pr
|
|||||||
|
|
||||||
export const createVpnClientAction = vpnStatePart.createAction<{
|
export const createVpnClientAction = vpnStatePart.createAction<{
|
||||||
clientId: string;
|
clientId: string;
|
||||||
tags?: string[];
|
serverDefinedClientTags?: string[];
|
||||||
description?: string;
|
description?: string;
|
||||||
}>(async (statePartArg, dataArg, actionContext): Promise<IVpnState> => {
|
}>(async (statePartArg, dataArg, actionContext): Promise<IVpnState> => {
|
||||||
const context = getActionContext();
|
const context = getActionContext();
|
||||||
@@ -988,7 +988,7 @@ export const createVpnClientAction = vpnStatePart.createAction<{
|
|||||||
const response = await request.fire({
|
const response = await request.fire({
|
||||||
identity: context.identity!,
|
identity: context.identity!,
|
||||||
clientId: dataArg.clientId,
|
clientId: dataArg.clientId,
|
||||||
tags: dataArg.tags,
|
serverDefinedClientTags: dataArg.serverDefinedClientTags,
|
||||||
description: dataArg.description,
|
description: dataArg.description,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -255,8 +255,8 @@ export class OpsViewVpn extends DeesElement {
|
|||||||
? html`<span class="statusBadge enabled">enabled</span>`
|
? html`<span class="statusBadge enabled">enabled</span>`
|
||||||
: html`<span class="statusBadge disabled">disabled</span>`,
|
: html`<span class="statusBadge disabled">disabled</span>`,
|
||||||
'VPN IP': client.assignedIp || '-',
|
'VPN IP': client.assignedIp || '-',
|
||||||
'Tags': client.tags?.length
|
'Tags': client.serverDefinedClientTags?.length
|
||||||
? html`${client.tags.map(t => html`<span class="tagBadge">${t}</span>`)}`
|
? html`${client.serverDefinedClientTags.map(t => html`<span class="tagBadge">${t}</span>`)}`
|
||||||
: '-',
|
: '-',
|
||||||
'Description': client.description || '-',
|
'Description': client.description || '-',
|
||||||
'Created': new Date(client.createdAt).toLocaleDateString(),
|
'Created': new Date(client.createdAt).toLocaleDateString(),
|
||||||
@@ -312,11 +312,11 @@ export class OpsViewVpn extends DeesElement {
|
|||||||
action: async (modal: any) => {
|
action: async (modal: any) => {
|
||||||
const form = modal.shadowRoot!.querySelector('dees-form') as any;
|
const form = modal.shadowRoot!.querySelector('dees-form') as any;
|
||||||
const data = await form.collectFormData();
|
const data = await form.collectFormData();
|
||||||
const tags = data.tags ? data.tags.split(',').map((t: string) => t.trim()).filter(Boolean) : undefined;
|
const serverDefinedClientTags = data.tags ? data.tags.split(',').map((t: string) => t.trim()).filter(Boolean) : undefined;
|
||||||
await appstate.vpnStatePart.dispatchAction(appstate.createVpnClientAction, {
|
await appstate.vpnStatePart.dispatchAction(appstate.createVpnClientAction, {
|
||||||
clientId: data.clientId,
|
clientId: data.clientId,
|
||||||
description: data.description || undefined,
|
description: data.description || undefined,
|
||||||
tags,
|
serverDefinedClientTags,
|
||||||
});
|
});
|
||||||
modal.destroy();
|
modal.destroy();
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user