Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 049fa00563 | |||
| e4e59d72f9 |
@@ -1,5 +1,14 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-03-29 - 1.5.0 - feat(wireguard)
|
||||||
|
add WireGuard transport support with management APIs and config generation
|
||||||
|
|
||||||
|
- add Rust WireGuard module integration using boringtun and route management through client/server management handlers
|
||||||
|
- extend TypeScript client and server configuration schemas with WireGuard-specific options and validation
|
||||||
|
- add server-side WireGuard peer management commands including keypair generation, peer add/remove, and peer listing
|
||||||
|
- export a WireGuard config generator for producing client and server .conf files
|
||||||
|
- add WireGuard-focused test coverage for config validation and config generation
|
||||||
|
|
||||||
## 2026-03-21 - 1.4.1 - fix(readme)
|
## 2026-03-21 - 1.4.1 - fix(readme)
|
||||||
preserve markdown line breaks in feature list
|
preserve markdown line breaks in feature list
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/smartvpn",
|
"name": "@push.rocks/smartvpn",
|
||||||
"version": "1.4.1",
|
"version": "1.5.0",
|
||||||
"private": false,
|
"private": false,
|
||||||
"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",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
130
rust/Cargo.lock
generated
130
rust/Cargo.lock
generated
@@ -137,12 +137,30 @@ version = "1.1.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "autocfg"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64"
|
||||||
|
version = "0.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.22.1"
|
version = "0.22.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "1.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.11.0"
|
version = "2.11.0"
|
||||||
@@ -180,6 +198,30 @@ dependencies = [
|
|||||||
"piper",
|
"piper",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "boringtun"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8dc4267b0c97985d9b089b19ff965b959e61870640d2f0842a97552e030fa43f"
|
||||||
|
dependencies = [
|
||||||
|
"aead",
|
||||||
|
"base64 0.13.1",
|
||||||
|
"blake2",
|
||||||
|
"chacha20poly1305",
|
||||||
|
"hex",
|
||||||
|
"hmac",
|
||||||
|
"ip_network",
|
||||||
|
"ip_network_table",
|
||||||
|
"libc",
|
||||||
|
"nix 0.25.1",
|
||||||
|
"parking_lot",
|
||||||
|
"rand_core 0.6.4",
|
||||||
|
"ring",
|
||||||
|
"tracing",
|
||||||
|
"untrusted",
|
||||||
|
"x25519-dalek",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.20.2"
|
version = "3.20.2"
|
||||||
@@ -655,6 +697,21 @@ version = "0.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hex"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hmac"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
||||||
|
dependencies = [
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
@@ -680,6 +737,28 @@ dependencies = [
|
|||||||
"generic-array",
|
"generic-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ip_network"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "aa2f047c0a98b2f299aa5d6d7088443570faae494e9ae1305e48be000c9e0eb1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ip_network_table"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4099b7cfc5c5e2fe8c5edf3f6f7adf7a714c9cc697534f63a5a5da30397cb2c0"
|
||||||
|
dependencies = [
|
||||||
|
"ip_network",
|
||||||
|
"ip_network_table-deps-treebitmap",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ip_network_table-deps-treebitmap"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8e537132deb99c0eb4b752f0346b6a836200eaaa3516dd7e5514b63930a09e5d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ipnet"
|
name = "ipnet"
|
||||||
version = "2.11.0"
|
version = "2.11.0"
|
||||||
@@ -824,13 +903,25 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nix"
|
||||||
|
version = "0.25.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nix"
|
name = "nix"
|
||||||
version = "0.30.1"
|
version = "0.30.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
|
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.11.0",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cfg_aliases",
|
"cfg_aliases",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -910,7 +1001,7 @@ version = "3.0.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be"
|
checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64 0.22.1",
|
||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1128,7 +1219,7 @@ version = "0.5.18"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.11.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1296,7 +1387,7 @@ version = "3.7.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.11.0",
|
||||||
"core-foundation",
|
"core-foundation",
|
||||||
"core-foundation-sys",
|
"core-foundation-sys",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -1433,7 +1524,8 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"base64",
|
"base64 0.22.1",
|
||||||
|
"boringtun",
|
||||||
"bytes",
|
"bytes",
|
||||||
"chacha20poly1305",
|
"chacha20poly1305",
|
||||||
"clap",
|
"clap",
|
||||||
@@ -1733,7 +1825,7 @@ dependencies = [
|
|||||||
"ipnet",
|
"ipnet",
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"nix",
|
"nix 0.30.1",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
@@ -2197,6 +2289,18 @@ version = "0.51.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "x25519-dalek"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277"
|
||||||
|
dependencies = [
|
||||||
|
"curve25519-dalek",
|
||||||
|
"rand_core 0.6.4",
|
||||||
|
"serde",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yasna"
|
name = "yasna"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
@@ -2231,6 +2335,20 @@ name = "zeroize"
|
|||||||
version = "1.8.2"
|
version = "1.8.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
|
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
|
||||||
|
dependencies = [
|
||||||
|
"zeroize_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zeroize_derive"
|
||||||
|
version = "1.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zmij"
|
name = "zmij"
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ rustls-pki-types = "1"
|
|||||||
rustls-pemfile = "2"
|
rustls-pemfile = "2"
|
||||||
webpki-roots = "1"
|
webpki-roots = "1"
|
||||||
mimalloc = "0.1"
|
mimalloc = "0.1"
|
||||||
|
boringtun = "0.7"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
|
|||||||
@@ -17,3 +17,4 @@ pub mod telemetry;
|
|||||||
pub mod ratelimit;
|
pub mod ratelimit;
|
||||||
pub mod qos;
|
pub mod qos;
|
||||||
pub mod mtu;
|
pub mod mtu;
|
||||||
|
pub mod wireguard;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use tracing::{info, error, warn};
|
|||||||
use crate::client::{ClientConfig, VpnClient};
|
use crate::client::{ClientConfig, VpnClient};
|
||||||
use crate::crypto;
|
use crate::crypto;
|
||||||
use crate::server::{ServerConfig, VpnServer};
|
use crate::server::{ServerConfig, VpnServer};
|
||||||
|
use crate::wireguard::{self, WgClient, WgClientConfig, WgPeerConfig, WgServer, WgServerConfig};
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// IPC protocol types
|
// IPC protocol types
|
||||||
@@ -93,6 +94,8 @@ pub async fn management_loop_stdio(mode: &str) -> Result<()> {
|
|||||||
|
|
||||||
let mut vpn_client = VpnClient::new();
|
let mut vpn_client = VpnClient::new();
|
||||||
let mut vpn_server = VpnServer::new();
|
let mut vpn_server = VpnServer::new();
|
||||||
|
let mut wg_client = WgClient::new();
|
||||||
|
let mut wg_server = WgServer::new();
|
||||||
|
|
||||||
send_event_stdout("ready", serde_json::json!({ "mode": mode }));
|
send_event_stdout("ready", serde_json::json!({ "mode": mode }));
|
||||||
|
|
||||||
@@ -127,8 +130,8 @@ pub async fn management_loop_stdio(mode: &str) -> Result<()> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let response = match mode {
|
let response = match mode {
|
||||||
"client" => handle_client_request(&request, &mut vpn_client).await,
|
"client" => handle_client_request(&request, &mut vpn_client, &mut wg_client).await,
|
||||||
"server" => handle_server_request(&request, &mut vpn_server).await,
|
"server" => handle_server_request(&request, &mut vpn_server, &mut wg_server).await,
|
||||||
_ => ManagementResponse::err(request.id.clone(), format!("Unknown mode: {}", mode)),
|
_ => ManagementResponse::err(request.id.clone(), format!("Unknown mode: {}", mode)),
|
||||||
};
|
};
|
||||||
send_response_stdout(&response);
|
send_response_stdout(&response);
|
||||||
@@ -150,6 +153,8 @@ pub async fn management_loop_socket(socket_path: &str, mode: &str) -> Result<()>
|
|||||||
// Shared state behind Mutex for socket mode (multiple connections)
|
// Shared state behind Mutex for socket mode (multiple connections)
|
||||||
let vpn_client = std::sync::Arc::new(Mutex::new(VpnClient::new()));
|
let vpn_client = std::sync::Arc::new(Mutex::new(VpnClient::new()));
|
||||||
let vpn_server = std::sync::Arc::new(Mutex::new(VpnServer::new()));
|
let vpn_server = std::sync::Arc::new(Mutex::new(VpnServer::new()));
|
||||||
|
let wg_client = std::sync::Arc::new(Mutex::new(WgClient::new()));
|
||||||
|
let wg_server = std::sync::Arc::new(Mutex::new(WgServer::new()));
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match listener.accept().await {
|
match listener.accept().await {
|
||||||
@@ -157,9 +162,11 @@ pub async fn management_loop_socket(socket_path: &str, mode: &str) -> Result<()>
|
|||||||
let mode = mode.to_string();
|
let mode = mode.to_string();
|
||||||
let client = vpn_client.clone();
|
let client = vpn_client.clone();
|
||||||
let server = vpn_server.clone();
|
let server = vpn_server.clone();
|
||||||
|
let wg_c = wg_client.clone();
|
||||||
|
let wg_s = wg_server.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
if let Err(e) =
|
if let Err(e) =
|
||||||
handle_socket_connection(stream, &mode, client, server).await
|
handle_socket_connection(stream, &mode, client, server, wg_c, wg_s).await
|
||||||
{
|
{
|
||||||
warn!("Socket connection error: {}", e);
|
warn!("Socket connection error: {}", e);
|
||||||
}
|
}
|
||||||
@@ -177,6 +184,8 @@ async fn handle_socket_connection(
|
|||||||
mode: &str,
|
mode: &str,
|
||||||
vpn_client: std::sync::Arc<Mutex<VpnClient>>,
|
vpn_client: std::sync::Arc<Mutex<VpnClient>>,
|
||||||
vpn_server: std::sync::Arc<Mutex<VpnServer>>,
|
vpn_server: std::sync::Arc<Mutex<VpnServer>>,
|
||||||
|
wg_client: std::sync::Arc<Mutex<WgClient>>,
|
||||||
|
wg_server: std::sync::Arc<Mutex<WgServer>>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let (reader, mut writer) = stream.into_split();
|
let (reader, mut writer) = stream.into_split();
|
||||||
let buf_reader = BufReader::new(reader);
|
let buf_reader = BufReader::new(reader);
|
||||||
@@ -227,11 +236,13 @@ async fn handle_socket_connection(
|
|||||||
let response = match mode {
|
let response = match mode {
|
||||||
"client" => {
|
"client" => {
|
||||||
let mut client = vpn_client.lock().await;
|
let mut client = vpn_client.lock().await;
|
||||||
handle_client_request(&request, &mut client).await
|
let mut wg_c = wg_client.lock().await;
|
||||||
|
handle_client_request(&request, &mut client, &mut wg_c).await
|
||||||
}
|
}
|
||||||
"server" => {
|
"server" => {
|
||||||
let mut server = vpn_server.lock().await;
|
let mut server = vpn_server.lock().await;
|
||||||
handle_server_request(&request, &mut server).await
|
let mut wg_s = wg_server.lock().await;
|
||||||
|
handle_server_request(&request, &mut server, &mut wg_s).await
|
||||||
}
|
}
|
||||||
_ => ManagementResponse::err(request.id.clone(), format!("Unknown mode: {}", mode)),
|
_ => ManagementResponse::err(request.id.clone(), format!("Unknown mode: {}", mode)),
|
||||||
};
|
};
|
||||||
@@ -252,38 +263,79 @@ async fn handle_socket_connection(
|
|||||||
async fn handle_client_request(
|
async fn handle_client_request(
|
||||||
request: &ManagementRequest,
|
request: &ManagementRequest,
|
||||||
vpn_client: &mut VpnClient,
|
vpn_client: &mut VpnClient,
|
||||||
|
wg_client: &mut WgClient,
|
||||||
) -> ManagementResponse {
|
) -> ManagementResponse {
|
||||||
let id = request.id.clone();
|
let id = request.id.clone();
|
||||||
|
|
||||||
match request.method.as_str() {
|
match request.method.as_str() {
|
||||||
"connect" => {
|
"connect" => {
|
||||||
let config: ClientConfig = match serde_json::from_value(
|
// Check if transport is "wireguard"
|
||||||
request.params.get("config").cloned().unwrap_or_default(),
|
let transport = request.params
|
||||||
) {
|
.get("config")
|
||||||
Ok(c) => c,
|
.and_then(|c| c.get("transport"))
|
||||||
Err(e) => {
|
.and_then(|t| t.as_str())
|
||||||
return ManagementResponse::err(id, format!("Invalid config: {}", e));
|
.unwrap_or("");
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
match vpn_client.connect(config).await {
|
if transport == "wireguard" {
|
||||||
Ok(assigned_ip) => {
|
let config: WgClientConfig = match serde_json::from_value(
|
||||||
ManagementResponse::ok(id, serde_json::json!({ "assignedIp": assigned_ip }))
|
request.params.get("config").cloned().unwrap_or_default(),
|
||||||
|
) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => {
|
||||||
|
return ManagementResponse::err(id, format!("Invalid WG config: {}", e));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match wg_client.connect(config).await {
|
||||||
|
Ok(assigned_ip) => {
|
||||||
|
ManagementResponse::ok(id, serde_json::json!({ "assignedIp": assigned_ip }))
|
||||||
|
}
|
||||||
|
Err(e) => ManagementResponse::err(id, format!("WG connect failed: {}", e)),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let config: ClientConfig = match serde_json::from_value(
|
||||||
|
request.params.get("config").cloned().unwrap_or_default(),
|
||||||
|
) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => {
|
||||||
|
return ManagementResponse::err(id, format!("Invalid config: {}", e));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match vpn_client.connect(config).await {
|
||||||
|
Ok(assigned_ip) => {
|
||||||
|
ManagementResponse::ok(id, serde_json::json!({ "assignedIp": assigned_ip }))
|
||||||
|
}
|
||||||
|
Err(e) => ManagementResponse::err(id, format!("Connect failed: {}", e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"disconnect" => {
|
||||||
|
if wg_client.is_running() {
|
||||||
|
match wg_client.disconnect().await {
|
||||||
|
Ok(()) => ManagementResponse::ok(id, serde_json::json!({})),
|
||||||
|
Err(e) => ManagementResponse::err(id, format!("WG disconnect failed: {}", e)),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match vpn_client.disconnect().await {
|
||||||
|
Ok(()) => ManagementResponse::ok(id, serde_json::json!({})),
|
||||||
|
Err(e) => ManagementResponse::err(id, format!("Disconnect failed: {}", e)),
|
||||||
}
|
}
|
||||||
Err(e) => ManagementResponse::err(id, format!("Connect failed: {}", e)),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"disconnect" => match vpn_client.disconnect().await {
|
|
||||||
Ok(()) => ManagementResponse::ok(id, serde_json::json!({})),
|
|
||||||
Err(e) => ManagementResponse::err(id, format!("Disconnect failed: {}", e)),
|
|
||||||
},
|
|
||||||
"getStatus" => {
|
"getStatus" => {
|
||||||
let status = vpn_client.get_status().await;
|
if wg_client.is_running() {
|
||||||
ManagementResponse::ok(id, status)
|
ManagementResponse::ok(id, wg_client.get_status().await)
|
||||||
|
} else {
|
||||||
|
let status = vpn_client.get_status().await;
|
||||||
|
ManagementResponse::ok(id, status)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
"getStatistics" => {
|
"getStatistics" => {
|
||||||
let stats = vpn_client.get_statistics().await;
|
if wg_client.is_running() {
|
||||||
ManagementResponse::ok(id, stats)
|
ManagementResponse::ok(id, wg_client.get_statistics().await)
|
||||||
|
} else {
|
||||||
|
let stats = vpn_client.get_statistics().await;
|
||||||
|
ManagementResponse::ok(id, stats)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
"getConnectionQuality" => {
|
"getConnectionQuality" => {
|
||||||
match vpn_client.get_connection_quality() {
|
match vpn_client.get_connection_quality() {
|
||||||
@@ -329,45 +381,92 @@ async fn handle_client_request(
|
|||||||
async fn handle_server_request(
|
async fn handle_server_request(
|
||||||
request: &ManagementRequest,
|
request: &ManagementRequest,
|
||||||
vpn_server: &mut VpnServer,
|
vpn_server: &mut VpnServer,
|
||||||
|
wg_server: &mut WgServer,
|
||||||
) -> ManagementResponse {
|
) -> ManagementResponse {
|
||||||
let id = request.id.clone();
|
let id = request.id.clone();
|
||||||
|
|
||||||
match request.method.as_str() {
|
match request.method.as_str() {
|
||||||
"start" => {
|
"start" => {
|
||||||
let config: ServerConfig = match serde_json::from_value(
|
// Check if transportMode is "wireguard"
|
||||||
request.params.get("config").cloned().unwrap_or_default(),
|
let transport_mode = request.params
|
||||||
) {
|
.get("config")
|
||||||
Ok(c) => c,
|
.and_then(|c| c.get("transportMode"))
|
||||||
Err(e) => {
|
.and_then(|t| t.as_str())
|
||||||
return ManagementResponse::err(id, format!("Invalid config: {}", e));
|
.unwrap_or("");
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
match vpn_server.start(config).await {
|
if transport_mode == "wireguard" {
|
||||||
Ok(()) => ManagementResponse::ok(id, serde_json::json!({})),
|
let config: WgServerConfig = match serde_json::from_value(
|
||||||
Err(e) => ManagementResponse::err(id, format!("Start failed: {}", e)),
|
request.params.get("config").cloned().unwrap_or_default(),
|
||||||
|
) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => {
|
||||||
|
return ManagementResponse::err(id, format!("Invalid WG config: {}", e));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match wg_server.start(config).await {
|
||||||
|
Ok(()) => ManagementResponse::ok(id, serde_json::json!({})),
|
||||||
|
Err(e) => ManagementResponse::err(id, format!("WG start failed: {}", e)),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let config: ServerConfig = match serde_json::from_value(
|
||||||
|
request.params.get("config").cloned().unwrap_or_default(),
|
||||||
|
) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => {
|
||||||
|
return ManagementResponse::err(id, format!("Invalid config: {}", e));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match vpn_server.start(config).await {
|
||||||
|
Ok(()) => ManagementResponse::ok(id, serde_json::json!({})),
|
||||||
|
Err(e) => ManagementResponse::err(id, format!("Start failed: {}", e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"stop" => {
|
||||||
|
if wg_server.is_running() {
|
||||||
|
match wg_server.stop().await {
|
||||||
|
Ok(()) => ManagementResponse::ok(id, serde_json::json!({})),
|
||||||
|
Err(e) => ManagementResponse::err(id, format!("WG stop failed: {}", e)),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match vpn_server.stop().await {
|
||||||
|
Ok(()) => ManagementResponse::ok(id, serde_json::json!({})),
|
||||||
|
Err(e) => ManagementResponse::err(id, format!("Stop failed: {}", e)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"stop" => match vpn_server.stop().await {
|
|
||||||
Ok(()) => ManagementResponse::ok(id, serde_json::json!({})),
|
|
||||||
Err(e) => ManagementResponse::err(id, format!("Stop failed: {}", e)),
|
|
||||||
},
|
|
||||||
"getStatus" => {
|
"getStatus" => {
|
||||||
let status = vpn_server.get_status();
|
if wg_server.is_running() {
|
||||||
ManagementResponse::ok(id, status)
|
ManagementResponse::ok(id, wg_server.get_status())
|
||||||
|
} else {
|
||||||
|
let status = vpn_server.get_status();
|
||||||
|
ManagementResponse::ok(id, status)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
"getStatistics" => {
|
"getStatistics" => {
|
||||||
let stats = vpn_server.get_statistics().await;
|
if wg_server.is_running() {
|
||||||
match serde_json::to_value(&stats) {
|
ManagementResponse::ok(id, wg_server.get_statistics().await)
|
||||||
Ok(v) => ManagementResponse::ok(id, v),
|
} else {
|
||||||
Err(e) => ManagementResponse::err(id, format!("Serialize error: {}", e)),
|
let stats = vpn_server.get_statistics().await;
|
||||||
|
match serde_json::to_value(&stats) {
|
||||||
|
Ok(v) => ManagementResponse::ok(id, v),
|
||||||
|
Err(e) => ManagementResponse::err(id, format!("Serialize error: {}", e)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"listClients" => {
|
"listClients" => {
|
||||||
let clients = vpn_server.list_clients().await;
|
if wg_server.is_running() {
|
||||||
match serde_json::to_value(&clients) {
|
let peers = wg_server.list_peers().await;
|
||||||
Ok(v) => ManagementResponse::ok(id, serde_json::json!({ "clients": v })),
|
match serde_json::to_value(&peers) {
|
||||||
Err(e) => ManagementResponse::err(id, format!("Serialize error: {}", e)),
|
Ok(v) => ManagementResponse::ok(id, serde_json::json!({ "clients": v })),
|
||||||
|
Err(e) => ManagementResponse::err(id, format!("Serialize error: {}", e)),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let clients = vpn_server.list_clients().await;
|
||||||
|
match serde_json::to_value(&clients) {
|
||||||
|
Ok(v) => ManagementResponse::ok(id, serde_json::json!({ "clients": v })),
|
||||||
|
Err(e) => ManagementResponse::err(id, format!("Serialize error: {}", e)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"disconnectClient" => {
|
"disconnectClient" => {
|
||||||
@@ -436,6 +535,56 @@ async fn handle_server_request(
|
|||||||
),
|
),
|
||||||
Err(e) => ManagementResponse::err(id, format!("Keypair generation failed: {}", e)),
|
Err(e) => ManagementResponse::err(id, format!("Keypair generation failed: {}", e)),
|
||||||
},
|
},
|
||||||
|
"generateWgKeypair" => {
|
||||||
|
let (public_key, private_key) = wireguard::generate_wg_keypair();
|
||||||
|
ManagementResponse::ok(
|
||||||
|
id,
|
||||||
|
serde_json::json!({
|
||||||
|
"publicKey": public_key,
|
||||||
|
"privateKey": private_key,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
"addWgPeer" => {
|
||||||
|
if !wg_server.is_running() {
|
||||||
|
return ManagementResponse::err(id, "WireGuard server not running".to_string());
|
||||||
|
}
|
||||||
|
let config: WgPeerConfig = match serde_json::from_value(
|
||||||
|
request.params.get("peer").cloned().unwrap_or_default(),
|
||||||
|
) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => {
|
||||||
|
return ManagementResponse::err(id, format!("Invalid peer config: {}", e));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match wg_server.add_peer(config).await {
|
||||||
|
Ok(()) => ManagementResponse::ok(id, serde_json::json!({})),
|
||||||
|
Err(e) => ManagementResponse::err(id, format!("Add peer failed: {}", e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"removeWgPeer" => {
|
||||||
|
if !wg_server.is_running() {
|
||||||
|
return ManagementResponse::err(id, "WireGuard server not running".to_string());
|
||||||
|
}
|
||||||
|
let public_key = match request.params.get("publicKey").and_then(|v| v.as_str()) {
|
||||||
|
Some(k) => k.to_string(),
|
||||||
|
None => return ManagementResponse::err(id, "Missing publicKey".to_string()),
|
||||||
|
};
|
||||||
|
match wg_server.remove_peer(&public_key).await {
|
||||||
|
Ok(()) => ManagementResponse::ok(id, serde_json::json!({})),
|
||||||
|
Err(e) => ManagementResponse::err(id, format!("Remove peer failed: {}", e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"listWgPeers" => {
|
||||||
|
if !wg_server.is_running() {
|
||||||
|
return ManagementResponse::err(id, "WireGuard server not running".to_string());
|
||||||
|
}
|
||||||
|
let peers = wg_server.list_peers().await;
|
||||||
|
match serde_json::to_value(&peers) {
|
||||||
|
Ok(v) => ManagementResponse::ok(id, serde_json::json!({ "peers": v })),
|
||||||
|
Err(e) => ManagementResponse::err(id, format!("Serialize error: {}", e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => ManagementResponse::err(id, format!("Unknown server method: {}", request.method)),
|
_ => ManagementResponse::err(id, format!("Unknown server method: {}", request.method)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1329
rust/src/wireguard.rs
Normal file
1329
rust/src/wireguard.rs
Normal file
File diff suppressed because it is too large
Load Diff
353
test/test.wireguard.node.ts
Normal file
353
test/test.wireguard.node.ts
Normal file
@@ -0,0 +1,353 @@
|
|||||||
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||||
|
import {
|
||||||
|
VpnConfig,
|
||||||
|
VpnServer,
|
||||||
|
WgConfigGenerator,
|
||||||
|
} from '../ts/index.js';
|
||||||
|
import type {
|
||||||
|
IVpnClientConfig,
|
||||||
|
IVpnServerConfig,
|
||||||
|
IVpnServerOptions,
|
||||||
|
IWgPeerConfig,
|
||||||
|
} from '../ts/index.js';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// WireGuard config validation — client
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// A valid 32-byte key in base64 (44 chars)
|
||||||
|
const VALID_KEY = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=';
|
||||||
|
const VALID_KEY_2 = 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=';
|
||||||
|
|
||||||
|
tap.test('WG client config: valid wireguard config passes validation', async () => {
|
||||||
|
const config: IVpnClientConfig = {
|
||||||
|
serverUrl: '', // not needed for WG
|
||||||
|
serverPublicKey: VALID_KEY,
|
||||||
|
transport: 'wireguard',
|
||||||
|
wgPrivateKey: VALID_KEY_2,
|
||||||
|
wgAddress: '10.8.0.2',
|
||||||
|
wgEndpoint: 'vpn.example.com:51820',
|
||||||
|
wgAllowedIps: ['0.0.0.0/0'],
|
||||||
|
};
|
||||||
|
VpnConfig.validateClientConfig(config);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('WG client config: rejects missing wgPrivateKey', async () => {
|
||||||
|
const config: IVpnClientConfig = {
|
||||||
|
serverUrl: '',
|
||||||
|
serverPublicKey: VALID_KEY,
|
||||||
|
transport: 'wireguard',
|
||||||
|
wgAddress: '10.8.0.2',
|
||||||
|
wgEndpoint: 'vpn.example.com:51820',
|
||||||
|
};
|
||||||
|
let threw = false;
|
||||||
|
try {
|
||||||
|
VpnConfig.validateClientConfig(config);
|
||||||
|
} catch (e) {
|
||||||
|
threw = true;
|
||||||
|
expect((e as Error).message).toContain('wgPrivateKey');
|
||||||
|
}
|
||||||
|
expect(threw).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('WG client config: rejects missing wgAddress', async () => {
|
||||||
|
const config: IVpnClientConfig = {
|
||||||
|
serverUrl: '',
|
||||||
|
serverPublicKey: VALID_KEY,
|
||||||
|
transport: 'wireguard',
|
||||||
|
wgPrivateKey: VALID_KEY_2,
|
||||||
|
wgEndpoint: 'vpn.example.com:51820',
|
||||||
|
};
|
||||||
|
let threw = false;
|
||||||
|
try {
|
||||||
|
VpnConfig.validateClientConfig(config);
|
||||||
|
} catch (e) {
|
||||||
|
threw = true;
|
||||||
|
expect((e as Error).message).toContain('wgAddress');
|
||||||
|
}
|
||||||
|
expect(threw).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('WG client config: rejects missing wgEndpoint', async () => {
|
||||||
|
const config: IVpnClientConfig = {
|
||||||
|
serverUrl: '',
|
||||||
|
serverPublicKey: VALID_KEY,
|
||||||
|
transport: 'wireguard',
|
||||||
|
wgPrivateKey: VALID_KEY_2,
|
||||||
|
wgAddress: '10.8.0.2',
|
||||||
|
};
|
||||||
|
let threw = false;
|
||||||
|
try {
|
||||||
|
VpnConfig.validateClientConfig(config);
|
||||||
|
} catch (e) {
|
||||||
|
threw = true;
|
||||||
|
expect((e as Error).message).toContain('wgEndpoint');
|
||||||
|
}
|
||||||
|
expect(threw).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('WG client config: rejects invalid key length', async () => {
|
||||||
|
const config: IVpnClientConfig = {
|
||||||
|
serverUrl: '',
|
||||||
|
serverPublicKey: VALID_KEY,
|
||||||
|
transport: 'wireguard',
|
||||||
|
wgPrivateKey: 'tooshort',
|
||||||
|
wgAddress: '10.8.0.2',
|
||||||
|
wgEndpoint: 'vpn.example.com:51820',
|
||||||
|
};
|
||||||
|
let threw = false;
|
||||||
|
try {
|
||||||
|
VpnConfig.validateClientConfig(config);
|
||||||
|
} catch (e) {
|
||||||
|
threw = true;
|
||||||
|
expect((e as Error).message).toContain('44 characters');
|
||||||
|
}
|
||||||
|
expect(threw).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('WG client config: rejects invalid CIDR in allowedIps', async () => {
|
||||||
|
const config: IVpnClientConfig = {
|
||||||
|
serverUrl: '',
|
||||||
|
serverPublicKey: VALID_KEY,
|
||||||
|
transport: 'wireguard',
|
||||||
|
wgPrivateKey: VALID_KEY_2,
|
||||||
|
wgAddress: '10.8.0.2',
|
||||||
|
wgEndpoint: 'vpn.example.com:51820',
|
||||||
|
wgAllowedIps: ['not-a-cidr'],
|
||||||
|
};
|
||||||
|
let threw = false;
|
||||||
|
try {
|
||||||
|
VpnConfig.validateClientConfig(config);
|
||||||
|
} catch (e) {
|
||||||
|
threw = true;
|
||||||
|
expect((e as Error).message).toContain('CIDR');
|
||||||
|
}
|
||||||
|
expect(threw).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// WireGuard config validation — server
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
tap.test('WG server config: valid config passes validation', async () => {
|
||||||
|
const config: IVpnServerConfig = {
|
||||||
|
listenAddr: '',
|
||||||
|
privateKey: VALID_KEY,
|
||||||
|
publicKey: VALID_KEY_2,
|
||||||
|
subnet: '10.8.0.0/24',
|
||||||
|
transportMode: 'wireguard',
|
||||||
|
wgPeers: [
|
||||||
|
{
|
||||||
|
publicKey: VALID_KEY_2,
|
||||||
|
allowedIps: ['10.8.0.2/32'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
VpnConfig.validateServerConfig(config);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('WG server config: rejects empty wgPeers', async () => {
|
||||||
|
const config: IVpnServerConfig = {
|
||||||
|
listenAddr: '',
|
||||||
|
privateKey: VALID_KEY,
|
||||||
|
publicKey: VALID_KEY_2,
|
||||||
|
subnet: '10.8.0.0/24',
|
||||||
|
transportMode: 'wireguard',
|
||||||
|
wgPeers: [],
|
||||||
|
};
|
||||||
|
let threw = false;
|
||||||
|
try {
|
||||||
|
VpnConfig.validateServerConfig(config);
|
||||||
|
} catch (e) {
|
||||||
|
threw = true;
|
||||||
|
expect((e as Error).message).toContain('wgPeers');
|
||||||
|
}
|
||||||
|
expect(threw).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('WG server config: rejects peer without publicKey', async () => {
|
||||||
|
const config: IVpnServerConfig = {
|
||||||
|
listenAddr: '',
|
||||||
|
privateKey: VALID_KEY,
|
||||||
|
publicKey: VALID_KEY_2,
|
||||||
|
subnet: '10.8.0.0/24',
|
||||||
|
transportMode: 'wireguard',
|
||||||
|
wgPeers: [
|
||||||
|
{
|
||||||
|
publicKey: '',
|
||||||
|
allowedIps: ['10.8.0.2/32'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
let threw = false;
|
||||||
|
try {
|
||||||
|
VpnConfig.validateServerConfig(config);
|
||||||
|
} catch (e) {
|
||||||
|
threw = true;
|
||||||
|
expect((e as Error).message).toContain('publicKey');
|
||||||
|
}
|
||||||
|
expect(threw).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('WG server config: rejects invalid wgListenPort', async () => {
|
||||||
|
const config: IVpnServerConfig = {
|
||||||
|
listenAddr: '',
|
||||||
|
privateKey: VALID_KEY,
|
||||||
|
publicKey: VALID_KEY_2,
|
||||||
|
subnet: '10.8.0.0/24',
|
||||||
|
transportMode: 'wireguard',
|
||||||
|
wgListenPort: 0,
|
||||||
|
wgPeers: [
|
||||||
|
{
|
||||||
|
publicKey: VALID_KEY_2,
|
||||||
|
allowedIps: ['10.8.0.2/32'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
let threw = false;
|
||||||
|
try {
|
||||||
|
VpnConfig.validateServerConfig(config);
|
||||||
|
} catch (e) {
|
||||||
|
threw = true;
|
||||||
|
expect((e as Error).message).toContain('wgListenPort');
|
||||||
|
}
|
||||||
|
expect(threw).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// WireGuard keypair generation via daemon
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
let server: VpnServer;
|
||||||
|
|
||||||
|
tap.test('WG: spawn server daemon for keypair generation', async () => {
|
||||||
|
const options: IVpnServerOptions = {
|
||||||
|
transport: { transport: 'stdio' },
|
||||||
|
};
|
||||||
|
server = new VpnServer(options);
|
||||||
|
const started = await server['bridge'].start();
|
||||||
|
expect(started).toBeTrue();
|
||||||
|
expect(server.running).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('WG: generateWgKeypair returns valid keypair', async () => {
|
||||||
|
const keypair = await server.generateWgKeypair();
|
||||||
|
expect(keypair.publicKey).toBeTypeofString();
|
||||||
|
expect(keypair.privateKey).toBeTypeofString();
|
||||||
|
// WireGuard keys: base64 of 32 bytes = 44 characters
|
||||||
|
expect(keypair.publicKey.length).toEqual(44);
|
||||||
|
expect(keypair.privateKey.length).toEqual(44);
|
||||||
|
// Verify they decode to 32 bytes
|
||||||
|
const pubBuf = Buffer.from(keypair.publicKey, 'base64');
|
||||||
|
const privBuf = Buffer.from(keypair.privateKey, 'base64');
|
||||||
|
expect(pubBuf.length).toEqual(32);
|
||||||
|
expect(privBuf.length).toEqual(32);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('WG: generateWgKeypair returns unique keys each time', async () => {
|
||||||
|
const kp1 = await server.generateWgKeypair();
|
||||||
|
const kp2 = await server.generateWgKeypair();
|
||||||
|
expect(kp1.publicKey).not.toEqual(kp2.publicKey);
|
||||||
|
expect(kp1.privateKey).not.toEqual(kp2.privateKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('WG: stop server daemon', async () => {
|
||||||
|
server.stop();
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
|
expect(server.running).toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// WireGuard config file generation
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
tap.test('WgConfigGenerator: generate client config', async () => {
|
||||||
|
const conf = WgConfigGenerator.generateClientConfig({
|
||||||
|
privateKey: 'clientPrivateKeyBase64====================',
|
||||||
|
address: '10.8.0.2/24',
|
||||||
|
dns: ['1.1.1.1', '8.8.8.8'],
|
||||||
|
mtu: 1420,
|
||||||
|
peer: {
|
||||||
|
publicKey: 'serverPublicKeyBase64====================',
|
||||||
|
endpoint: 'vpn.example.com:51820',
|
||||||
|
allowedIps: ['0.0.0.0/0', '::/0'],
|
||||||
|
persistentKeepalive: 25,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(conf).toContain('[Interface]');
|
||||||
|
expect(conf).toContain('PrivateKey = clientPrivateKeyBase64====================');
|
||||||
|
expect(conf).toContain('Address = 10.8.0.2/24');
|
||||||
|
expect(conf).toContain('DNS = 1.1.1.1, 8.8.8.8');
|
||||||
|
expect(conf).toContain('MTU = 1420');
|
||||||
|
expect(conf).toContain('[Peer]');
|
||||||
|
expect(conf).toContain('PublicKey = serverPublicKeyBase64====================');
|
||||||
|
expect(conf).toContain('Endpoint = vpn.example.com:51820');
|
||||||
|
expect(conf).toContain('AllowedIPs = 0.0.0.0/0, ::/0');
|
||||||
|
expect(conf).toContain('PersistentKeepalive = 25');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('WgConfigGenerator: generate client config without optional fields', async () => {
|
||||||
|
const conf = WgConfigGenerator.generateClientConfig({
|
||||||
|
privateKey: 'key1',
|
||||||
|
address: '10.0.0.2/32',
|
||||||
|
peer: {
|
||||||
|
publicKey: 'key2',
|
||||||
|
endpoint: 'server:51820',
|
||||||
|
allowedIps: ['10.0.0.0/24'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(conf).toContain('[Interface]');
|
||||||
|
expect(conf).not.toContain('DNS');
|
||||||
|
expect(conf).not.toContain('MTU');
|
||||||
|
expect(conf).not.toContain('PresharedKey');
|
||||||
|
expect(conf).not.toContain('PersistentKeepalive');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('WgConfigGenerator: generate server config with NAT', async () => {
|
||||||
|
const conf = WgConfigGenerator.generateServerConfig({
|
||||||
|
privateKey: 'serverPrivKey',
|
||||||
|
address: '10.8.0.1/24',
|
||||||
|
listenPort: 51820,
|
||||||
|
dns: ['1.1.1.1'],
|
||||||
|
enableNat: true,
|
||||||
|
natInterface: 'ens3',
|
||||||
|
peers: [
|
||||||
|
{
|
||||||
|
publicKey: 'peer1PubKey',
|
||||||
|
allowedIps: ['10.8.0.2/32'],
|
||||||
|
presharedKey: 'psk1',
|
||||||
|
persistentKeepalive: 25,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
publicKey: 'peer2PubKey',
|
||||||
|
allowedIps: ['10.8.0.3/32'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(conf).toContain('[Interface]');
|
||||||
|
expect(conf).toContain('ListenPort = 51820');
|
||||||
|
expect(conf).toContain('PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -t nat -A POSTROUTING -o ens3 -j MASQUERADE');
|
||||||
|
expect(conf).toContain('PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -t nat -D POSTROUTING -o ens3 -j MASQUERADE');
|
||||||
|
// Two [Peer] sections
|
||||||
|
const peerCount = (conf.match(/\[Peer\]/g) || []).length;
|
||||||
|
expect(peerCount).toEqual(2);
|
||||||
|
expect(conf).toContain('PresharedKey = psk1');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('WgConfigGenerator: generate server config without NAT', async () => {
|
||||||
|
const conf = WgConfigGenerator.generateServerConfig({
|
||||||
|
privateKey: 'serverPrivKey',
|
||||||
|
address: '10.8.0.1/24',
|
||||||
|
listenPort: 51820,
|
||||||
|
peers: [
|
||||||
|
{
|
||||||
|
publicKey: 'peerKey',
|
||||||
|
allowedIps: ['10.8.0.2/32'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(conf).not.toContain('PostUp');
|
||||||
|
expect(conf).not.toContain('PostDown');
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartvpn',
|
name: '@push.rocks/smartvpn',
|
||||||
version: '1.4.1',
|
version: '1.5.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'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ export { VpnClient } from './smartvpn.classes.vpnclient.js';
|
|||||||
export { VpnServer } from './smartvpn.classes.vpnserver.js';
|
export { VpnServer } from './smartvpn.classes.vpnserver.js';
|
||||||
export { VpnConfig } from './smartvpn.classes.vpnconfig.js';
|
export { VpnConfig } from './smartvpn.classes.vpnconfig.js';
|
||||||
export { VpnInstaller } from './smartvpn.classes.vpninstaller.js';
|
export { VpnInstaller } from './smartvpn.classes.vpninstaller.js';
|
||||||
|
export { WgConfigGenerator } from './smartvpn.classes.wgconfig.js';
|
||||||
|
|||||||
@@ -12,17 +12,45 @@ export class VpnConfig {
|
|||||||
* Validate a client config object. Throws on invalid config.
|
* Validate a client config object. Throws on invalid config.
|
||||||
*/
|
*/
|
||||||
public static validateClientConfig(config: IVpnClientConfig): void {
|
public static validateClientConfig(config: IVpnClientConfig): void {
|
||||||
if (!config.serverUrl) {
|
if (config.transport === 'wireguard') {
|
||||||
throw new Error('VpnConfig: serverUrl is required');
|
// WireGuard-specific validation
|
||||||
}
|
if (!config.wgPrivateKey) {
|
||||||
// For QUIC-only transport, serverUrl is a host:port address; for WebSocket/auto it must be ws:// or wss://
|
throw new Error('VpnConfig: wgPrivateKey is required for WireGuard transport');
|
||||||
if (config.transport !== 'quic') {
|
}
|
||||||
if (!config.serverUrl.startsWith('wss://') && !config.serverUrl.startsWith('ws://')) {
|
VpnConfig.validateBase64Key(config.wgPrivateKey, 'wgPrivateKey');
|
||||||
throw new Error('VpnConfig: serverUrl must start with wss:// or ws:// (for WebSocket transport)');
|
if (!config.wgAddress) {
|
||||||
|
throw new Error('VpnConfig: wgAddress is required for WireGuard transport');
|
||||||
|
}
|
||||||
|
if (!config.serverPublicKey) {
|
||||||
|
throw new Error('VpnConfig: serverPublicKey is required for WireGuard transport');
|
||||||
|
}
|
||||||
|
VpnConfig.validateBase64Key(config.serverPublicKey, 'serverPublicKey');
|
||||||
|
if (!config.wgEndpoint) {
|
||||||
|
throw new Error('VpnConfig: wgEndpoint is required for WireGuard transport');
|
||||||
|
}
|
||||||
|
if (config.wgPresharedKey) {
|
||||||
|
VpnConfig.validateBase64Key(config.wgPresharedKey, 'wgPresharedKey');
|
||||||
|
}
|
||||||
|
if (config.wgAllowedIps) {
|
||||||
|
for (const cidr of config.wgAllowedIps) {
|
||||||
|
if (!VpnConfig.isValidCidr(cidr)) {
|
||||||
|
throw new Error(`VpnConfig: invalid allowedIp CIDR: ${cidr}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!config.serverUrl) {
|
||||||
|
throw new Error('VpnConfig: serverUrl is required');
|
||||||
|
}
|
||||||
|
// For QUIC-only transport, serverUrl is a host:port address; for WebSocket/auto it must be ws:// or wss://
|
||||||
|
if (config.transport !== 'quic') {
|
||||||
|
if (!config.serverUrl.startsWith('wss://') && !config.serverUrl.startsWith('ws://')) {
|
||||||
|
throw new Error('VpnConfig: serverUrl must start with wss:// or ws:// (for WebSocket transport)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!config.serverPublicKey) {
|
||||||
|
throw new Error('VpnConfig: serverPublicKey is required');
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (!config.serverPublicKey) {
|
|
||||||
throw new Error('VpnConfig: serverPublicKey is required');
|
|
||||||
}
|
}
|
||||||
if (config.mtu !== undefined && (config.mtu < 576 || config.mtu > 65535)) {
|
if (config.mtu !== undefined && (config.mtu < 576 || config.mtu > 65535)) {
|
||||||
throw new Error('VpnConfig: mtu must be between 576 and 65535');
|
throw new Error('VpnConfig: mtu must be between 576 and 65535');
|
||||||
@@ -43,20 +71,51 @@ export class VpnConfig {
|
|||||||
* Validate a server config object. Throws on invalid config.
|
* Validate a server config object. Throws on invalid config.
|
||||||
*/
|
*/
|
||||||
public static validateServerConfig(config: IVpnServerConfig): void {
|
public static validateServerConfig(config: IVpnServerConfig): void {
|
||||||
if (!config.listenAddr) {
|
if (config.transportMode === 'wireguard') {
|
||||||
throw new Error('VpnConfig: listenAddr is required');
|
// WireGuard server validation
|
||||||
}
|
if (!config.privateKey) {
|
||||||
if (!config.privateKey) {
|
throw new Error('VpnConfig: privateKey is required');
|
||||||
throw new Error('VpnConfig: privateKey is required');
|
}
|
||||||
}
|
VpnConfig.validateBase64Key(config.privateKey, 'privateKey');
|
||||||
if (!config.publicKey) {
|
if (!config.wgPeers || config.wgPeers.length === 0) {
|
||||||
throw new Error('VpnConfig: publicKey is required');
|
throw new Error('VpnConfig: at least one wgPeers entry is required for WireGuard mode');
|
||||||
}
|
}
|
||||||
if (!config.subnet) {
|
for (const peer of config.wgPeers) {
|
||||||
throw new Error('VpnConfig: subnet is required');
|
if (!peer.publicKey) {
|
||||||
}
|
throw new Error('VpnConfig: peer publicKey is required');
|
||||||
if (!VpnConfig.isValidSubnet(config.subnet)) {
|
}
|
||||||
throw new Error(`VpnConfig: invalid subnet: ${config.subnet}`);
|
VpnConfig.validateBase64Key(peer.publicKey, 'peer.publicKey');
|
||||||
|
if (!peer.allowedIps || peer.allowedIps.length === 0) {
|
||||||
|
throw new Error('VpnConfig: peer allowedIps is required');
|
||||||
|
}
|
||||||
|
for (const cidr of peer.allowedIps) {
|
||||||
|
if (!VpnConfig.isValidCidr(cidr)) {
|
||||||
|
throw new Error(`VpnConfig: invalid peer allowedIp CIDR: ${cidr}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (peer.presharedKey) {
|
||||||
|
VpnConfig.validateBase64Key(peer.presharedKey, 'peer.presharedKey');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (config.wgListenPort !== undefined && (config.wgListenPort < 1 || config.wgListenPort > 65535)) {
|
||||||
|
throw new Error('VpnConfig: wgListenPort must be between 1 and 65535');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!config.listenAddr) {
|
||||||
|
throw new Error('VpnConfig: listenAddr is required');
|
||||||
|
}
|
||||||
|
if (!config.privateKey) {
|
||||||
|
throw new Error('VpnConfig: privateKey is required');
|
||||||
|
}
|
||||||
|
if (!config.publicKey) {
|
||||||
|
throw new Error('VpnConfig: publicKey is required');
|
||||||
|
}
|
||||||
|
if (!config.subnet) {
|
||||||
|
throw new Error('VpnConfig: subnet is required');
|
||||||
|
}
|
||||||
|
if (!VpnConfig.isValidSubnet(config.subnet)) {
|
||||||
|
throw new Error(`VpnConfig: invalid subnet: ${config.subnet}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (config.mtu !== undefined && (config.mtu < 576 || config.mtu > 65535)) {
|
if (config.mtu !== undefined && (config.mtu < 576 || config.mtu > 65535)) {
|
||||||
throw new Error('VpnConfig: mtu must be between 576 and 65535');
|
throw new Error('VpnConfig: mtu must be between 576 and 65535');
|
||||||
@@ -104,4 +163,41 @@ export class VpnConfig {
|
|||||||
const prefixNum = parseInt(prefix, 10);
|
const prefixNum = parseInt(prefix, 10);
|
||||||
return !isNaN(prefixNum) && prefixNum >= 0 && prefixNum <= 32;
|
return !isNaN(prefixNum) && prefixNum >= 0 && prefixNum <= 32;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate a CIDR string (IPv4 or IPv6).
|
||||||
|
*/
|
||||||
|
private static isValidCidr(cidr: string): boolean {
|
||||||
|
const parts = cidr.split('/');
|
||||||
|
if (parts.length !== 2) return false;
|
||||||
|
const prefixNum = parseInt(parts[1], 10);
|
||||||
|
if (isNaN(prefixNum) || prefixNum < 0) return false;
|
||||||
|
// IPv4
|
||||||
|
if (VpnConfig.isValidIp(parts[0])) {
|
||||||
|
return prefixNum <= 32;
|
||||||
|
}
|
||||||
|
// IPv6 (basic check)
|
||||||
|
if (parts[0].includes(':')) {
|
||||||
|
return prefixNum <= 128;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate a base64-encoded 32-byte key (WireGuard X25519 format).
|
||||||
|
*/
|
||||||
|
private static validateBase64Key(key: string, fieldName: string): void {
|
||||||
|
if (key.length !== 44) {
|
||||||
|
throw new Error(`VpnConfig: ${fieldName} must be 44 characters (base64 of 32 bytes), got ${key.length}`);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const buf = Buffer.from(key, 'base64');
|
||||||
|
if (buf.length !== 32) {
|
||||||
|
throw new Error(`VpnConfig: ${fieldName} must decode to 32 bytes, got ${buf.length}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error && e.message.startsWith('VpnConfig:')) throw e;
|
||||||
|
throw new Error(`VpnConfig: ${fieldName} is not valid base64`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import type {
|
|||||||
IVpnClientInfo,
|
IVpnClientInfo,
|
||||||
IVpnKeypair,
|
IVpnKeypair,
|
||||||
IVpnClientTelemetry,
|
IVpnClientTelemetry,
|
||||||
|
IWgPeerConfig,
|
||||||
|
IWgPeerInfo,
|
||||||
TVpnServerCommands,
|
TVpnServerCommands,
|
||||||
} from './smartvpn.interfaces.js';
|
} from './smartvpn.interfaces.js';
|
||||||
|
|
||||||
@@ -121,6 +123,35 @@ export class VpnServer extends plugins.events.EventEmitter {
|
|||||||
return this.bridge.sendCommand('getClientTelemetry', { clientId });
|
return this.bridge.sendCommand('getClientTelemetry', { clientId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a WireGuard-compatible X25519 keypair.
|
||||||
|
*/
|
||||||
|
public async generateWgKeypair(): Promise<IVpnKeypair> {
|
||||||
|
return this.bridge.sendCommand('generateWgKeypair', {} as Record<string, never>);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a WireGuard peer (server must be running in wireguard mode).
|
||||||
|
*/
|
||||||
|
public async addWgPeer(peer: IWgPeerConfig): Promise<void> {
|
||||||
|
await this.bridge.sendCommand('addWgPeer', { peer });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a WireGuard peer by public key.
|
||||||
|
*/
|
||||||
|
public async removeWgPeer(publicKey: string): Promise<void> {
|
||||||
|
await this.bridge.sendCommand('removeWgPeer', { publicKey });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List WireGuard peers with stats.
|
||||||
|
*/
|
||||||
|
public async listWgPeers(): Promise<IWgPeerInfo[]> {
|
||||||
|
const result = await this.bridge.sendCommand('listWgPeers', {} as Record<string, never>);
|
||||||
|
return result.peers;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop the daemon bridge.
|
* Stop the daemon bridge.
|
||||||
*/
|
*/
|
||||||
|
|||||||
123
ts/smartvpn.classes.wgconfig.ts
Normal file
123
ts/smartvpn.classes.wgconfig.ts
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import type { IWgPeerConfig } from './smartvpn.interfaces.js';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// WireGuard .conf file generator
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export interface IWgClientConfOptions {
|
||||||
|
/** Client private key (base64) */
|
||||||
|
privateKey: string;
|
||||||
|
/** Client TUN address with prefix (e.g. 10.8.0.2/24) */
|
||||||
|
address: string;
|
||||||
|
/** DNS servers */
|
||||||
|
dns?: string[];
|
||||||
|
/** TUN MTU */
|
||||||
|
mtu?: number;
|
||||||
|
/** Server peer config */
|
||||||
|
peer: {
|
||||||
|
publicKey: string;
|
||||||
|
presharedKey?: string;
|
||||||
|
endpoint: string;
|
||||||
|
allowedIps: string[];
|
||||||
|
persistentKeepalive?: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IWgServerConfOptions {
|
||||||
|
/** Server private key (base64) */
|
||||||
|
privateKey: string;
|
||||||
|
/** Server TUN address with prefix (e.g. 10.8.0.1/24) */
|
||||||
|
address: string;
|
||||||
|
/** UDP listen port */
|
||||||
|
listenPort: number;
|
||||||
|
/** DNS servers */
|
||||||
|
dns?: string[];
|
||||||
|
/** TUN MTU */
|
||||||
|
mtu?: number;
|
||||||
|
/** Enable NAT — adds PostUp/PostDown iptables rules */
|
||||||
|
enableNat?: boolean;
|
||||||
|
/** Network interface for NAT (e.g. eth0). Auto-detected if omitted. */
|
||||||
|
natInterface?: string;
|
||||||
|
/** Configured peers */
|
||||||
|
peers: IWgPeerConfig[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates standard WireGuard .conf files compatible with wg-quick,
|
||||||
|
* WireGuard iOS/Android apps, and other standard WireGuard clients.
|
||||||
|
*/
|
||||||
|
export class WgConfigGenerator {
|
||||||
|
/**
|
||||||
|
* Generate a client .conf file content.
|
||||||
|
*/
|
||||||
|
public static generateClientConfig(opts: IWgClientConfOptions): string {
|
||||||
|
const lines: string[] = [];
|
||||||
|
|
||||||
|
lines.push('[Interface]');
|
||||||
|
lines.push(`PrivateKey = ${opts.privateKey}`);
|
||||||
|
lines.push(`Address = ${opts.address}`);
|
||||||
|
if (opts.dns && opts.dns.length > 0) {
|
||||||
|
lines.push(`DNS = ${opts.dns.join(', ')}`);
|
||||||
|
}
|
||||||
|
if (opts.mtu) {
|
||||||
|
lines.push(`MTU = ${opts.mtu}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push('');
|
||||||
|
lines.push('[Peer]');
|
||||||
|
lines.push(`PublicKey = ${opts.peer.publicKey}`);
|
||||||
|
if (opts.peer.presharedKey) {
|
||||||
|
lines.push(`PresharedKey = ${opts.peer.presharedKey}`);
|
||||||
|
}
|
||||||
|
lines.push(`Endpoint = ${opts.peer.endpoint}`);
|
||||||
|
lines.push(`AllowedIPs = ${opts.peer.allowedIps.join(', ')}`);
|
||||||
|
if (opts.peer.persistentKeepalive) {
|
||||||
|
lines.push(`PersistentKeepalive = ${opts.peer.persistentKeepalive}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push('');
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a server .conf file content.
|
||||||
|
*/
|
||||||
|
public static generateServerConfig(opts: IWgServerConfOptions): string {
|
||||||
|
const lines: string[] = [];
|
||||||
|
|
||||||
|
lines.push('[Interface]');
|
||||||
|
lines.push(`PrivateKey = ${opts.privateKey}`);
|
||||||
|
lines.push(`Address = ${opts.address}`);
|
||||||
|
lines.push(`ListenPort = ${opts.listenPort}`);
|
||||||
|
if (opts.dns && opts.dns.length > 0) {
|
||||||
|
lines.push(`DNS = ${opts.dns.join(', ')}`);
|
||||||
|
}
|
||||||
|
if (opts.mtu) {
|
||||||
|
lines.push(`MTU = ${opts.mtu}`);
|
||||||
|
}
|
||||||
|
if (opts.enableNat) {
|
||||||
|
const iface = opts.natInterface || 'eth0';
|
||||||
|
lines.push(`PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -t nat -A POSTROUTING -o ${iface} -j MASQUERADE`);
|
||||||
|
lines.push(`PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -t nat -D POSTROUTING -o ${iface} -j MASQUERADE`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const peer of opts.peers) {
|
||||||
|
lines.push('');
|
||||||
|
lines.push('[Peer]');
|
||||||
|
lines.push(`PublicKey = ${peer.publicKey}`);
|
||||||
|
if (peer.presharedKey) {
|
||||||
|
lines.push(`PresharedKey = ${peer.presharedKey}`);
|
||||||
|
}
|
||||||
|
lines.push(`AllowedIPs = ${peer.allowedIps.join(', ')}`);
|
||||||
|
if (peer.endpoint) {
|
||||||
|
lines.push(`Endpoint = ${peer.endpoint}`);
|
||||||
|
}
|
||||||
|
if (peer.persistentKeepalive) {
|
||||||
|
lines.push(`PersistentKeepalive = ${peer.persistentKeepalive}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push('');
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,10 +32,24 @@ export interface IVpnClientConfig {
|
|||||||
mtu?: number;
|
mtu?: number;
|
||||||
/** Keepalive interval in seconds (default: 30) */
|
/** Keepalive interval in seconds (default: 30) */
|
||||||
keepaliveIntervalSecs?: number;
|
keepaliveIntervalSecs?: number;
|
||||||
/** Transport protocol: 'auto' (default, tries QUIC then WS), 'websocket', or 'quic' */
|
/** Transport protocol: 'auto' (default, tries QUIC then WS), 'websocket', 'quic', or 'wireguard' */
|
||||||
transport?: 'auto' | 'websocket' | 'quic';
|
transport?: 'auto' | 'websocket' | 'quic' | 'wireguard';
|
||||||
/** For QUIC: SHA-256 hash of server certificate (base64) for cert pinning */
|
/** For QUIC: SHA-256 hash of server certificate (base64) for cert pinning */
|
||||||
serverCertHash?: string;
|
serverCertHash?: string;
|
||||||
|
/** WireGuard: client private key (base64, X25519) */
|
||||||
|
wgPrivateKey?: string;
|
||||||
|
/** WireGuard: client TUN address (e.g. 10.8.0.2) */
|
||||||
|
wgAddress?: string;
|
||||||
|
/** WireGuard: client TUN address prefix length (default: 24) */
|
||||||
|
wgAddressPrefix?: number;
|
||||||
|
/** WireGuard: preshared key (base64, optional) */
|
||||||
|
wgPresharedKey?: string;
|
||||||
|
/** WireGuard: persistent keepalive interval in seconds */
|
||||||
|
wgPersistentKeepalive?: number;
|
||||||
|
/** WireGuard: server endpoint (host:port, e.g. vpn.example.com:51820) */
|
||||||
|
wgEndpoint?: string;
|
||||||
|
/** WireGuard: allowed IPs (CIDR strings, e.g. ['0.0.0.0/0']) */
|
||||||
|
wgAllowedIps?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IVpnClientOptions {
|
export interface IVpnClientOptions {
|
||||||
@@ -72,12 +86,16 @@ export interface IVpnServerConfig {
|
|||||||
defaultRateLimitBytesPerSec?: number;
|
defaultRateLimitBytesPerSec?: number;
|
||||||
/** Default burst size for new clients (bytes). Omit for unlimited. */
|
/** Default burst size for new clients (bytes). Omit for unlimited. */
|
||||||
defaultBurstBytes?: number;
|
defaultBurstBytes?: number;
|
||||||
/** Transport mode: 'both' (default, WS+QUIC), 'websocket', or 'quic' */
|
/** Transport mode: 'both' (default, WS+QUIC), 'websocket', 'quic', or 'wireguard' */
|
||||||
transportMode?: 'websocket' | 'quic' | 'both';
|
transportMode?: 'websocket' | 'quic' | 'both' | 'wireguard';
|
||||||
/** QUIC listen address (host:port). Defaults to listenAddr. */
|
/** QUIC listen address (host:port). Defaults to listenAddr. */
|
||||||
quicListenAddr?: string;
|
quicListenAddr?: string;
|
||||||
/** QUIC idle timeout in seconds (default: 30) */
|
/** QUIC idle timeout in seconds (default: 30) */
|
||||||
quicIdleTimeoutSecs?: number;
|
quicIdleTimeoutSecs?: number;
|
||||||
|
/** WireGuard: UDP listen port (default: 51820) */
|
||||||
|
wgListenPort?: number;
|
||||||
|
/** WireGuard: configured peers */
|
||||||
|
wgPeers?: IWgPeerConfig[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IVpnServerOptions {
|
export interface IVpnServerOptions {
|
||||||
@@ -187,6 +205,35 @@ export interface IVpnClientTelemetry {
|
|||||||
burstBytes?: number;
|
burstBytes?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// WireGuard-specific types
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export interface IWgPeerConfig {
|
||||||
|
/** Peer's public key (base64, X25519) */
|
||||||
|
publicKey: string;
|
||||||
|
/** Optional preshared key (base64) */
|
||||||
|
presharedKey?: string;
|
||||||
|
/** Allowed IP ranges (CIDR strings) */
|
||||||
|
allowedIps: string[];
|
||||||
|
/** Peer endpoint (host:port) — optional for server peers, required for client */
|
||||||
|
endpoint?: string;
|
||||||
|
/** Persistent keepalive interval in seconds */
|
||||||
|
persistentKeepalive?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IWgPeerInfo {
|
||||||
|
publicKey: string;
|
||||||
|
allowedIps: string[];
|
||||||
|
endpoint?: string;
|
||||||
|
persistentKeepalive?: number;
|
||||||
|
bytesSent: number;
|
||||||
|
bytesReceived: number;
|
||||||
|
packetsSent: number;
|
||||||
|
packetsReceived: number;
|
||||||
|
lastHandshakeTime?: string;
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// IPC Command maps (used by smartrust RustBridge<TCommands>)
|
// IPC Command maps (used by smartrust RustBridge<TCommands>)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -211,6 +258,10 @@ export type TVpnServerCommands = {
|
|||||||
setClientRateLimit: { params: { clientId: string; rateBytesPerSec: number; burstBytes: number }; result: void };
|
setClientRateLimit: { params: { clientId: string; rateBytesPerSec: number; burstBytes: number }; result: void };
|
||||||
removeClientRateLimit: { params: { clientId: string }; result: void };
|
removeClientRateLimit: { params: { clientId: string }; result: void };
|
||||||
getClientTelemetry: { params: { clientId: string }; result: IVpnClientTelemetry };
|
getClientTelemetry: { params: { clientId: string }; result: IVpnClientTelemetry };
|
||||||
|
generateWgKeypair: { params: Record<string, never>; result: IVpnKeypair };
|
||||||
|
addWgPeer: { params: { peer: IWgPeerConfig }; result: void };
|
||||||
|
removeWgPeer: { params: { publicKey: string }; result: void };
|
||||||
|
listWgPeers: { params: Record<string, never>; result: { peers: IWgPeerInfo[] } };
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user