Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cfb727b86d | |||
| 1e4b9997f4 | |||
| bb32f23d77 | |||
| 1aa6451dba | |||
| eb0408c036 | |||
| 098a2567fa | |||
| c6534df362 | |||
| 2e4b375ad5 | |||
| 802bcf1c3d | |||
| bad0bd9053 | |||
| ca990781b0 | |||
| 6807aefce8 | |||
| 450ec4816e | |||
| ab4310b775 |
42
changelog.md
42
changelog.md
@@ -1,5 +1,47 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-03-31 - 11.21.5 - fix(routing)
|
||||||
|
apply VPN route allowlists dynamically after VPN clients load
|
||||||
|
|
||||||
|
- Moves VPN security injection for hardcoded and programmatic routes into RouteConfigManager.applyRoutes() so allowlists are generated from current VPN client state.
|
||||||
|
- Re-applies routes after starting the VPN manager to ensure tag-based ipAllowLists are available once VPN clients are loaded.
|
||||||
|
- Avoids caching constructor routes with stale VPN security baked in while preserving HTTP/3 route augmentation.
|
||||||
|
|
||||||
|
## 2026-03-31 - 11.21.4 - fix(deps)
|
||||||
|
bump @push.rocks/smartvpn to 1.16.4
|
||||||
|
|
||||||
|
- Updates the @push.rocks/smartvpn dependency from 1.16.3 to 1.16.4 in package.json.
|
||||||
|
|
||||||
|
## 2026-03-31 - 11.21.3 - fix(deps)
|
||||||
|
bump @push.rocks/smartvpn to 1.16.3
|
||||||
|
|
||||||
|
- Updates the @push.rocks/smartvpn dependency from 1.16.2 to 1.16.3.
|
||||||
|
|
||||||
|
## 2026-03-31 - 11.21.2 - fix(deps)
|
||||||
|
bump @push.rocks/smartvpn to 1.16.2
|
||||||
|
|
||||||
|
- Updates the @push.rocks/smartvpn dependency from 1.16.1 to 1.16.2 in package.json.
|
||||||
|
|
||||||
|
## 2026-03-31 - 11.21.1 - fix(vpn)
|
||||||
|
resolve VPN-gated route domains into per-client AllowedIPs with cached DNS lookups
|
||||||
|
|
||||||
|
- Derive WireGuard AllowedIPs from DNS A records of matched vpn.required route domains instead of only configured public proxy IPs.
|
||||||
|
- Cache resolved domain IPs for 5 minutes and fall back to stale results on DNS lookup failures.
|
||||||
|
- Make per-client AllowedIPs generation asynchronous throughout VPN config export and regeneration flows.
|
||||||
|
|
||||||
|
## 2026-03-31 - 11.21.0 - feat(vpn)
|
||||||
|
add tag-aware WireGuard AllowedIPs for VPN-gated routes
|
||||||
|
|
||||||
|
- compute per-client WireGuard AllowedIPs from server-defined client tags and VPN-required proxy routes
|
||||||
|
- include the server public IP in AllowedIPs when a client can access VPN-gated domains so routed traffic reaches the proxy
|
||||||
|
- preserve and inject WireGuard private keys in generated and exported client configs for valid exports
|
||||||
|
|
||||||
|
## 2026-03-31 - 11.20.1 - fix(vpn-manager)
|
||||||
|
persist WireGuard private keys for valid client exports and QR codes
|
||||||
|
|
||||||
|
- Store each client's WireGuard private key when creating and rotating keys.
|
||||||
|
- Inject the stored private key into exported WireGuard configs so generated configs are complete and scannable.
|
||||||
|
|
||||||
## 2026-03-30 - 11.20.0 - feat(vpn-ui)
|
## 2026-03-30 - 11.20.0 - feat(vpn-ui)
|
||||||
add QR code export for WireGuard client configurations
|
add QR code export for WireGuard client configurations
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@serve.zone/dcrouter",
|
"name": "@serve.zone/dcrouter",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "11.20.0",
|
"version": "11.21.5",
|
||||||
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
|
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
@@ -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.16.1",
|
"@push.rocks/smartvpn": "1.16.4",
|
||||||
"@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.16.1
|
specifier: 1.16.4
|
||||||
version: 1.16.1
|
version: 1.16.4
|
||||||
'@push.rocks/taskbuffer':
|
'@push.rocks/taskbuffer':
|
||||||
specifier: ^8.0.2
|
specifier: ^8.0.2
|
||||||
version: 8.0.2
|
version: 8.0.2
|
||||||
@@ -1339,8 +1339,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.16.1':
|
'@push.rocks/smartvpn@1.16.4':
|
||||||
resolution: {integrity: sha512-LQzt3ajMKIs3anYki/3drt7XcCuekoKvApCltLEjsoGEEX5JkXGSZFB+UFvqEhG8NcEuHw574rU3tB2orHzKTQ==}
|
resolution: {integrity: sha512-ps7NcdBzaaGQFjHcXUN8JC623xZbLNyIYfICxDLJb2BxzzuZa667fW0KxQQCwLtZaB2txN5sMlaOKFi27tXTBA==}
|
||||||
|
|
||||||
'@push.rocks/smartwatch@6.4.0':
|
'@push.rocks/smartwatch@6.4.0':
|
||||||
resolution: {integrity: sha512-KDswRgE/siBmZRCsRA07MtW5oF4c9uQEBkwTGPIWneHzksbCDsvs/7agKFEL7WnNifLNwo8w1K1qoiVWkX1fvw==}
|
resolution: {integrity: sha512-KDswRgE/siBmZRCsRA07MtW5oF4c9uQEBkwTGPIWneHzksbCDsvs/7agKFEL7WnNifLNwo8w1K1qoiVWkX1fvw==}
|
||||||
@@ -6622,7 +6622,7 @@ snapshots:
|
|||||||
'@types/semver': 7.7.1
|
'@types/semver': 7.7.1
|
||||||
semver: 7.7.4
|
semver: 7.7.4
|
||||||
|
|
||||||
'@push.rocks/smartvpn@1.16.1':
|
'@push.rocks/smartvpn@1.16.4':
|
||||||
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
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { DcRouter } from '../ts/index.js';
|
import { DcRouter } from '../ts/index.js';
|
||||||
|
|
||||||
const devRouter = new DcRouter({
|
const devRouter = new DcRouter({
|
||||||
|
// Server public IP (used for VPN AllowedIPs)
|
||||||
|
publicIp: '203.0.113.1',
|
||||||
// SmartProxy routes for development/demo
|
// SmartProxy routes for development/demo
|
||||||
smartProxyConfig: {
|
smartProxyConfig: {
|
||||||
routes: [
|
routes: [
|
||||||
@@ -23,7 +25,19 @@ const devRouter = new DcRouter({
|
|||||||
tls: { mode: 'passthrough' },
|
tls: { mode: 'passthrough' },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
{
|
||||||
|
name: 'vpn-internal-app',
|
||||||
|
match: { ports: [18080], domains: ['internal.example.com'] },
|
||||||
|
action: { type: 'forward', targets: [{ host: 'localhost', port: 5000 }] },
|
||||||
|
vpn: { required: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'vpn-eng-dashboard',
|
||||||
|
match: { ports: [18080], domains: ['eng.example.com'] },
|
||||||
|
action: { type: 'forward', targets: [{ host: 'localhost', port: 5001 }] },
|
||||||
|
vpn: { required: true, allowedServerDefinedClientTags: ['engineering'] },
|
||||||
|
},
|
||||||
|
] as any[],
|
||||||
},
|
},
|
||||||
// VPN with pre-defined clients
|
// VPN with pre-defined clients
|
||||||
vpnConfig: {
|
vpnConfig: {
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '11.20.0',
|
version: '11.21.5',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -813,12 +813,8 @@ export class DcRouter {
|
|||||||
logger.log('info', 'HTTP/3: Augmented qualifying HTTPS routes with QUIC/H3 configuration');
|
logger.log('info', 'HTTP/3: Augmented qualifying HTTPS routes with QUIC/H3 configuration');
|
||||||
}
|
}
|
||||||
|
|
||||||
// VPN route security injection: restrict vpn.required routes to VPN subnet
|
// Cache constructor routes for RouteConfigManager (without VPN security baked in —
|
||||||
if (this.options.vpnConfig?.enabled) {
|
// applyRoutes() injects VPN security dynamically so it stays current with client changes)
|
||||||
routes = this.injectVpnSecurity(routes);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cache constructor routes for RouteConfigManager
|
|
||||||
this.constructorRoutes = [...routes];
|
this.constructorRoutes = [...routes];
|
||||||
|
|
||||||
// If we have routes or need a basic SmartProxy instance, create it
|
// If we have routes or need a basic SmartProxy instance, create it
|
||||||
@@ -2105,54 +2101,75 @@ export class DcRouter {
|
|||||||
// Re-apply routes so tag-based ipAllowLists get updated
|
// Re-apply routes so tag-based ipAllowLists get updated
|
||||||
this.routeConfigManager?.applyRoutes();
|
this.routeConfigManager?.applyRoutes();
|
||||||
},
|
},
|
||||||
|
getClientAllowedIPs: async (clientTags: string[]) => {
|
||||||
|
const subnet = this.options.vpnConfig?.subnet || '10.8.0.0/24';
|
||||||
|
const ips = new Set<string>([subnet]);
|
||||||
|
|
||||||
|
// Check routes for VPN-gated tag match and collect domains
|
||||||
|
const routes = this.options.smartProxyConfig?.routes || [];
|
||||||
|
const domainsToResolve = new Set<string>();
|
||||||
|
for (const route of routes) {
|
||||||
|
const dcRoute = route as import('../ts_interfaces/data/remoteingress.js').IDcRouterRouteConfig;
|
||||||
|
if (!dcRoute.vpn?.required) continue;
|
||||||
|
|
||||||
|
const routeTags = dcRoute.vpn.allowedServerDefinedClientTags;
|
||||||
|
if (!routeTags?.length || clientTags.some(t => routeTags.includes(t))) {
|
||||||
|
// Collect domains from this route
|
||||||
|
const domains = (route.match as any)?.domains;
|
||||||
|
if (Array.isArray(domains)) {
|
||||||
|
for (const d of domains) {
|
||||||
|
// Strip wildcard prefix for DNS resolution (*.example.com → example.com)
|
||||||
|
domainsToResolve.add(d.replace(/^\*\./, ''));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve DNS A records for matched domains (with caching)
|
||||||
|
for (const domain of domainsToResolve) {
|
||||||
|
const resolvedIps = await this.resolveVpnDomainIPs(domain);
|
||||||
|
for (const ip of resolvedIps) {
|
||||||
|
ips.add(`${ip}/32`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...ips];
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.vpnManager.start();
|
await this.vpnManager.start();
|
||||||
|
|
||||||
|
// Re-apply routes now that VPN clients are loaded — ensures hardcoded routes
|
||||||
|
// get correct tag-based ipAllowLists (not possible during setupSmartProxy since
|
||||||
|
// VPN server wasn't ready yet)
|
||||||
|
this.routeConfigManager?.applyRoutes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Cache for DNS-resolved IPs of VPN-gated domains. TTL: 5 minutes. */
|
||||||
|
private vpnDomainIpCache = new Map<string, { ips: string[]; expiresAt: number }>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inject VPN security into routes that have vpn.required === true.
|
* Resolve a domain's A record(s) for VPN AllowedIPs, with a 5-minute cache.
|
||||||
* Adds the VPN subnet to security.ipAllowList so only VPN clients can access them.
|
|
||||||
*/
|
*/
|
||||||
private injectVpnSecurity(routes: plugins.smartproxy.IRouteConfig[]): plugins.smartproxy.IRouteConfig[] {
|
private async resolveVpnDomainIPs(domain: string): Promise<string[]> {
|
||||||
const vpnSubnet = this.options.vpnConfig?.subnet || '10.8.0.0/24';
|
const cached = this.vpnDomainIpCache.get(domain);
|
||||||
let injectedCount = 0;
|
if (cached && cached.expiresAt > Date.now()) {
|
||||||
|
return cached.ips;
|
||||||
const result = routes.map((route) => {
|
}
|
||||||
const dcrouterRoute = route as import('../ts_interfaces/data/remoteingress.js').IDcRouterRouteConfig;
|
try {
|
||||||
if (dcrouterRoute.vpn?.required) {
|
const { promises: dnsPromises } = await import('dns');
|
||||||
injectedCount++;
|
const ips = await dnsPromises.resolve4(domain);
|
||||||
const existing = route.security?.ipAllowList || [];
|
this.vpnDomainIpCache.set(domain, { ips, expiresAt: Date.now() + 5 * 60 * 1000 });
|
||||||
|
return ips;
|
||||||
let vpnAllowList: string[];
|
} catch (err) {
|
||||||
if (dcrouterRoute.vpn.allowedServerDefinedClientTags?.length && this.vpnManager) {
|
logger.log('warn', `VPN: Failed to resolve ${domain} for AllowedIPs: ${(err as Error).message}`);
|
||||||
// Tag-based: only specific client IPs
|
return cached?.ips || []; // Return stale cache on failure, or empty
|
||||||
vpnAllowList = this.vpnManager.getClientIpsForServerDefinedTags(
|
|
||||||
dcrouterRoute.vpn.allowedServerDefinedClientTags,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// No tags specified: entire VPN subnet
|
|
||||||
vpnAllowList = [vpnSubnet];
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...route,
|
|
||||||
security: {
|
|
||||||
...route.security,
|
|
||||||
ipAllowList: [...existing, ...vpnAllowList],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return route;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (injectedCount > 0) {
|
|
||||||
logger.log('info', `VPN: Injected ipAllowList into ${injectedCount} VPN-protected route(s)`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// VPN security injection is now handled dynamically by RouteConfigManager.applyRoutes()
|
||||||
|
// via the getVpnAllowList callback — no longer a separate method here.
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set up RADIUS server for network authentication
|
* Set up RADIUS server for network authentication
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -252,41 +252,42 @@ export class RouteConfigManager {
|
|||||||
|
|
||||||
const enabledRoutes: plugins.smartproxy.IRouteConfig[] = [];
|
const enabledRoutes: plugins.smartproxy.IRouteConfig[] = [];
|
||||||
|
|
||||||
// Add enabled hardcoded routes (respecting overrides)
|
const http3Config = this.getHttp3Config?.();
|
||||||
|
const vpnAllowList = this.getVpnAllowList;
|
||||||
|
|
||||||
|
// Helper: inject VPN security into a route if vpn.required is set
|
||||||
|
const injectVpn = (route: plugins.smartproxy.IRouteConfig): plugins.smartproxy.IRouteConfig => {
|
||||||
|
if (!vpnAllowList) return route;
|
||||||
|
const dcRoute = route as IDcRouterRouteConfig;
|
||||||
|
if (!dcRoute.vpn?.required) return route;
|
||||||
|
const allowList = vpnAllowList(dcRoute.vpn.allowedServerDefinedClientTags);
|
||||||
|
return {
|
||||||
|
...route,
|
||||||
|
security: {
|
||||||
|
...route.security,
|
||||||
|
ipAllowList: [...(route.security?.ipAllowList || []), ...allowList],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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(route);
|
enabledRoutes.push(injectVpn(route));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add enabled programmatic routes (with HTTP/3 and VPN augmentation)
|
// Add enabled programmatic routes (with HTTP/3 and VPN augmentation)
|
||||||
const http3Config = this.getHttp3Config?.();
|
|
||||||
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;
|
||||||
if (http3Config && http3Config.enabled !== false) {
|
if (http3Config && http3Config.enabled !== false) {
|
||||||
route = augmentRouteWithHttp3(route, { enabled: true, ...http3Config });
|
route = augmentRouteWithHttp3(route, { enabled: true, ...http3Config });
|
||||||
}
|
}
|
||||||
// Inject VPN security for programmatic routes with vpn.required
|
enabledRoutes.push(injectVpn(route));
|
||||||
if (vpnAllowList) {
|
|
||||||
const dcRoute = route as IDcRouterRouteConfig;
|
|
||||||
if (dcRoute.vpn?.required) {
|
|
||||||
const existing = route.security?.ipAllowList || [];
|
|
||||||
const allowList = vpnAllowList(dcRoute.vpn.allowedServerDefinedClientTags);
|
|
||||||
route = {
|
|
||||||
...route,
|
|
||||||
security: {
|
|
||||||
...route.security,
|
|
||||||
ipAllowList: [...existing, ...allowList],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
enabledRoutes.push(route);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ export interface IVpnManagerConfig {
|
|||||||
allowList?: string[];
|
allowList?: string[];
|
||||||
blockList?: string[];
|
blockList?: string[];
|
||||||
};
|
};
|
||||||
|
/** Compute per-client AllowedIPs based on the client's server-defined tags.
|
||||||
|
* Called at config generation time (create/export). Returns CIDRs for WireGuard AllowedIPs.
|
||||||
|
* When not set, defaults to [subnet]. */
|
||||||
|
getClientAllowedIPs?: (clientTags: string[]) => Promise<string[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IPersistedServerKeys {
|
interface IPersistedServerKeys {
|
||||||
@@ -46,6 +50,8 @@ interface IPersistedClient {
|
|||||||
assignedIp?: string;
|
assignedIp?: string;
|
||||||
noisePublicKey: string;
|
noisePublicKey: string;
|
||||||
wgPublicKey: string;
|
wgPublicKey: string;
|
||||||
|
/** WireGuard private key — stored so exports and QR codes produce valid configs */
|
||||||
|
wgPrivateKey?: string;
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
expiresAt?: string;
|
expiresAt?: string;
|
||||||
@@ -188,7 +194,16 @@ export class VpnManager {
|
|||||||
description: opts.description,
|
description: opts.description,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Persist client entry (without private keys)
|
// Override AllowedIPs with per-client values based on tag-matched routes
|
||||||
|
if (this.config.getClientAllowedIPs && bundle.wireguardConfig) {
|
||||||
|
const allowedIPs = await this.config.getClientAllowedIPs(opts.serverDefinedClientTags || []);
|
||||||
|
bundle.wireguardConfig = bundle.wireguardConfig.replace(
|
||||||
|
/AllowedIPs\s*=\s*.+/,
|
||||||
|
`AllowedIPs = ${allowedIPs.join(', ')}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persist client entry (including WG private key for export/QR)
|
||||||
const persisted: IPersistedClient = {
|
const persisted: IPersistedClient = {
|
||||||
clientId: bundle.entry.clientId,
|
clientId: bundle.entry.clientId,
|
||||||
enabled: bundle.entry.enabled ?? true,
|
enabled: bundle.entry.enabled ?? true,
|
||||||
@@ -197,6 +212,8 @@ export class VpnManager {
|
|||||||
assignedIp: bundle.entry.assignedIp,
|
assignedIp: bundle.entry.assignedIp,
|
||||||
noisePublicKey: bundle.entry.publicKey,
|
noisePublicKey: bundle.entry.publicKey,
|
||||||
wgPublicKey: bundle.entry.wgPublicKey || '',
|
wgPublicKey: bundle.entry.wgPublicKey || '',
|
||||||
|
wgPrivateKey: bundle.secrets?.wgPrivateKey
|
||||||
|
|| bundle.wireguardConfig?.match(/PrivateKey\s*=\s*(.+)/)?.[1]?.trim(),
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
expiresAt: bundle.entry.expiresAt,
|
expiresAt: bundle.entry.expiresAt,
|
||||||
@@ -265,11 +282,13 @@ export class VpnManager {
|
|||||||
if (!this.vpnServer) throw new Error('VPN server not running');
|
if (!this.vpnServer) throw new Error('VPN server not running');
|
||||||
const bundle = await this.vpnServer.rotateClientKey(clientId);
|
const bundle = await this.vpnServer.rotateClientKey(clientId);
|
||||||
|
|
||||||
// Update persisted entry with new public keys
|
// Update persisted entry with new keys (including private key for export/QR)
|
||||||
const client = this.clients.get(clientId);
|
const client = this.clients.get(clientId);
|
||||||
if (client) {
|
if (client) {
|
||||||
client.noisePublicKey = bundle.entry.publicKey;
|
client.noisePublicKey = bundle.entry.publicKey;
|
||||||
client.wgPublicKey = bundle.entry.wgPublicKey || '';
|
client.wgPublicKey = bundle.entry.wgPublicKey || '';
|
||||||
|
client.wgPrivateKey = bundle.secrets?.wgPrivateKey
|
||||||
|
|| bundle.wireguardConfig?.match(/PrivateKey\s*=\s*(.+)/)?.[1]?.trim();
|
||||||
client.updatedAt = Date.now();
|
client.updatedAt = Date.now();
|
||||||
await this.persistClient(client);
|
await this.persistClient(client);
|
||||||
}
|
}
|
||||||
@@ -278,11 +297,35 @@ export class VpnManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Export a client config (without secrets).
|
* Export a client config. Injects stored WG private key and per-client AllowedIPs.
|
||||||
*/
|
*/
|
||||||
public async exportClientConfig(clientId: string, format: 'smartvpn' | 'wireguard'): Promise<string> {
|
public async exportClientConfig(clientId: string, format: 'smartvpn' | 'wireguard'): Promise<string> {
|
||||||
if (!this.vpnServer) throw new Error('VPN server not running');
|
if (!this.vpnServer) throw new Error('VPN server not running');
|
||||||
return this.vpnServer.exportClientConfig(clientId, format);
|
let config = await this.vpnServer.exportClientConfig(clientId, format);
|
||||||
|
|
||||||
|
if (format === 'wireguard') {
|
||||||
|
const persisted = this.clients.get(clientId);
|
||||||
|
|
||||||
|
// Inject stored WG private key so exports produce valid, scannable configs
|
||||||
|
if (persisted?.wgPrivateKey) {
|
||||||
|
config = config.replace(
|
||||||
|
'[Interface]\n',
|
||||||
|
`[Interface]\nPrivateKey = ${persisted.wgPrivateKey}\n`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override AllowedIPs with per-client values based on tag-matched routes
|
||||||
|
if (this.config.getClientAllowedIPs) {
|
||||||
|
const clientTags = persisted?.serverDefinedClientTags || [];
|
||||||
|
const allowedIPs = await this.config.getClientAllowedIPs(clientTags);
|
||||||
|
config = config.replace(
|
||||||
|
/AllowedIPs\s*=\s*.+/,
|
||||||
|
`AllowedIPs = ${allowedIPs.join(', ')}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Tag-based access control ───────────────────────────────────────────
|
// ── Tag-based access control ───────────────────────────────────────────
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '11.20.0',
|
version: '11.21.5',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user