feat(vpn): add destination-based VPN routing policy and standardize socket proxy forwarding

This commit is contained in:
2026-03-30 13:06:14 +00:00
parent d53cff6a94
commit cc3a7cb5b6
10 changed files with 47 additions and 47 deletions

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/dcrouter',
version: '11.15.0',
version: '11.16.0',
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
}

View File

@@ -206,14 +206,21 @@ export interface IDcRouterOptions {
dns?: string[];
/** Server endpoint hostname for client configs (e.g. 'vpn.example.com') */
serverEndpoint?: string;
/** Override forwarding mode. Default: auto-detect (tun if root, socket otherwise) */
forwardingMode?: 'tun' | 'socket';
/** Pre-defined VPN clients created on startup */
clients?: Array<{
clientId: string;
serverDefinedClientTags?: string[];
description?: string;
}>;
/** Destination routing policy for VPN client traffic.
* Default in socket mode: { default: 'forceTarget', target: '127.0.0.1' } (all traffic → SmartProxy).
* Default in tun mode: not set (all traffic passes through). */
destinationPolicy?: {
default: 'forceTarget' | 'block' | 'allow';
target?: string;
allowList?: string[];
blockList?: string[];
};
};
}
@@ -677,9 +684,8 @@ export class DcRouter {
if (this.vpnManager && this.options.vpnConfig?.enabled) {
const subnet = this.vpnManager.getSubnet();
const wgPort = this.options.vpnConfig.wgListenPort ?? 51820;
const mode = this.vpnManager.forwardingMode;
const clientCount = this.vpnManager.listClients().length;
logger.log('info', `VPN Service: mode=${mode}, subnet=${subnet}, wg=:${wgPort}, clients=${clientCount}`);
logger.log('info', `VPN Service: subnet=${subnet}, wg=:${wgPort}, clients=${clientCount}`);
}
// Remote Ingress summary
@@ -963,19 +969,14 @@ export class DcRouter {
smartProxyConfig.proxyIPs = ['127.0.0.1'];
}
// When VPN is in socket mode, the userspace NAT engine sends PP v2 headers
// on outbound connections to SmartProxy to preserve VPN client tunnel IPs.
// VPN uses socket mode with PP v2 — SmartProxy must accept proxy protocol from localhost
if (this.options.vpnConfig?.enabled) {
const vpnForwardingMode = this.options.vpnConfig.forwardingMode
?? (process.getuid?.() === 0 ? 'tun' : 'socket');
if (vpnForwardingMode === 'socket') {
smartProxyConfig.acceptProxyProtocol = true;
if (!smartProxyConfig.proxyIPs) {
smartProxyConfig.proxyIPs = [];
}
if (!smartProxyConfig.proxyIPs.includes('127.0.0.1')) {
smartProxyConfig.proxyIPs.push('127.0.0.1');
}
smartProxyConfig.acceptProxyProtocol = true;
if (!smartProxyConfig.proxyIPs) {
smartProxyConfig.proxyIPs = [];
}
if (!smartProxyConfig.proxyIPs.includes('127.0.0.1')) {
smartProxyConfig.proxyIPs.push('127.0.0.1');
}
}
@@ -2098,8 +2099,8 @@ export class DcRouter {
wgListenPort: this.options.vpnConfig.wgListenPort,
dns: this.options.vpnConfig.dns,
serverEndpoint: this.options.vpnConfig.serverEndpoint,
forwardingMode: this.options.vpnConfig.forwardingMode,
initialClients: this.options.vpnConfig.clients,
destinationPolicy: this.options.vpnConfig.destinationPolicy,
onClientChanged: () => {
// Re-apply routes so tag-based ipAllowLists get updated
this.routeConfigManager?.applyRoutes();

View File

@@ -48,7 +48,6 @@ export class VpnHandler {
return {
status: {
running: false,
forwardingMode: 'socket' as const,
subnet: vpnConfig?.subnet || '10.8.0.0/24',
wgListenPort: vpnConfig?.wgListenPort ?? 51820,
serverPublicKeys: null,
@@ -62,7 +61,6 @@ export class VpnHandler {
return {
status: {
running: manager.running,
forwardingMode: manager.forwardingMode,
subnet: manager.getSubnet(),
wgListenPort: vpnConfig?.wgListenPort ?? 51820,
serverPublicKeys: manager.getServerPublicKeys(),

View File

@@ -14,8 +14,6 @@ export interface IVpnManagerConfig {
dns?: string[];
/** Server endpoint hostname for client configs (e.g. 'vpn.example.com') */
serverEndpoint?: string;
/** Override forwarding mode. Default: auto-detect (tun if root, socket otherwise) */
forwardingMode?: 'tun' | 'socket';
/** Pre-defined VPN clients created on startup (idempotent — skips already-persisted clients) */
initialClients?: Array<{
clientId: string;
@@ -24,6 +22,13 @@ export interface IVpnManagerConfig {
}>;
/** Called when clients are created/deleted/toggled — triggers route re-application */
onClientChanged?: () => void;
/** Destination routing policy override. Default: forceTarget to 127.0.0.1 */
destinationPolicy?: {
default: 'forceTarget' | 'block' | 'allow';
target?: string;
allowList?: string[];
blockList?: string[];
};
}
interface IPersistedServerKeys {
@@ -58,19 +63,10 @@ export class VpnManager {
private vpnServer?: plugins.smartvpn.VpnServer;
private clients: Map<string, IPersistedClient> = new Map();
private serverKeys?: IPersistedServerKeys;
private _forwardingMode: 'tun' | 'socket';
constructor(storageManager: StorageManager, config: IVpnManagerConfig) {
this.storageManager = storageManager;
this.config = config;
// Auto-detect forwarding mode: tun if root, socket otherwise
this._forwardingMode = config.forwardingMode
?? (process.getuid?.() === 0 ? 'tun' : 'socket');
}
/** The effective forwarding mode (tun or socket). */
public get forwardingMode(): 'tun' | 'socket' {
return this._forwardingMode;
}
/** The VPN subnet CIDR. */
@@ -123,12 +119,14 @@ export class VpnManager {
publicKey: this.serverKeys.noisePublicKey,
subnet,
dns: this.config.dns,
forwardingMode: this._forwardingMode,
forwardingMode: 'socket',
transportMode: 'all',
wgPrivateKey: this.serverKeys.wgPrivateKey,
wgListenPort,
clients: clientEntries,
socketForwardProxyProtocol: this._forwardingMode === 'socket',
socketForwardProxyProtocol: true,
destinationPolicy: this.config.destinationPolicy
?? { default: 'forceTarget' as const, target: '127.0.0.1' },
};
await this.vpnServer.start(serverConfig);
@@ -147,7 +145,7 @@ export class VpnManager {
}
}
logger.log('info', `VPN server started: mode=${this._forwardingMode}, subnet=${subnet}, wg=:${wgListenPort}, clients=${this.clients.size}`);
logger.log('info', `VPN server started: subnet=${subnet}, wg=:${wgListenPort}, clients=${this.clients.size}`);
}
/**