feat(server): add configurable client endpoint and allowed IPs for generated VPN configs

This commit is contained in:
2026-03-30 17:55:27 +00:00
parent cfa91fd419
commit a1b62f6b62
4 changed files with 62 additions and 13 deletions

View File

@@ -1,5 +1,12 @@
# Changelog # 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) ## 2026-03-30 - 1.15.0 - feat(vpnserver)
add nftables-backed destination policy enforcement for TUN mode add nftables-backed destination policy enforcement for TUN mode

View File

@@ -84,6 +84,13 @@ pub struct ServerConfig {
pub wg_listen_port: Option<u16>, pub wg_listen_port: Option<u16>,
/// WireGuard: pre-configured peers. /// WireGuard: pre-configured peers.
pub wg_peers: Option<Vec<crate::wireguard::WgPeerConfig>>, pub wg_peers: Option<Vec<crate::wireguard::WgPeerConfig>>,
/// 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<String>,
/// AllowedIPs for generated WireGuard client configs.
/// Defaults to ["0.0.0.0/0"] (full tunnel).
pub client_allowed_ips: Option<Vec<String>>,
} }
/// Information about a connected client. /// Information about a connected client.
@@ -587,9 +594,12 @@ impl VpnServer {
state.client_registry.write().await.add(entry.clone())?; state.client_registry.write().await.add(entry.clone())?;
// Build SmartVPN client config // 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!({ let smartvpn_config = serde_json::json!({
"serverUrl": format!("wss://{}", "serverUrl": smartvpn_server_url,
state.config.listen_addr.replace("0.0.0.0", "localhost")),
"serverPublicKey": state.config.public_key, "serverPublicKey": state.config.public_key,
"clientPrivateKey": noise_priv, "clientPrivateKey": noise_priv,
"clientPublicKey": noise_pub, "clientPublicKey": noise_pub,
@@ -599,15 +609,21 @@ impl VpnServer {
}); });
// Build WireGuard config string // 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!( 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, wg_priv,
assigned_ip, assigned_ip,
state.config.dns.as_ref() state.config.dns.as_ref()
.map(|d| format!("DNS = {}", d.join(", "))) .map(|d| format!("DNS = {}", d.join(", ")))
.unwrap_or_default(), .unwrap_or_default(),
state.config.public_key, state.config.public_key,
state.config.listen_addr, wg_allowed_ips,
wg_endpoint,
); );
let entry_json = serde_json::to_value(&entry)?; let entry_json = serde_json::to_value(&entry)?;
@@ -732,9 +748,12 @@ impl VpnServer {
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.unwrap_or("0.0.0.0"); .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!({ let smartvpn_config = serde_json::json!({
"serverUrl": format!("wss://{}", "serverUrl": smartvpn_server_url,
state.config.listen_addr.replace("0.0.0.0", "localhost")),
"serverPublicKey": state.config.public_key, "serverPublicKey": state.config.public_key,
"clientPrivateKey": noise_priv, "clientPrivateKey": noise_priv,
"clientPublicKey": noise_pub, "clientPublicKey": noise_pub,
@@ -743,14 +762,20 @@ impl VpnServer {
"keepaliveIntervalSecs": state.config.keepalive_interval_secs, "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!( 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, wg_priv, assigned_ip,
state.config.dns.as_ref() state.config.dns.as_ref()
.map(|d| format!("DNS = {}", d.join(", "))) .map(|d| format!("DNS = {}", d.join(", ")))
.unwrap_or_default(), .unwrap_or_default(),
state.config.public_key, state.config.public_key,
state.config.listen_addr, wg_allowed_ips,
wg_endpoint,
); );
Ok(serde_json::json!({ Ok(serde_json::json!({
@@ -774,10 +799,13 @@ impl VpnServer {
match format { match format {
"smartvpn" => { "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!({ Ok(serde_json::json!({
"config": { "config": {
"serverUrl": format!("wss://{}", "serverUrl": smartvpn_server_url,
state.config.listen_addr.replace("0.0.0.0", "localhost")),
"serverPublicKey": state.config.public_key, "serverPublicKey": state.config.public_key,
"clientPublicKey": entry.public_key, "clientPublicKey": entry.public_key,
"dns": state.config.dns, "dns": state.config.dns,
@@ -788,14 +816,20 @@ impl VpnServer {
} }
"wireguard" => { "wireguard" => {
let assigned_ip = entry.assigned_ip.as_deref().unwrap_or("0.0.0.0"); 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!( 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, assigned_ip,
state.config.dns.as_ref() state.config.dns.as_ref()
.map(|d| format!("DNS = {}", d.join(", "))) .map(|d| format!("DNS = {}", d.join(", ")))
.unwrap_or_default(), .unwrap_or_default(),
state.config.public_key, state.config.public_key,
state.config.listen_addr, wg_allowed_ips,
wg_endpoint,
); );
Ok(serde_json::json!({ "config": config })) Ok(serde_json::json!({ "config": config }))
} }

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@push.rocks/smartvpn', 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' description: 'A VPN solution with TypeScript control plane and Rust data plane daemon'
} }

View File

@@ -129,6 +129,14 @@ export interface IVpnServerConfig {
* Controls where decrypted traffic goes: allow through, block, or redirect to a target. * Controls where decrypted traffic goes: allow through, block, or redirect to a target.
* Default: all traffic passes through (backward compatible). */ * Default: all traffic passes through (backward compatible). */
destinationPolicy?: IDestinationPolicy; 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[];
} }
/** /**