fix(vpn): harden VPN route access and wireguard client configuration handling
This commit is contained in:
+12
-1
@@ -740,10 +740,14 @@ export class DcRouter {
|
||||
|
||||
// VPN Server: optional, depends on SmartProxy
|
||||
if (this.options.vpnConfig?.enabled) {
|
||||
const vpnServiceDeps = ['SmartProxy'];
|
||||
if (this.options.dbConfig?.enabled !== false) {
|
||||
vpnServiceDeps.push('ConfigManagers');
|
||||
}
|
||||
this.serviceManager.addService(
|
||||
new plugins.taskbuffer.Service('VpnServer')
|
||||
.optional()
|
||||
.dependsOn('SmartProxy')
|
||||
.dependsOn(...vpnServiceDeps)
|
||||
.withStart(async () => {
|
||||
await this.setupVpnServer();
|
||||
})
|
||||
@@ -2418,6 +2422,13 @@ export class DcRouter {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.options.dbConfig?.enabled === false) {
|
||||
throw new Error('VPN requires dbConfig.enabled because clients, keys, routes, and target profiles are persisted in DcRouterDb');
|
||||
}
|
||||
if (!this.routeConfigManager || !this.targetProfileManager) {
|
||||
throw new Error('VPN requires initialized route and target profile managers');
|
||||
}
|
||||
|
||||
logger.log('info', 'Setting up VPN server...');
|
||||
|
||||
this.vpnManager = new VpnManager({
|
||||
|
||||
@@ -607,19 +607,21 @@ export class RouteConfigManager {
|
||||
route: plugins.smartproxy.IRouteConfig,
|
||||
routeId?: string,
|
||||
): plugins.smartproxy.IRouteConfig {
|
||||
const vpnCallback = this.getVpnClientIpsForRoute;
|
||||
if (!vpnCallback) return route;
|
||||
|
||||
const dcRoute = route as IDcRouterRouteConfig;
|
||||
if (!dcRoute.vpnOnly) return route;
|
||||
|
||||
const vpnEntries = vpnCallback(dcRoute, routeId);
|
||||
const existingEntries = route.security?.ipAllowList || [];
|
||||
const vpnEntries = this.getVpnClientIpsForRoute?.(dcRoute, routeId) || [];
|
||||
const existingBlockList = route.security?.ipBlockList || [];
|
||||
const ipBlockList = vpnEntries.length
|
||||
? existingBlockList
|
||||
: [...new Set([...existingBlockList, '*'])];
|
||||
|
||||
return {
|
||||
...route,
|
||||
security: {
|
||||
...route.security,
|
||||
ipAllowList: [...existingEntries, ...vpnEntries],
|
||||
ipAllowList: vpnEntries,
|
||||
ipBlockList,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -88,6 +88,8 @@ export class TargetProfileHandler {
|
||||
routeRefs: dataArg.routeRefs,
|
||||
createdBy: userId,
|
||||
});
|
||||
await this.opsServerRef.dcRouterRef.routeConfigManager?.applyRoutes();
|
||||
await this.opsServerRef.dcRouterRef.vpnManager?.refreshAllClientSecurity();
|
||||
return { success: true, id };
|
||||
},
|
||||
),
|
||||
|
||||
@@ -111,6 +111,7 @@ export class VpnManager {
|
||||
|
||||
const subnet = this.getSubnet();
|
||||
const wgListenPort = this.config.wgListenPort ?? 51820;
|
||||
const serverEndpoint = this.getWireGuardServerEndpoint();
|
||||
|
||||
const desiredForwardingMode = this.getDesiredForwardingMode(anyClientUsesHostIp);
|
||||
if (anyClientUsesHostIp && desiredForwardingMode === 'hybrid') {
|
||||
@@ -133,21 +134,19 @@ export class VpnManager {
|
||||
: { 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
|
||||
listenAddr: '127.0.0.1:0', // Required by smartvpn, unused in wireguard-only mode
|
||||
privateKey: this.serverKeys.noisePrivateKey,
|
||||
publicKey: this.serverKeys.noisePublicKey,
|
||||
subnet,
|
||||
dns: this.config.dns,
|
||||
forwardingMode: forwardingMode as any,
|
||||
transportMode: 'all',
|
||||
transportMode: 'wireguard',
|
||||
wgPrivateKey: this.serverKeys.wgPrivateKey,
|
||||
wgListenPort,
|
||||
clients: clientEntries,
|
||||
socketForwardProxyProtocol: !isBridge,
|
||||
destinationPolicy: this.getServerDestinationPolicy(forwardingMode, defaultDestinationPolicy),
|
||||
serverEndpoint: this.config.serverEndpoint
|
||||
? `${this.config.serverEndpoint}:${wgListenPort}`
|
||||
: undefined,
|
||||
serverEndpoint,
|
||||
clientAllowedIPs: [subnet],
|
||||
// Bridge-specific config
|
||||
...(isBridge ? {
|
||||
@@ -187,7 +186,7 @@ export class VpnManager {
|
||||
} catch {
|
||||
// Ignore stop errors
|
||||
}
|
||||
this.vpnServer.stop();
|
||||
await this.vpnServer.stop();
|
||||
this.vpnServer = undefined;
|
||||
}
|
||||
this.resolvedForwardingMode = undefined;
|
||||
@@ -244,14 +243,10 @@ export class VpnManager {
|
||||
vlanId: doc.vlanId,
|
||||
});
|
||||
|
||||
// Override AllowedIPs with per-client values based on target profiles
|
||||
if (this.config.getClientAllowedIPs && bundle.wireguardConfig) {
|
||||
const allowedIPs = await this.config.getClientAllowedIPs(doc.targetProfileIds || []);
|
||||
bundle.wireguardConfig = bundle.wireguardConfig.replace(
|
||||
/AllowedIPs\s*=\s*.+/,
|
||||
`AllowedIPs = ${allowedIPs.join(', ')}`,
|
||||
);
|
||||
}
|
||||
bundle.wireguardConfig = await this.rewriteWireGuardAllowedIPs(
|
||||
bundle.wireguardConfig,
|
||||
doc.targetProfileIds || [],
|
||||
);
|
||||
|
||||
// Persist client entry (including WG private key for export/QR)
|
||||
doc.clientId = bundle.entry.clientId;
|
||||
@@ -381,9 +376,13 @@ export class VpnManager {
|
||||
public async rotateClientKey(clientId: string): Promise<plugins.smartvpn.IClientConfigBundle> {
|
||||
if (!this.vpnServer) throw new Error('VPN server not running');
|
||||
const bundle = await this.vpnServer.rotateClientKey(clientId);
|
||||
const client = this.clients.get(clientId);
|
||||
bundle.wireguardConfig = await this.rewriteWireGuardAllowedIPs(
|
||||
bundle.wireguardConfig,
|
||||
client?.targetProfileIds || [],
|
||||
);
|
||||
|
||||
// Update persisted entry with new keys (including private key for export/QR)
|
||||
const client = this.clients.get(clientId);
|
||||
if (client) {
|
||||
client.noisePublicKey = bundle.entry.publicKey;
|
||||
client.wgPublicKey = bundle.entry.wgPublicKey || '';
|
||||
@@ -414,15 +413,7 @@ export class VpnManager {
|
||||
);
|
||||
}
|
||||
|
||||
// Override AllowedIPs with per-client values based on target profiles
|
||||
if (this.config.getClientAllowedIPs) {
|
||||
const profileIds = persisted?.targetProfileIds || [];
|
||||
const allowedIPs = await this.config.getClientAllowedIPs(profileIds);
|
||||
config = config.replace(
|
||||
/AllowedIPs\s*=\s*.+/,
|
||||
`AllowedIPs = ${allowedIPs.join(', ')}`,
|
||||
);
|
||||
}
|
||||
config = await this.rewriteWireGuardAllowedIPs(config, persisted?.targetProfileIds || []);
|
||||
}
|
||||
|
||||
return config;
|
||||
@@ -515,6 +506,46 @@ export class VpnManager {
|
||||
}
|
||||
}
|
||||
|
||||
private getWireGuardServerEndpoint(): string {
|
||||
const endpoint = this.config.serverEndpoint?.trim();
|
||||
if (!endpoint) {
|
||||
throw new Error('vpnConfig.serverEndpoint is required when VPN is enabled');
|
||||
}
|
||||
if (endpoint.includes('://') || endpoint.includes('/')) {
|
||||
throw new Error('vpnConfig.serverEndpoint must be a host or host:port, not a URL');
|
||||
}
|
||||
|
||||
const host = endpoint.includes(':') ? endpoint.split(':')[0] : endpoint;
|
||||
const lowerHost = host.toLowerCase();
|
||||
if (
|
||||
lowerHost === 'localhost'
|
||||
|| lowerHost === '0.0.0.0'
|
||||
|| lowerHost.startsWith('127.')
|
||||
) {
|
||||
throw new Error('vpnConfig.serverEndpoint must be reachable by VPN clients');
|
||||
}
|
||||
|
||||
return endpoint.includes(':')
|
||||
? endpoint
|
||||
: `${endpoint}:${this.config.wgListenPort ?? 51820}`;
|
||||
}
|
||||
|
||||
private async rewriteWireGuardAllowedIPs(
|
||||
wireguardConfig: string,
|
||||
targetProfileIds: string[],
|
||||
): Promise<string> {
|
||||
if (!this.config.getClientAllowedIPs) return wireguardConfig;
|
||||
|
||||
const allowedIPs = await this.config.getClientAllowedIPs(targetProfileIds);
|
||||
const effectiveAllowedIPs = allowedIPs.length ? allowedIPs : [this.getSubnet()];
|
||||
const allowedLine = `AllowedIPs = ${effectiveAllowedIPs.join(', ')}`;
|
||||
|
||||
if (/^AllowedIPs\s*=.*$/m.test(wireguardConfig)) {
|
||||
return wireguardConfig.replace(/^AllowedIPs\s*=.*$/m, allowedLine);
|
||||
}
|
||||
return `${wireguardConfig.trimEnd()}\n${allowedLine}\n`;
|
||||
}
|
||||
|
||||
// ── Private helpers ────────────────────────────────────────────────────
|
||||
|
||||
private async loadOrGenerateServerKeys(): Promise<VpnServerKeysDoc> {
|
||||
@@ -532,7 +563,7 @@ export class VpnManager {
|
||||
|
||||
const noiseKeys = await tempServer.generateKeypair();
|
||||
const wgKeys = await tempServer.generateWgKeypair();
|
||||
tempServer.stop();
|
||||
await tempServer.stop();
|
||||
|
||||
const doc = stored || new VpnServerKeysDoc();
|
||||
doc.noisePrivateKey = noiseKeys.privateKey;
|
||||
|
||||
Reference in New Issue
Block a user