fix(routing): serialize route updates and correct VPN-gated route application
This commit is contained in:
@@ -1,5 +1,14 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-04-06 - 13.0.11 - fix(routing)
|
||||||
|
serialize route updates and correct VPN-gated route application
|
||||||
|
|
||||||
|
- RouteConfigManager now serializes concurrent applyRoutes calls to prevent overlapping SmartProxy updates and stale route overwrites.
|
||||||
|
- VPN-only routes deny access until VPN state is ready, then re-apply routes after VPN clients load or change to refresh ipAllowLists safely.
|
||||||
|
- Certificate provisioning retries now go through RouteConfigManager when available so the full merged route set is reapplied consistently.
|
||||||
|
- Reference resolution now expands network targets with multiple hosts into multiple route targets.
|
||||||
|
- Adds rollback when VPN client persistence fails, enforces unique target profile names, and fixes maxConnections parsing in the source profiles UI.
|
||||||
|
|
||||||
## 2026-04-06 - 13.0.10 - fix(repo)
|
## 2026-04-06 - 13.0.10 - fix(repo)
|
||||||
no changes to commit
|
no changes to commit
|
||||||
|
|
||||||
|
|||||||
@@ -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.19.1",
|
"@push.rocks/smartvpn": "1.19.2",
|
||||||
"@push.rocks/taskbuffer": "^8.0.2",
|
"@push.rocks/taskbuffer": "^8.0.2",
|
||||||
"@serve.zone/catalog": "^2.11.2",
|
"@serve.zone/catalog": "^2.11.2",
|
||||||
"@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.19.1
|
specifier: 1.19.2
|
||||||
version: 1.19.1
|
version: 1.19.2
|
||||||
'@push.rocks/taskbuffer':
|
'@push.rocks/taskbuffer':
|
||||||
specifier: ^8.0.2
|
specifier: ^8.0.2
|
||||||
version: 8.0.2
|
version: 8.0.2
|
||||||
@@ -1345,8 +1345,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.19.1':
|
'@push.rocks/smartvpn@1.19.2':
|
||||||
resolution: {integrity: sha512-zvC/rrba1tZcXzzzrhX97BEUN6smo1KcqcULu6ZAGpDNhR7c5PU8oWwFxIy33UdDf5NLActkS0L3dq42sGB8nw==}
|
resolution: {integrity: sha512-ygy7jnd4lfXmsHpdL0jS2k6bQAicSSoYcz7OzRpD0jQ970ghAnq2TgC3ccDl23YT9pt0QJPQLkGbVXN5+adQVg==}
|
||||||
|
|
||||||
'@push.rocks/smartwatch@6.4.0':
|
'@push.rocks/smartwatch@6.4.0':
|
||||||
resolution: {integrity: sha512-KDswRgE/siBmZRCsRA07MtW5oF4c9uQEBkwTGPIWneHzksbCDsvs/7agKFEL7WnNifLNwo8w1K1qoiVWkX1fvw==}
|
resolution: {integrity: sha512-KDswRgE/siBmZRCsRA07MtW5oF4c9uQEBkwTGPIWneHzksbCDsvs/7agKFEL7WnNifLNwo8w1K1qoiVWkX1fvw==}
|
||||||
@@ -6723,7 +6723,7 @@ snapshots:
|
|||||||
'@types/semver': 7.7.1
|
'@types/semver': 7.7.1
|
||||||
semver: 7.7.4
|
semver: 7.7.4
|
||||||
|
|
||||||
'@push.rocks/smartvpn@1.19.1':
|
'@push.rocks/smartvpn@1.19.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartnftables': 1.1.0
|
'@push.rocks/smartnftables': 1.1.0
|
||||||
'@push.rocks/smartpath': 6.0.0
|
'@push.rocks/smartpath': 6.0.0
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '13.0.10',
|
version: '13.0.11',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -431,7 +431,15 @@ export class DcRouter {
|
|||||||
// failed silently (SmartProxy doesn't emit certificate-failed for this path).
|
// failed silently (SmartProxy doesn't emit certificate-failed for this path).
|
||||||
// Calling updateRoutes() re-triggers provisionCertificatesViaCallback internally,
|
// Calling updateRoutes() re-triggers provisionCertificatesViaCallback internally,
|
||||||
// which calls certProvisionFunction again — now with smartAcmeReady === true.
|
// which calls certProvisionFunction again — now with smartAcmeReady === true.
|
||||||
if (this.smartProxy) {
|
if (this.routeConfigManager) {
|
||||||
|
// Go through RouteConfigManager to get the full merged route set
|
||||||
|
// and serialize via the route-update mutex (prevents stale overwrites)
|
||||||
|
logger.log('info', 'Re-triggering certificate provisioning via RouteConfigManager');
|
||||||
|
this.routeConfigManager.applyRoutes().catch((err: any) => {
|
||||||
|
logger.log('warn', `Failed to re-trigger cert provisioning: ${err?.message || err}`);
|
||||||
|
});
|
||||||
|
} else if (this.smartProxy) {
|
||||||
|
// No RouteConfigManager (DB disabled) — re-send current routes to trigger cert provisioning
|
||||||
if (this.certProvisionScheduler) {
|
if (this.certProvisionScheduler) {
|
||||||
this.certProvisionScheduler.clear();
|
this.certProvisionScheduler.clear();
|
||||||
}
|
}
|
||||||
@@ -477,7 +485,8 @@ export class DcRouter {
|
|||||||
this.options.vpnConfig?.enabled
|
this.options.vpnConfig?.enabled
|
||||||
? (route: import('../ts_interfaces/data/remoteingress.js').IDcRouterRouteConfig, routeId?: string) => {
|
? (route: import('../ts_interfaces/data/remoteingress.js').IDcRouterRouteConfig, routeId?: string) => {
|
||||||
if (!this.vpnManager || !this.targetProfileManager) {
|
if (!this.vpnManager || !this.targetProfileManager) {
|
||||||
return [this.options.vpnConfig?.subnet || '10.8.0.0/24'];
|
// VPN not ready yet — deny all until re-apply after VPN starts
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
return this.targetProfileManager.getMatchingClientIps(
|
return this.targetProfileManager.getMatchingClientIps(
|
||||||
route, routeId, this.vpnManager.listClients(),
|
route, routeId, this.vpnManager.listClients(),
|
||||||
@@ -2149,7 +2158,10 @@ export class DcRouter {
|
|||||||
bridgeIpRangeEnd: this.options.vpnConfig.bridgeIpRangeEnd,
|
bridgeIpRangeEnd: this.options.vpnConfig.bridgeIpRangeEnd,
|
||||||
onClientChanged: () => {
|
onClientChanged: () => {
|
||||||
// Re-apply routes so profile-based ipAllowLists get updated
|
// Re-apply routes so profile-based ipAllowLists get updated
|
||||||
this.routeConfigManager?.applyRoutes();
|
// (serialized by RouteConfigManager's mutex — safe as fire-and-forget)
|
||||||
|
this.routeConfigManager?.applyRoutes().catch((err) => {
|
||||||
|
logger.log('warn', `Failed to re-apply routes after VPN client change: ${err?.message || err}`);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
getClientDirectTargets: (targetProfileIds: string[]) => {
|
getClientDirectTargets: (targetProfileIds: string[]) => {
|
||||||
if (!this.targetProfileManager) return [];
|
if (!this.targetProfileManager) return [];
|
||||||
@@ -2191,7 +2203,7 @@ export class DcRouter {
|
|||||||
// Re-apply routes now that VPN clients are loaded — ensures hardcoded routes
|
// Re-apply routes now that VPN clients are loaded — ensures hardcoded routes
|
||||||
// get correct profile-based ipAllowLists (not possible during setupSmartProxy since
|
// get correct profile-based ipAllowLists (not possible during setupSmartProxy since
|
||||||
// VPN server wasn't ready yet)
|
// VPN server wasn't ready yet)
|
||||||
this.routeConfigManager?.applyRoutes();
|
await this.routeConfigManager?.applyRoutes();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Cache for DNS-resolved IPs of VPN-gated domains. TTL: 5 minutes. */
|
/** Cache for DNS-resolved IPs of VPN-gated domains. TTL: 5 minutes. */
|
||||||
@@ -2209,6 +2221,11 @@ export class DcRouter {
|
|||||||
const { promises: dnsPromises } = await import('dns');
|
const { promises: dnsPromises } = await import('dns');
|
||||||
const ips = await dnsPromises.resolve4(domain);
|
const ips = await dnsPromises.resolve4(domain);
|
||||||
this.vpnDomainIpCache.set(domain, { ips, expiresAt: Date.now() + 5 * 60 * 1000 });
|
this.vpnDomainIpCache.set(domain, { ips, expiresAt: Date.now() + 5 * 60 * 1000 });
|
||||||
|
// Evict oldest entries if cache exceeds 1000 entries
|
||||||
|
if (this.vpnDomainIpCache.size > 1000) {
|
||||||
|
const firstKey = this.vpnDomainIpCache.keys().next().value;
|
||||||
|
if (firstKey) this.vpnDomainIpCache.delete(firstKey);
|
||||||
|
}
|
||||||
return ips;
|
return ips;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.log('warn', `VPN: Failed to resolve ${domain} for AllowedIPs: ${(err as Error).message}`);
|
logger.log('warn', `VPN: Failed to resolve ${domain} for AllowedIPs: ${(err as Error).message}`);
|
||||||
|
|||||||
@@ -308,14 +308,15 @@ export class ReferenceResolver {
|
|||||||
if (resolvedMetadata.networkTargetRef) {
|
if (resolvedMetadata.networkTargetRef) {
|
||||||
const target = this.targets.get(resolvedMetadata.networkTargetRef);
|
const target = this.targets.get(resolvedMetadata.networkTargetRef);
|
||||||
if (target) {
|
if (target) {
|
||||||
|
const hosts = Array.isArray(target.host) ? target.host : [target.host];
|
||||||
route = {
|
route = {
|
||||||
...route,
|
...route,
|
||||||
action: {
|
action: {
|
||||||
...route.action,
|
...route.action,
|
||||||
targets: [{
|
targets: hosts.map((h) => ({
|
||||||
host: target.host as string,
|
host: h,
|
||||||
port: target.port,
|
port: target.port,
|
||||||
}],
|
})),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
resolvedMetadata.networkTargetName = target.name;
|
resolvedMetadata.networkTargetName = target.name;
|
||||||
|
|||||||
@@ -12,10 +12,41 @@ import type { IDcRouterRouteConfig } from '../../ts_interfaces/data/remoteingres
|
|||||||
import { type IHttp3Config, augmentRouteWithHttp3 } from '../http3/index.js';
|
import { type IHttp3Config, augmentRouteWithHttp3 } from '../http3/index.js';
|
||||||
import type { ReferenceResolver } from './classes.reference-resolver.js';
|
import type { ReferenceResolver } from './classes.reference-resolver.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple async mutex — serializes concurrent applyRoutes() calls so the Rust engine
|
||||||
|
* never receives rapid overlapping route updates that can churn UDP/QUIC listeners.
|
||||||
|
*/
|
||||||
|
class RouteUpdateMutex {
|
||||||
|
private locked = false;
|
||||||
|
private queue: Array<() => void> = [];
|
||||||
|
|
||||||
|
async runExclusive<T>(fn: () => Promise<T>): Promise<T> {
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
if (!this.locked) {
|
||||||
|
this.locked = true;
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
this.queue.push(resolve);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
return await fn();
|
||||||
|
} finally {
|
||||||
|
this.locked = false;
|
||||||
|
const next = this.queue.shift();
|
||||||
|
if (next) {
|
||||||
|
this.locked = true;
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class RouteConfigManager {
|
export class RouteConfigManager {
|
||||||
private storedRoutes = new Map<string, IStoredRoute>();
|
private storedRoutes = new Map<string, IStoredRoute>();
|
||||||
private overrides = new Map<string, IRouteOverride>();
|
private overrides = new Map<string, IRouteOverride>();
|
||||||
private warnings: IRouteWarning[] = [];
|
private warnings: IRouteWarning[] = [];
|
||||||
|
private routeUpdateMutex = new RouteUpdateMutex();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private getHardcodedRoutes: () => plugins.smartproxy.IRouteConfig[],
|
private getHardcodedRoutes: () => plugins.smartproxy.IRouteConfig[],
|
||||||
@@ -357,57 +388,60 @@ export class RouteConfigManager {
|
|||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
public async applyRoutes(): Promise<void> {
|
public async applyRoutes(): Promise<void> {
|
||||||
const smartProxy = this.getSmartProxy();
|
await this.routeUpdateMutex.runExclusive(async () => {
|
||||||
if (!smartProxy) return;
|
const smartProxy = this.getSmartProxy();
|
||||||
|
if (!smartProxy) return;
|
||||||
|
|
||||||
const enabledRoutes: plugins.smartproxy.IRouteConfig[] = [];
|
const enabledRoutes: plugins.smartproxy.IRouteConfig[] = [];
|
||||||
|
|
||||||
const http3Config = this.getHttp3Config?.();
|
const http3Config = this.getHttp3Config?.();
|
||||||
const vpnCallback = this.getVpnClientIpsForRoute;
|
const vpnCallback = this.getVpnClientIpsForRoute;
|
||||||
|
|
||||||
// Helper: inject VPN security into a vpnOnly route
|
// Helper: inject VPN security into a vpnOnly route
|
||||||
const injectVpn = (route: plugins.smartproxy.IRouteConfig, routeId?: string): plugins.smartproxy.IRouteConfig => {
|
const injectVpn = (route: plugins.smartproxy.IRouteConfig, routeId?: string): plugins.smartproxy.IRouteConfig => {
|
||||||
if (!vpnCallback) return route;
|
if (!vpnCallback) return route;
|
||||||
const dcRoute = route as IDcRouterRouteConfig;
|
const dcRoute = route as IDcRouterRouteConfig;
|
||||||
if (!dcRoute.vpnOnly) return route;
|
if (!dcRoute.vpnOnly) return route;
|
||||||
const allowList = vpnCallback(dcRoute, routeId);
|
const vpnIps = vpnCallback(dcRoute, routeId);
|
||||||
return {
|
const existingIps = route.security?.ipAllowList || [];
|
||||||
...route,
|
return {
|
||||||
security: {
|
...route,
|
||||||
...route.security,
|
security: {
|
||||||
ipAllowList: allowList,
|
...route.security,
|
||||||
},
|
ipAllowList: [...existingIps, ...vpnIps],
|
||||||
|
},
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
|
|
||||||
// Add enabled hardcoded routes (respecting overrides, with fresh VPN injection)
|
// Add enabled hardcoded routes (respecting overrides, with fresh VPN injection)
|
||||||
for (const route of this.getHardcodedRoutes()) {
|
for (const route of this.getHardcodedRoutes()) {
|
||||||
const name = route.name || '';
|
const name = route.name || '';
|
||||||
const override = this.overrides.get(name);
|
const override = this.overrides.get(name);
|
||||||
if (override && !override.enabled) {
|
if (override && !override.enabled) {
|
||||||
continue; // Skip disabled hardcoded route
|
continue; // Skip disabled hardcoded route
|
||||||
}
|
|
||||||
enabledRoutes.push(injectVpn(route));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add enabled programmatic routes (with HTTP/3 and VPN augmentation)
|
|
||||||
for (const stored of this.storedRoutes.values()) {
|
|
||||||
if (stored.enabled) {
|
|
||||||
let route = stored.route;
|
|
||||||
if (http3Config?.enabled !== false) {
|
|
||||||
route = augmentRouteWithHttp3(route, { enabled: true, ...http3Config });
|
|
||||||
}
|
}
|
||||||
enabledRoutes.push(injectVpn(route, stored.id));
|
enabledRoutes.push(injectVpn(route));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
await smartProxy.updateRoutes(enabledRoutes);
|
// Add enabled programmatic routes (with HTTP/3 and VPN augmentation)
|
||||||
|
for (const stored of this.storedRoutes.values()) {
|
||||||
|
if (stored.enabled) {
|
||||||
|
let route = stored.route;
|
||||||
|
if (http3Config?.enabled !== false) {
|
||||||
|
route = augmentRouteWithHttp3(route, { enabled: true, ...http3Config });
|
||||||
|
}
|
||||||
|
enabledRoutes.push(injectVpn(route, stored.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Notify listeners (e.g. RemoteIngressManager) of the merged route set
|
await smartProxy.updateRoutes(enabledRoutes);
|
||||||
if (this.onRoutesApplied) {
|
|
||||||
this.onRoutesApplied(enabledRoutes);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.log('info', `Applied ${enabledRoutes.length} routes to SmartProxy (${this.storedRoutes.size} programmatic, ${this.overrides.size} overrides)`);
|
// Notify listeners (e.g. RemoteIngressManager) of the merged route set
|
||||||
|
if (this.onRoutesApplied) {
|
||||||
|
this.onRoutesApplied(enabledRoutes);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log('info', `Applied ${enabledRoutes.length} routes to SmartProxy (${this.storedRoutes.size} programmatic, ${this.overrides.size} overrides)`);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,13 @@ export class TargetProfileManager {
|
|||||||
routeRefs?: string[];
|
routeRefs?: string[];
|
||||||
createdBy: string;
|
createdBy: string;
|
||||||
}): Promise<string> {
|
}): Promise<string> {
|
||||||
|
// Enforce unique profile names
|
||||||
|
for (const existing of this.profiles.values()) {
|
||||||
|
if (existing.name === data.name) {
|
||||||
|
throw new Error(`Target profile with name '${data.name}' already exists (id: ${existing.id})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const id = plugins.uuid.v4();
|
const id = plugins.uuid.v4();
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
|
|||||||
@@ -39,10 +39,6 @@ export class SourceProfileDoc extends plugins.smartdata.SmartDataDbDoc<SourcePro
|
|||||||
return await SourceProfileDoc.getInstance({ id });
|
return await SourceProfileDoc.getInstance({ id });
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async findByName(name: string): Promise<SourceProfileDoc | null> {
|
|
||||||
return await SourceProfileDoc.getInstance({ name });
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async findAll(): Promise<SourceProfileDoc[]> {
|
public static async findAll(): Promise<SourceProfileDoc[]> {
|
||||||
return await SourceProfileDoc.getInstances({});
|
return await SourceProfileDoc.getInstances({});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,10 +42,6 @@ export class TargetProfileDoc extends plugins.smartdata.SmartDataDbDoc<TargetPro
|
|||||||
return await TargetProfileDoc.getInstance({ id });
|
return await TargetProfileDoc.getInstance({ id });
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async findByName(name: string): Promise<TargetProfileDoc | null> {
|
|
||||||
return await TargetProfileDoc.getInstance({ name });
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async findAll(): Promise<TargetProfileDoc[]> {
|
public static async findAll(): Promise<TargetProfileDoc[]> {
|
||||||
return await TargetProfileDoc.getInstances({});
|
return await TargetProfileDoc.getInstances({});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,15 +67,7 @@ export class VpnClientDoc extends plugins.smartdata.SmartDataDbDoc<VpnClientDoc,
|
|||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async findByClientId(clientId: string): Promise<VpnClientDoc | null> {
|
|
||||||
return await VpnClientDoc.getInstance({ clientId });
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async findAll(): Promise<VpnClientDoc[]> {
|
public static async findAll(): Promise<VpnClientDoc[]> {
|
||||||
return await VpnClientDoc.getInstances({});
|
return await VpnClientDoc.getInstances({});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async findEnabled(): Promise<VpnClientDoc[]> {
|
|
||||||
return await VpnClientDoc.getInstances({ enabled: true });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,8 +111,8 @@ export class TargetProfileHandler {
|
|||||||
routeRefs: dataArg.routeRefs,
|
routeRefs: dataArg.routeRefs,
|
||||||
});
|
});
|
||||||
// Re-apply routes and refresh VPN client security to update access
|
// Re-apply routes and refresh VPN client security to update access
|
||||||
this.opsServerRef.dcRouterRef.routeConfigManager?.applyRoutes();
|
await this.opsServerRef.dcRouterRef.routeConfigManager?.applyRoutes();
|
||||||
this.opsServerRef.dcRouterRef.vpnManager?.refreshAllClientSecurity();
|
await this.opsServerRef.dcRouterRef.vpnManager?.refreshAllClientSecurity();
|
||||||
return { success: true };
|
return { success: true };
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -131,8 +131,8 @@ export class TargetProfileHandler {
|
|||||||
const result = await manager.deleteProfile(dataArg.id, dataArg.force);
|
const result = await manager.deleteProfile(dataArg.id, dataArg.force);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
// Re-apply routes and refresh VPN client security to update access
|
// Re-apply routes and refresh VPN client security to update access
|
||||||
this.opsServerRef.dcRouterRef.routeConfigManager?.applyRoutes();
|
await this.opsServerRef.dcRouterRef.routeConfigManager?.applyRoutes();
|
||||||
this.opsServerRef.dcRouterRef.vpnManager?.refreshAllClientSecurity();
|
await this.opsServerRef.dcRouterRef.vpnManager?.refreshAllClientSecurity();
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -267,7 +267,18 @@ export class VpnManager {
|
|||||||
doc.vlanId = opts.vlanId;
|
doc.vlanId = opts.vlanId;
|
||||||
}
|
}
|
||||||
this.clients.set(doc.clientId, doc);
|
this.clients.set(doc.clientId, doc);
|
||||||
await this.persistClient(doc);
|
try {
|
||||||
|
await this.persistClient(doc);
|
||||||
|
} catch (err) {
|
||||||
|
// Rollback: remove from in-memory map and daemon to stay consistent with DB
|
||||||
|
this.clients.delete(doc.clientId);
|
||||||
|
try {
|
||||||
|
await this.vpnServer!.removeClient(doc.clientId);
|
||||||
|
} catch {
|
||||||
|
// best-effort daemon cleanup
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
// Sync per-client security to the running daemon
|
// Sync per-client security to the running daemon
|
||||||
const security = this.buildClientSecurity(doc);
|
const security = this.buildClientSecurity(doc);
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '13.0.10',
|
version: '13.0.11',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,7 +149,8 @@ export class OpsViewSourceProfiles extends DeesElement {
|
|||||||
const data = await form.collectFormData();
|
const data = await form.collectFormData();
|
||||||
const ipAllowList: string[] = Array.isArray(data.ipAllowList) ? data.ipAllowList : [];
|
const ipAllowList: string[] = Array.isArray(data.ipAllowList) ? data.ipAllowList : [];
|
||||||
const ipBlockList: string[] = Array.isArray(data.ipBlockList) ? data.ipBlockList : [];
|
const ipBlockList: string[] = Array.isArray(data.ipBlockList) ? data.ipBlockList : [];
|
||||||
const maxConnections = data.maxConnections ? parseInt(String(data.maxConnections)) : undefined;
|
const parsed = data.maxConnections ? parseInt(String(data.maxConnections), 10) : NaN;
|
||||||
|
const maxConnections = Number.isNaN(parsed) ? undefined : parsed;
|
||||||
|
|
||||||
await appstate.profilesTargetsStatePart.dispatchAction(appstate.createProfileAction, {
|
await appstate.profilesTargetsStatePart.dispatchAction(appstate.createProfileAction, {
|
||||||
name: String(data.name),
|
name: String(data.name),
|
||||||
@@ -190,7 +191,8 @@ export class OpsViewSourceProfiles extends DeesElement {
|
|||||||
const data = await form.collectFormData();
|
const data = await form.collectFormData();
|
||||||
const ipAllowList: string[] = Array.isArray(data.ipAllowList) ? data.ipAllowList : [];
|
const ipAllowList: string[] = Array.isArray(data.ipAllowList) ? data.ipAllowList : [];
|
||||||
const ipBlockList: string[] = Array.isArray(data.ipBlockList) ? data.ipBlockList : [];
|
const ipBlockList: string[] = Array.isArray(data.ipBlockList) ? data.ipBlockList : [];
|
||||||
const maxConnections = data.maxConnections ? parseInt(String(data.maxConnections)) : undefined;
|
const parsed = data.maxConnections ? parseInt(String(data.maxConnections), 10) : NaN;
|
||||||
|
const maxConnections = Number.isNaN(parsed) ? undefined : parsed;
|
||||||
|
|
||||||
await appstate.profilesTargetsStatePart.dispatchAction(appstate.updateProfileAction, {
|
await appstate.profilesTargetsStatePart.dispatchAction(appstate.updateProfileAction, {
|
||||||
id: profile.id,
|
id: profile.id,
|
||||||
|
|||||||
Reference in New Issue
Block a user