feat(vpn): add destination-based VPN routing policy and standardize socket proxy forwarding
This commit is contained in:
@@ -1,5 +1,13 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-03-30 - 11.16.0 - feat(vpn)
|
||||||
|
add destination-based VPN routing policy and standardize socket proxy forwarding
|
||||||
|
|
||||||
|
- replace configurable VPN forwarding mode with socket-based forwarding and always enable proxy protocol support to SmartProxy from localhost
|
||||||
|
- add destinationPolicy configuration for controlling default VPN traffic handling, including forceTarget, allow, and block rules
|
||||||
|
- remove forwarding mode reporting from VPN status APIs, logs, and ops UI to reflect the simplified VPN runtime model
|
||||||
|
- update @push.rocks/smartvpn to 1.14.0 to support the new VPN routing behavior
|
||||||
|
|
||||||
## 2026-03-30 - 11.15.0 - feat(vpn)
|
## 2026-03-30 - 11.15.0 - feat(vpn)
|
||||||
add tag-based VPN route access control and support configured initial VPN clients
|
add tag-based VPN route access control and support configured initial VPN clients
|
||||||
|
|
||||||
|
|||||||
@@ -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.13.0",
|
"@push.rocks/smartvpn": "1.14.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.13.0
|
specifier: 1.14.0
|
||||||
version: 1.13.0
|
version: 1.14.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.13.0':
|
'@push.rocks/smartvpn@1.14.0':
|
||||||
resolution: {integrity: sha512-oQY+GIvB9OZQMFEI/f4zwKwaUWPgG8Fsz8AGhPDedvH32jYNYEb9B957yRAROf7ndyQM/LThm7mN/5cx8ALyLw==}
|
resolution: {integrity: sha512-zJmHiuLwY4OEN4jBVrJf1hAXpfO9f6Bulq/v1DrB16nR7VgE82KNqRLt0Wi/9PCsAUfmVJTvOf4yirnjBrEWQg==}
|
||||||
|
|
||||||
'@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.13.0':
|
'@push.rocks/smartvpn@1.14.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.15.0',
|
version: '11.16.0',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -206,14 +206,21 @@ export interface IDcRouterOptions {
|
|||||||
dns?: string[];
|
dns?: string[];
|
||||||
/** Server endpoint hostname for client configs (e.g. 'vpn.example.com') */
|
/** Server endpoint hostname for client configs (e.g. 'vpn.example.com') */
|
||||||
serverEndpoint?: string;
|
serverEndpoint?: string;
|
||||||
/** Override forwarding mode. Default: auto-detect (tun if root, socket otherwise) */
|
|
||||||
forwardingMode?: 'tun' | 'socket';
|
|
||||||
/** Pre-defined VPN clients created on startup */
|
/** Pre-defined VPN clients created on startup */
|
||||||
clients?: Array<{
|
clients?: Array<{
|
||||||
clientId: string;
|
clientId: string;
|
||||||
serverDefinedClientTags?: string[];
|
serverDefinedClientTags?: string[];
|
||||||
description?: 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) {
|
if (this.vpnManager && this.options.vpnConfig?.enabled) {
|
||||||
const subnet = this.vpnManager.getSubnet();
|
const subnet = this.vpnManager.getSubnet();
|
||||||
const wgPort = this.options.vpnConfig.wgListenPort ?? 51820;
|
const wgPort = this.options.vpnConfig.wgListenPort ?? 51820;
|
||||||
const mode = this.vpnManager.forwardingMode;
|
|
||||||
const clientCount = this.vpnManager.listClients().length;
|
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
|
// Remote Ingress summary
|
||||||
@@ -963,19 +969,14 @@ export class DcRouter {
|
|||||||
smartProxyConfig.proxyIPs = ['127.0.0.1'];
|
smartProxyConfig.proxyIPs = ['127.0.0.1'];
|
||||||
}
|
}
|
||||||
|
|
||||||
// When VPN is in socket mode, the userspace NAT engine sends PP v2 headers
|
// VPN uses socket mode with PP v2 — SmartProxy must accept proxy protocol from localhost
|
||||||
// on outbound connections to SmartProxy to preserve VPN client tunnel IPs.
|
|
||||||
if (this.options.vpnConfig?.enabled) {
|
if (this.options.vpnConfig?.enabled) {
|
||||||
const vpnForwardingMode = this.options.vpnConfig.forwardingMode
|
smartProxyConfig.acceptProxyProtocol = true;
|
||||||
?? (process.getuid?.() === 0 ? 'tun' : 'socket');
|
if (!smartProxyConfig.proxyIPs) {
|
||||||
if (vpnForwardingMode === 'socket') {
|
smartProxyConfig.proxyIPs = [];
|
||||||
smartProxyConfig.acceptProxyProtocol = true;
|
}
|
||||||
if (!smartProxyConfig.proxyIPs) {
|
if (!smartProxyConfig.proxyIPs.includes('127.0.0.1')) {
|
||||||
smartProxyConfig.proxyIPs = [];
|
smartProxyConfig.proxyIPs.push('127.0.0.1');
|
||||||
}
|
|
||||||
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,
|
wgListenPort: this.options.vpnConfig.wgListenPort,
|
||||||
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,
|
|
||||||
initialClients: this.options.vpnConfig.clients,
|
initialClients: this.options.vpnConfig.clients,
|
||||||
|
destinationPolicy: this.options.vpnConfig.destinationPolicy,
|
||||||
onClientChanged: () => {
|
onClientChanged: () => {
|
||||||
// Re-apply routes so tag-based ipAllowLists get updated
|
// Re-apply routes so tag-based ipAllowLists get updated
|
||||||
this.routeConfigManager?.applyRoutes();
|
this.routeConfigManager?.applyRoutes();
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ export class VpnHandler {
|
|||||||
return {
|
return {
|
||||||
status: {
|
status: {
|
||||||
running: false,
|
running: false,
|
||||||
forwardingMode: 'socket' as const,
|
|
||||||
subnet: vpnConfig?.subnet || '10.8.0.0/24',
|
subnet: vpnConfig?.subnet || '10.8.0.0/24',
|
||||||
wgListenPort: vpnConfig?.wgListenPort ?? 51820,
|
wgListenPort: vpnConfig?.wgListenPort ?? 51820,
|
||||||
serverPublicKeys: null,
|
serverPublicKeys: null,
|
||||||
@@ -62,7 +61,6 @@ export class VpnHandler {
|
|||||||
return {
|
return {
|
||||||
status: {
|
status: {
|
||||||
running: manager.running,
|
running: manager.running,
|
||||||
forwardingMode: manager.forwardingMode,
|
|
||||||
subnet: manager.getSubnet(),
|
subnet: manager.getSubnet(),
|
||||||
wgListenPort: vpnConfig?.wgListenPort ?? 51820,
|
wgListenPort: vpnConfig?.wgListenPort ?? 51820,
|
||||||
serverPublicKeys: manager.getServerPublicKeys(),
|
serverPublicKeys: manager.getServerPublicKeys(),
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ export interface IVpnManagerConfig {
|
|||||||
dns?: string[];
|
dns?: string[];
|
||||||
/** Server endpoint hostname for client configs (e.g. 'vpn.example.com') */
|
/** Server endpoint hostname for client configs (e.g. 'vpn.example.com') */
|
||||||
serverEndpoint?: string;
|
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) */
|
/** Pre-defined VPN clients created on startup (idempotent — skips already-persisted clients) */
|
||||||
initialClients?: Array<{
|
initialClients?: Array<{
|
||||||
clientId: string;
|
clientId: string;
|
||||||
@@ -24,6 +22,13 @@ export interface IVpnManagerConfig {
|
|||||||
}>;
|
}>;
|
||||||
/** Called when clients are created/deleted/toggled — triggers route re-application */
|
/** Called when clients are created/deleted/toggled — triggers route re-application */
|
||||||
onClientChanged?: () => void;
|
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 {
|
interface IPersistedServerKeys {
|
||||||
@@ -58,19 +63,10 @@ export class VpnManager {
|
|||||||
private vpnServer?: plugins.smartvpn.VpnServer;
|
private vpnServer?: plugins.smartvpn.VpnServer;
|
||||||
private clients: Map<string, IPersistedClient> = new Map();
|
private clients: Map<string, IPersistedClient> = new Map();
|
||||||
private serverKeys?: IPersistedServerKeys;
|
private serverKeys?: IPersistedServerKeys;
|
||||||
private _forwardingMode: 'tun' | 'socket';
|
|
||||||
|
|
||||||
constructor(storageManager: StorageManager, config: IVpnManagerConfig) {
|
constructor(storageManager: StorageManager, config: IVpnManagerConfig) {
|
||||||
this.storageManager = storageManager;
|
this.storageManager = storageManager;
|
||||||
this.config = config;
|
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. */
|
/** The VPN subnet CIDR. */
|
||||||
@@ -123,12 +119,14 @@ export class VpnManager {
|
|||||||
publicKey: this.serverKeys.noisePublicKey,
|
publicKey: this.serverKeys.noisePublicKey,
|
||||||
subnet,
|
subnet,
|
||||||
dns: this.config.dns,
|
dns: this.config.dns,
|
||||||
forwardingMode: this._forwardingMode,
|
forwardingMode: 'socket',
|
||||||
transportMode: 'all',
|
transportMode: 'all',
|
||||||
wgPrivateKey: this.serverKeys.wgPrivateKey,
|
wgPrivateKey: this.serverKeys.wgPrivateKey,
|
||||||
wgListenPort,
|
wgListenPort,
|
||||||
clients: clientEntries,
|
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);
|
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}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ export interface IVpnClient {
|
|||||||
*/
|
*/
|
||||||
export interface IVpnServerStatus {
|
export interface IVpnServerStatus {
|
||||||
running: boolean;
|
running: boolean;
|
||||||
forwardingMode: 'tun' | 'socket';
|
|
||||||
subnet: string;
|
subnet: string;
|
||||||
wgListenPort: number;
|
wgListenPort: number;
|
||||||
serverPublicKeys: {
|
serverPublicKeys: {
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '11.15.0',
|
version: '11.16.0',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ export class OpsViewVpn extends DeesElement {
|
|||||||
type: 'text',
|
type: 'text',
|
||||||
value: status?.running ? 'Running' : 'Stopped',
|
value: status?.running ? 'Running' : 'Stopped',
|
||||||
icon: 'lucide:server',
|
icon: 'lucide:server',
|
||||||
description: status?.running ? `${status.forwardingMode} mode` : 'VPN server not running',
|
description: status?.running ? 'Active' : 'VPN server not running',
|
||||||
color: status?.running ? '#10b981' : '#ef4444',
|
color: status?.running ? '#10b981' : '#ef4444',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -232,10 +232,6 @@ export class OpsViewVpn extends DeesElement {
|
|||||||
<span class="infoLabel">WireGuard Port</span>
|
<span class="infoLabel">WireGuard Port</span>
|
||||||
<span class="infoValue">${status.wgListenPort}</span>
|
<span class="infoValue">${status.wgListenPort}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="infoItem">
|
|
||||||
<span class="infoLabel">Forwarding Mode</span>
|
|
||||||
<span class="infoValue">${status.forwardingMode}</span>
|
|
||||||
</div>
|
|
||||||
${status.serverPublicKeys ? html`
|
${status.serverPublicKeys ? html`
|
||||||
<div class="infoItem">
|
<div class="infoItem">
|
||||||
<span class="infoLabel">WG Public Key</span>
|
<span class="infoLabel">WG Public Key</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user