diff --git a/changelog.md b/changelog.md index 198abcb..f8a4781 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,12 @@ # Changelog +## 2026-03-30 - 1.16.0 - feat(server) +add configurable client endpoint and allowed IPs for generated VPN configs + +- adds serverEndpoint to generated SmartVPN and WireGuard client configs so remote clients can use a public address instead of the listen address +- adds clientAllowedIPs to generated WireGuard configs to support full-tunnel or split-tunnel routing +- updates TypeScript interfaces to expose the new server configuration options + ## 2026-03-30 - 1.15.0 - feat(vpnserver) add nftables-backed destination policy enforcement for TUN mode diff --git a/rust/src/server.rs b/rust/src/server.rs index 1fd2076..291544a 100644 --- a/rust/src/server.rs +++ b/rust/src/server.rs @@ -84,6 +84,13 @@ pub struct ServerConfig { pub wg_listen_port: Option, /// WireGuard: pre-configured peers. pub wg_peers: Option>, + /// Public endpoint address for generated client configs (e.g. "vpn.example.com:51820"). + /// Used as WireGuard `Endpoint` and SmartVPN `serverUrl` host. + /// Defaults to listen_addr. + pub server_endpoint: Option, + /// AllowedIPs for generated WireGuard client configs. + /// Defaults to ["0.0.0.0/0"] (full tunnel). + pub client_allowed_ips: Option>, } /// Information about a connected client. @@ -587,9 +594,12 @@ impl VpnServer { state.client_registry.write().await.add(entry.clone())?; // Build SmartVPN client config + let smartvpn_server_url = format!("wss://{}", + state.config.server_endpoint.as_deref() + .unwrap_or(&state.config.listen_addr) + .replace("0.0.0.0", "localhost")); let smartvpn_config = serde_json::json!({ - "serverUrl": format!("wss://{}", - state.config.listen_addr.replace("0.0.0.0", "localhost")), + "serverUrl": smartvpn_server_url, "serverPublicKey": state.config.public_key, "clientPrivateKey": noise_priv, "clientPublicKey": noise_pub, @@ -599,15 +609,21 @@ impl VpnServer { }); // Build WireGuard config string + let wg_endpoint = state.config.server_endpoint.as_deref() + .unwrap_or(&state.config.listen_addr); + let wg_allowed_ips = state.config.client_allowed_ips.as_ref() + .map(|ips| ips.join(", ")) + .unwrap_or_else(|| "0.0.0.0/0".to_string()); let wg_config = format!( - "[Interface]\nPrivateKey = {}\nAddress = {}/24\n{}\n[Peer]\nPublicKey = {}\nAllowedIPs = 0.0.0.0/0\nEndpoint = {}\nPersistentKeepalive = 25\n", + "[Interface]\nPrivateKey = {}\nAddress = {}/24\n{}\n[Peer]\nPublicKey = {}\nAllowedIPs = {}\nEndpoint = {}\nPersistentKeepalive = 25\n", wg_priv, assigned_ip, state.config.dns.as_ref() .map(|d| format!("DNS = {}", d.join(", "))) .unwrap_or_default(), state.config.public_key, - state.config.listen_addr, + wg_allowed_ips, + wg_endpoint, ); let entry_json = serde_json::to_value(&entry)?; @@ -732,9 +748,12 @@ impl VpnServer { .and_then(|v| v.as_str()) .unwrap_or("0.0.0.0"); + let smartvpn_server_url = format!("wss://{}", + state.config.server_endpoint.as_deref() + .unwrap_or(&state.config.listen_addr) + .replace("0.0.0.0", "localhost")); let smartvpn_config = serde_json::json!({ - "serverUrl": format!("wss://{}", - state.config.listen_addr.replace("0.0.0.0", "localhost")), + "serverUrl": smartvpn_server_url, "serverPublicKey": state.config.public_key, "clientPrivateKey": noise_priv, "clientPublicKey": noise_pub, @@ -743,14 +762,20 @@ impl VpnServer { "keepaliveIntervalSecs": state.config.keepalive_interval_secs, }); + let wg_endpoint = state.config.server_endpoint.as_deref() + .unwrap_or(&state.config.listen_addr); + let wg_allowed_ips = state.config.client_allowed_ips.as_ref() + .map(|ips| ips.join(", ")) + .unwrap_or_else(|| "0.0.0.0/0".to_string()); let wg_config = format!( - "[Interface]\nPrivateKey = {}\nAddress = {}/24\n{}\n[Peer]\nPublicKey = {}\nAllowedIPs = 0.0.0.0/0\nEndpoint = {}\nPersistentKeepalive = 25\n", + "[Interface]\nPrivateKey = {}\nAddress = {}/24\n{}\n[Peer]\nPublicKey = {}\nAllowedIPs = {}\nEndpoint = {}\nPersistentKeepalive = 25\n", wg_priv, assigned_ip, state.config.dns.as_ref() .map(|d| format!("DNS = {}", d.join(", "))) .unwrap_or_default(), state.config.public_key, - state.config.listen_addr, + wg_allowed_ips, + wg_endpoint, ); Ok(serde_json::json!({ @@ -774,10 +799,13 @@ impl VpnServer { match format { "smartvpn" => { + let smartvpn_server_url = format!("wss://{}", + state.config.server_endpoint.as_deref() + .unwrap_or(&state.config.listen_addr) + .replace("0.0.0.0", "localhost")); Ok(serde_json::json!({ "config": { - "serverUrl": format!("wss://{}", - state.config.listen_addr.replace("0.0.0.0", "localhost")), + "serverUrl": smartvpn_server_url, "serverPublicKey": state.config.public_key, "clientPublicKey": entry.public_key, "dns": state.config.dns, @@ -788,14 +816,20 @@ impl VpnServer { } "wireguard" => { let assigned_ip = entry.assigned_ip.as_deref().unwrap_or("0.0.0.0"); + let wg_endpoint = state.config.server_endpoint.as_deref() + .unwrap_or(&state.config.listen_addr); + let wg_allowed_ips = state.config.client_allowed_ips.as_ref() + .map(|ips| ips.join(", ")) + .unwrap_or_else(|| "0.0.0.0/0".to_string()); let config = format!( - "[Interface]\nAddress = {}/24\n{}\n[Peer]\nPublicKey = {}\nAllowedIPs = 0.0.0.0/0\nEndpoint = {}\nPersistentKeepalive = 25\n", + "[Interface]\nAddress = {}/24\n{}\n[Peer]\nPublicKey = {}\nAllowedIPs = {}\nEndpoint = {}\nPersistentKeepalive = 25\n", assigned_ip, state.config.dns.as_ref() .map(|d| format!("DNS = {}", d.join(", "))) .unwrap_or_default(), state.config.public_key, - state.config.listen_addr, + wg_allowed_ips, + wg_endpoint, ); Ok(serde_json::json!({ "config": config })) } diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 3091704..a995b3a 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@push.rocks/smartvpn', - version: '1.15.0', + version: '1.16.0', description: 'A VPN solution with TypeScript control plane and Rust data plane daemon' } diff --git a/ts/smartvpn.interfaces.ts b/ts/smartvpn.interfaces.ts index 8c2dea4..e436a5a 100644 --- a/ts/smartvpn.interfaces.ts +++ b/ts/smartvpn.interfaces.ts @@ -129,6 +129,14 @@ export interface IVpnServerConfig { * Controls where decrypted traffic goes: allow through, block, or redirect to a target. * Default: all traffic passes through (backward compatible). */ destinationPolicy?: IDestinationPolicy; + /** Public endpoint address for generated client configs (e.g. 'vpn.example.com:51820'). + * Used as the WireGuard `Endpoint =` and SmartVPN `serverUrl` host. + * Defaults to listenAddr (which is typically wrong for remote clients). */ + serverEndpoint?: string; + /** AllowedIPs for generated WireGuard client configs. + * Controls what traffic the client routes through the VPN tunnel. + * Defaults to ['0.0.0.0/0'] (full tunnel). Set to e.g. ['10.8.0.0/24'] for split tunnel. */ + clientAllowedIPs?: string[]; } /**