Release v1.1.0

This commit is contained in:
thakares committed 2026-09-02 15:19:19 +05:30
1 parent 34227efd2b
commit edc710cbd2
46 files changed
+5324 -190

No files matched your search

+10 -4
View File
@@ -91,7 +91,11 @@ impl ClientConfigBuilder {
// Check if already contains port
host_trimmed.to_string()
} else {
format!("{}:{}", host_trimmed, interface.listen_port)
format!(
"{}:{}",
host_trimmed,
interface.listen_port.unwrap_or(51820)
)
};
lines.push(format!("Endpoint = {endpoint}"));
@@ -144,7 +148,7 @@ mod tests {
use chrono::Utc;
use ipnet::IpNet;
use nx9_wg_core::crypto::{generate_keypair, generate_preshared_key};
use nx9_wg_core::types::wireguard::{PeerState, PeerType};
use nx9_wg_core::types::wireguard::{InterfaceRole, PeerState, PeerType};
use std::str::FromStr;
use uuid::Uuid;
@@ -158,9 +162,10 @@ mod tests {
let iface = Interface {
id: Uuid::new_v4(),
name: "wg0".to_string(),
role: InterfaceRole::Overlay,
private_key: srv_priv,
public_key: srv_pub.clone(),
listen_port: 51820,
listen_port: Some(51820),
address_v4: IpNet::from_str("10.0.0.1/24").unwrap(),
address_v6: None,
mtu: Some(1420),
@@ -231,9 +236,10 @@ mod tests {
let iface = Interface {
id: Uuid::new_v4(),
name: "wg0".to_string(),
role: InterfaceRole::Overlay,
private_key: srv_priv,
public_key: srv_pub,
listen_port: 51820,
listen_port: Some(51820),
address_v4: IpNet::from_str("10.0.0.1/24").unwrap(),
address_v6: None,
mtu: Some(1420),
+2 -2
View File
@@ -137,7 +137,7 @@ impl WireGuardEngine for SimulatedWireGuardEngine {
.filter(|p| p.state == PeerState::Active)
.map(|p| {
let allowed_ips: Vec<String> = p
.server_wireguard_allowed_ips()
.server_wireguard_allowed_ips_for_role(interface.role)
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
@@ -163,7 +163,7 @@ impl WireGuardEngine for SimulatedWireGuardEngine {
let stats = LiveInterfaceStats {
name: interface.name.clone(),
public_key: interface.public_key.as_str().to_string(),
listen_port: interface.listen_port,
listen_port: interface.listen_port.unwrap_or(0),
fwmark: 0,
peers: live_peers,
addresses,
+2
View File
@@ -6,6 +6,7 @@ pub mod error;
#[cfg(target_os = "linux")]
mod native_linux;
pub mod qr;
pub mod upstream_parser;
pub use config_builder::ClientConfigBuilder;
pub use engine::{
@@ -17,3 +18,4 @@ pub use qr::{
generate_qr_ascii, generate_qr_base64, generate_qr_data_url, generate_qr_png_bytes,
generate_qr_svg,
};
pub use upstream_parser::{ParsedUpstreamConfig, ParsedUpstreamPeer, UpstreamConfigParser};
+7 -2
View File
@@ -450,11 +450,16 @@ async fn configure_device(interface: &Interface, peers: &[Peer]) -> Result<()> {
let mut device_attrs: Vec<WireguardAttribute> = vec![
WireguardAttribute::IfName(interface.name.clone()),
WireguardAttribute::PrivateKey(private_key_bytes),
WireguardAttribute::ListenPort(interface.listen_port),
WireguardAttribute::Fwmark(0),
WireguardAttribute::Flags(WireguardDeviceFlags::ReplacePeers),
];
if let Some(port) = interface.listen_port {
if port != 0 {
device_attrs.push(WireguardAttribute::ListenPort(port));
}
}
// Build peer configurations for active peers only
let mut wg_peers = Vec::new();
for peer in peers.iter().filter(|p| p.state == PeerState::Active) {
@@ -484,7 +489,7 @@ async fn configure_device(interface: &Interface, peers: &[Peer]) -> Result<()> {
}
// Server-side Allowed IPs (cryptokey routing in Linux kernel)
let server_allowed_str = peer.server_wireguard_allowed_ips();
let server_allowed_str = peer.server_wireguard_allowed_ips_for_role(interface.role);
let allowed_ips = parse_allowed_ips(&server_allowed_str)?;
if !allowed_ips.is_empty() {
peer_attrs.push(WireguardPeerAttribute::Flags(
+464
View File
@@ -0,0 +1,464 @@
//! Third-party WireGuard configuration (.conf) parser and validator.
//!
//! Enforces:
//! - Exactly one `[Interface]` section containing `PrivateKey` and `Address`.
//! - Exactly one `[Peer]` section containing `PublicKey`, `Endpoint`, and `AllowedIPs`.
//! - Preservation of `0.0.0.0/0`, `::/0`, and specific CIDRs.
//! - Secret safety: Never exposes private or preshared keys in error messages.
use crate::error::{Result, WireGuardError};
use chrono::Utc;
use ipnet::IpNet;
use nx9_wg_core::crypto::derive_public_key;
use nx9_wg_core::types::wireguard::{
Interface, InterfaceRole, Peer, PeerProfile, PeerState, PeerType, WireGuardPresharedKey,
WireGuardPrivateKey, WireGuardPublicKey,
};
use nx9_wg_core::validation::{validate_interface_name, validate_listen_port, validate_mtu};
use std::net::{IpAddr, SocketAddr};
use std::str::FromStr;
use uuid::Uuid;
/// Parsed provider peer definition from standard WireGuard config.
#[derive(Debug, Clone)]
pub struct ParsedUpstreamPeer {
pub name: String,
pub public_key: WireGuardPublicKey,
pub preshared_key: Option<WireGuardPresharedKey>,
pub endpoint: String,
pub allowed_ips: String,
pub persistent_keepalive: Option<u16>,
}
/// Fully validated Upstream interface configuration.
#[derive(Debug, Clone)]
pub struct ParsedUpstreamConfig {
pub interface_name: String,
pub private_key: WireGuardPrivateKey,
pub public_key: WireGuardPublicKey,
pub listen_port: Option<u16>,
pub address_v4: IpNet,
pub address_v6: Option<IpNet>,
pub dns: Option<String>,
pub mtu: Option<u16>,
pub peer: ParsedUpstreamPeer,
}
impl ParsedUpstreamConfig {
/// Convert parsed configuration into desired-state domain structs (`Interface`, `Peer`).
pub fn into_desired_state(self, interface_id: Uuid, peer_id: Uuid) -> (Interface, Peer) {
let now = Utc::now().naive_utc();
let iface = Interface {
id: interface_id,
name: self.interface_name,
role: InterfaceRole::Upstream,
private_key: self.private_key,
public_key: self.public_key,
listen_port: self.listen_port,
address_v4: self.address_v4,
address_v6: self.address_v6,
mtu: self.mtu,
dns: self.dns.clone(),
enabled: true,
pre_up: None,
post_up: None,
pre_down: None,
post_down: None,
created_at: now,
updated_at: now,
};
let peer = Peer {
id: peer_id,
interface_id,
name: self.peer.name,
peer_type: PeerType::Server,
state: PeerState::Active,
public_key: self.peer.public_key,
private_key: None,
preshared_key: self.peer.preshared_key,
endpoint: Some(self.peer.endpoint),
allowed_ips: self.peer.allowed_ips.clone(),
server_allowed_ips: Some(self.peer.allowed_ips),
address_v4: None,
address_v6: None,
dns: self.dns,
mtu: self.mtu,
persistent_keepalive: self.peer.persistent_keepalive,
profile: PeerProfile::Custom,
expires_at: None,
last_handshake_at: None,
created_at: now,
updated_at: now,
};
(iface, peer)
}
}
/// Upstream WireGuard .conf parser.
#[derive(Debug, Clone, Default)]
pub struct UpstreamConfigParser;
impl UpstreamConfigParser {
/// Parse and validate a third-party WireGuard configuration string.
pub fn parse(raw_conf: &str, interface_name: &str) -> Result<ParsedUpstreamConfig> {
// 1. Validate interface name
validate_interface_name(interface_name)
.map_err(|e| WireGuardError::Config(format!("invalid interface name: {e}")))?;
if interface_name == "wg0" {
return Err(WireGuardError::Config(
"An Upstream interface cannot use the reserved name 'wg0'".to_string(),
));
}
// 2. Parse sections and key-values
let mut current_section: Option<String> = None;
let mut interface_section_count = 0;
let mut peer_section_count = 0;
let mut iface_private_key: Option<String> = None;
let mut iface_addresses: Vec<String> = Vec::new();
let mut iface_dns: Vec<String> = Vec::new();
let mut iface_mtu: Option<u16> = None;
let mut iface_listen_port: Option<u16> = None;
let mut peer_public_key: Option<String> = None;
let mut peer_preshared_key: Option<String> = None;
let mut peer_endpoint: Option<String> = None;
let mut peer_allowed_ips: Vec<String> = Vec::new();
let mut peer_keepalive: Option<u16> = None;
for (line_num, raw_line) in raw_conf.lines().enumerate() {
let line_idx = line_num + 1;
let line = raw_line.trim();
if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
continue;
}
// Section header
if line.starts_with('[') && line.ends_with(']') {
let sec_name = line[1..line.len() - 1].trim();
if sec_name.eq_ignore_ascii_case("interface") {
interface_section_count += 1;
current_section = Some("Interface".to_string());
} else if sec_name.eq_ignore_ascii_case("peer") {
peer_section_count += 1;
current_section = Some("Peer".to_string());
} else {
return Err(WireGuardError::Config(format!(
"Unsupported section '[{sec_name}]' on line {line_idx}"
)));
}
continue;
}
// Key-Value pair
let (key, val) = match line.split_once('=') {
Some((k, v)) => (k.trim(), v.trim()),
None => {
return Err(WireGuardError::Config(format!(
"Invalid key-value syntax on line {line_idx}: '{line}'"
)));
}
};
// Remove trailing comments from value if any
let clean_val = match val.split_once('#').or_else(|| val.split_once(';')) {
Some((clean, _)) => clean.trim(),
None => val,
};
match current_section.as_deref() {
Some("Interface") => {
if key.eq_ignore_ascii_case("privatekey") {
if clean_val.is_empty() {
return Err(WireGuardError::Config(
"PrivateKey value cannot be empty".to_string(),
));
}
iface_private_key = Some(clean_val.to_string());
} else if key.eq_ignore_ascii_case("address") {
for addr in clean_val.split(',') {
let trimmed = addr.trim();
if !trimmed.is_empty() {
iface_addresses.push(trimmed.to_string());
}
}
} else if key.eq_ignore_ascii_case("dns") {
for d in clean_val.split(',') {
let trimmed = d.trim();
if !trimmed.is_empty() {
iface_dns.push(trimmed.to_string());
}
}
} else if key.eq_ignore_ascii_case("mtu") {
let parsed_mtu = clean_val.parse::<u16>().map_err(|_| {
WireGuardError::Config(format!(
"Invalid MTU '{clean_val}' on line {line_idx}"
))
})?;
validate_mtu(parsed_mtu).map_err(|e| {
WireGuardError::Config(format!("MTU validation failed: {e}"))
})?;
iface_mtu = Some(parsed_mtu);
} else if key.eq_ignore_ascii_case("listenport") {
let parsed_port = clean_val.parse::<u16>().map_err(|_| {
WireGuardError::Config(format!(
"Invalid ListenPort '{clean_val}' on line {line_idx}"
))
})?;
validate_listen_port(parsed_port).map_err(|e| {
WireGuardError::Config(format!("ListenPort validation failed: {e}"))
})?;
iface_listen_port = Some(parsed_port);
} else {
tracing::debug!(key = %key, "Ignoring unrecognized Interface setting in upstream config");
}
}
Some("Peer") => {
if key.eq_ignore_ascii_case("publickey") {
if clean_val.is_empty() {
return Err(WireGuardError::Config(
"PublicKey value cannot be empty".to_string(),
));
}
peer_public_key = Some(clean_val.to_string());
} else if key.eq_ignore_ascii_case("presharedkey") {
if !clean_val.is_empty() {
peer_preshared_key = Some(clean_val.to_string());
}
} else if key.eq_ignore_ascii_case("endpoint") {
if clean_val.is_empty() {
return Err(WireGuardError::Config(
"Endpoint value cannot be empty".to_string(),
));
}
peer_endpoint = Some(clean_val.to_string());
} else if key.eq_ignore_ascii_case("allowedips") {
for item in clean_val.split(',') {
let trimmed = item.trim();
if !trimmed.is_empty() {
peer_allowed_ips.push(trimmed.to_string());
}
}
} else if key.eq_ignore_ascii_case("persistentkeepalive") {
let ka = clean_val.parse::<u16>().map_err(|_| {
WireGuardError::Config(format!(
"Invalid PersistentKeepalive '{clean_val}' on line {line_idx}"
))
})?;
peer_keepalive = Some(ka);
} else {
tracing::debug!(key = %key, "Ignoring unrecognized Peer setting in upstream config");
}
}
None => {
return Err(WireGuardError::Config(format!(
"Configuration entry '{line}' found outside any section on line {line_idx}"
)));
}
_ => unreachable!(),
}
}
// 3. Section cardinality checks
if interface_section_count == 0 {
return Err(WireGuardError::Config(
"Missing [Interface] section in WireGuard configuration".to_string(),
));
}
if interface_section_count > 1 {
return Err(WireGuardError::Config(format!(
"Configuration contains {interface_section_count} [Interface] sections (expected exactly 1)"
)));
}
if peer_section_count == 0 {
return Err(WireGuardError::Config(
"An Upstream configuration must contain exactly one [Peer] section (found 0)"
.to_string(),
));
}
if peer_section_count > 1 {
return Err(WireGuardError::Config(format!(
"An Upstream configuration must contain exactly one [Peer] section (found {peer_section_count})"
)));
}
// 4. Validate Interface fields
let raw_priv_k = iface_private_key.ok_or_else(|| {
WireGuardError::Config(
"Missing required 'PrivateKey' in [Interface] section".to_string(),
)
})?;
let pub_k = derive_public_key(&raw_priv_k).map_err(|_| {
WireGuardError::Config(
"Invalid PrivateKey: failed to decode 32-byte WireGuard key".to_string(),
)
})?;
let priv_k = WireGuardPrivateKey::new(raw_priv_k);
if iface_addresses.is_empty() {
return Err(WireGuardError::Config(
"Missing required 'Address' in [Interface] section".to_string(),
));
}
let mut v4_addr: Option<IpNet> = None;
let mut v6_addr: Option<IpNet> = None;
for addr_str in &iface_addresses {
let net = IpNet::from_str(addr_str).map_err(|e| {
WireGuardError::Config(format!("Invalid Address '{addr_str}': {e}"))
})?;
match net {
IpNet::V4(_) => {
if v4_addr.is_none() {
v4_addr = Some(net);
}
}
IpNet::V6(_) => {
if v6_addr.is_none() {
v6_addr = Some(net);
}
}
}
}
let address_v4 = v4_addr.ok_or_else(|| {
WireGuardError::Config(
"Upstream configuration requires at least one IPv4 address in Address".to_string(),
)
})?;
let dns = if iface_dns.is_empty() {
None
} else {
for d in &iface_dns {
if d.parse::<IpAddr>().is_err() {
return Err(WireGuardError::Config(format!("Invalid DNS address '{d}'")));
}
}
Some(iface_dns.join(", "))
};
let listen_port = iface_listen_port;
// 5. Validate Peer fields
let raw_peer_pub = peer_public_key.ok_or_else(|| {
WireGuardError::Config("Missing required 'PublicKey' in [Peer] section".to_string())
})?;
// Validate public key format (32 bytes base64)
use base64::Engine;
use base64::engine::general_purpose::STANDARD;
let pub_bytes = STANDARD.decode(raw_peer_pub.trim()).map_err(|_| {
WireGuardError::Config("Invalid Peer PublicKey: malformed base64".to_string())
})?;
if pub_bytes.len() != 32 {
return Err(WireGuardError::Config(
"Invalid Peer PublicKey: must be 32 bytes (256 bits)".to_string(),
));
}
let peer_pub = WireGuardPublicKey::new(raw_peer_pub);
let preshared_k = if let Some(psk_str) = peer_preshared_key {
let psk_bytes = STANDARD.decode(psk_str.trim()).map_err(|_| {
WireGuardError::Config("Invalid Peer PresharedKey: malformed base64".to_string())
})?;
if psk_bytes.len() != 32 {
return Err(WireGuardError::Config(
"Invalid Peer PresharedKey: must be 32 bytes (256 bits)".to_string(),
));
}
Some(WireGuardPresharedKey::new(psk_str))
} else {
None
};
let raw_endpoint = peer_endpoint.ok_or_else(|| {
WireGuardError::Config("Missing required 'Endpoint' in [Peer] section".to_string())
})?;
// Validate endpoint
validate_endpoint_syntax(&raw_endpoint)?;
if peer_allowed_ips.is_empty() {
return Err(WireGuardError::Config(
"Missing required 'AllowedIPs' in [Peer] section".to_string(),
));
}
let mut validated_allowed_ips = Vec::new();
for item in &peer_allowed_ips {
let net = IpNet::from_str(item).map_err(|e| {
WireGuardError::Config(format!("Invalid AllowedIPs CIDR '{item}': {e}"))
})?;
validated_allowed_ips.push(net.to_string());
}
Ok(ParsedUpstreamConfig {
interface_name: interface_name.to_string(),
private_key: priv_k,
public_key: pub_k,
listen_port,
address_v4,
address_v6: v6_addr,
dns,
mtu: iface_mtu,
peer: ParsedUpstreamPeer {
name: format!("{interface_name}-provider"),
public_key: peer_pub,
preshared_key: preshared_k,
endpoint: raw_endpoint,
allowed_ips: validated_allowed_ips.join(", "),
persistent_keepalive: peer_keepalive,
},
})
}
}
/// Validate endpoint format: IP:port or hostname:port
fn validate_endpoint_syntax(endpoint_str: &str) -> Result<()> {
let trimmed = endpoint_str.trim();
if trimmed.is_empty() {
return Err(WireGuardError::Config(
"Endpoint cannot be empty".to_string(),
));
}
if trimmed.parse::<SocketAddr>().is_ok() {
return Ok(());
}
if let Some(idx) = trimmed.rfind(':') {
let host = &trimmed[..idx];
let port_str = &trimmed[idx + 1..];
if host.is_empty() {
return Err(WireGuardError::Config(format!(
"Invalid endpoint '{trimmed}': missing host"
)));
}
let port = port_str.parse::<u16>().map_err(|_| {
WireGuardError::Config(format!("Invalid endpoint port '{port_str}' in '{trimmed}'"))
})?;
if port == 0 {
return Err(WireGuardError::Config(format!(
"Invalid endpoint port 0 in '{trimmed}'"
)));
}
return Ok(());
}
Err(WireGuardError::Config(format!(
"Invalid endpoint '{trimmed}': missing port (expected host:port)"
)))
}
@@ -0,0 +1,274 @@
//! Comprehensive test suite for third-party WireGuard Upstream .conf parsing and validation.
use nx9_wg_core::crypto::generate_keypair;
use nx9_wg_core::types::wireguard::InterfaceRole;
use nx9_wireguard::UpstreamConfigParser;
use uuid::Uuid;
#[test]
fn test_valid_proton_style_configuration() {
let (priv_k, pub_k) = generate_keypair();
let (_, peer_pub_k) = generate_keypair();
let conf = format!(
r#"
# ProtonVPN WireGuard Configuration
[Interface]
PrivateKey = {}
Address = 10.2.0.2/32
DNS = 10.2.0.1
MTU = 1420
[Peer]
# Server Node
PublicKey = {}
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = 37.19.199.155:51820
PersistentKeepalive = 25
"#,
priv_k.as_str(),
peer_pub_k.as_str()
);
let parsed = UpstreamConfigParser::parse(&conf, "proton0").expect("parse proton config");
assert_eq!(parsed.interface_name, "proton0");
assert_eq!(parsed.public_key.as_str(), pub_k.as_str());
assert_eq!(parsed.address_v4.to_string(), "10.2.0.2/32");
assert_eq!(parsed.address_v6, None);
assert_eq!(parsed.dns, Some("10.2.0.1".to_string()));
assert_eq!(parsed.mtu, Some(1420));
assert_eq!(parsed.listen_port, None);
assert_eq!(parsed.peer.name, "proton0-provider");
assert_eq!(parsed.peer.public_key.as_str(), peer_pub_k.as_str());
assert_eq!(parsed.peer.endpoint, "37.19.199.155:51820");
assert_eq!(parsed.peer.allowed_ips, "0.0.0.0/0, ::/0");
assert_eq!(parsed.peer.persistent_keepalive, Some(25));
assert_eq!(parsed.peer.preshared_key, None);
let iface_id = Uuid::new_v4();
let peer_id = Uuid::new_v4();
let (iface, peer) = parsed.into_desired_state(iface_id, peer_id);
assert_eq!(iface.id, iface_id);
assert_eq!(iface.name, "proton0");
assert_eq!(iface.role, InterfaceRole::Upstream);
assert_eq!(iface.listen_port, None);
assert!(iface.enabled);
assert_eq!(peer.id, peer_id);
assert_eq!(peer.interface_id, iface_id);
assert_eq!(peer.name, "proton0-provider");
assert_eq!(peer.allowed_ips, "0.0.0.0/0, ::/0");
assert_eq!(
peer.server_wireguard_allowed_ips_for_role(InterfaceRole::Upstream),
"0.0.0.0/0, ::/0"
);
}
#[test]
fn test_omitted_listen_port_remains_unspecified() {
let (priv_k, _) = generate_keypair();
let (_, peer_pub_k) = generate_keypair();
let conf = format!(
"[Interface]\nPrivateKey = {}\nAddress = 10.2.0.2/32\n[Peer]\nPublicKey = {}\nAllowedIPs = 0.0.0.0/0, ::/0\nEndpoint = 37.19.199.155:51820\n",
priv_k.as_str(),
peer_pub_k.as_str()
);
let parsed = UpstreamConfigParser::parse(&conf, "proton0").expect("parse config");
assert_eq!(parsed.listen_port, None);
assert_eq!(parsed.peer.endpoint, "37.19.199.155:51820");
}
#[test]
fn test_explicit_listen_port_is_preserved() {
let (priv_k, _) = generate_keypair();
let (_, peer_pub_k) = generate_keypair();
let conf = format!(
"[Interface]\nPrivateKey = {}\nAddress = 10.2.0.2/32\nListenPort = 45000\n[Peer]\nPublicKey = {}\nAllowedIPs = 0.0.0.0/0, ::/0\nEndpoint = 37.19.199.155:51820\n",
priv_k.as_str(),
peer_pub_k.as_str()
);
let parsed = UpstreamConfigParser::parse(&conf, "proton0").expect("parse config");
assert_eq!(parsed.listen_port, Some(45000));
}
#[test]
fn test_endpoint_port_is_not_local_listen_port() {
let (priv_k, _) = generate_keypair();
let (_, peer_pub_k) = generate_keypair();
let conf = format!(
"[Interface]\nPrivateKey = {}\nAddress = 10.2.0.2/32\n[Peer]\nPublicKey = {}\nAllowedIPs = 0.0.0.0/0, ::/0\nEndpoint = 37.19.199.155:51820\n",
priv_k.as_str(),
peer_pub_k.as_str()
);
let parsed = UpstreamConfigParser::parse(&conf, "proton0").expect("parse config");
assert_eq!(parsed.listen_port, None);
assert!(parsed.peer.endpoint.ends_with(":51820"));
}
#[test]
fn test_dual_stack_address_and_preshared_key() {
let (priv_k, _) = generate_keypair();
let (_, peer_pub_k) = generate_keypair();
let (psk, _) = generate_keypair();
let conf = format!(
r#"
[Interface]
PrivateKey = {}
Address = 10.2.0.2/32, fd00::2/64
DNS = 10.2.0.1, 1.1.1.1
[Peer]
PublicKey = {}
PresharedKey = {}
AllowedIPs = 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
Endpoint = vpn.example.com:51820
"#,
priv_k.as_str(),
peer_pub_k.as_str(),
psk.as_str()
);
let parsed = UpstreamConfigParser::parse(&conf, "vpn0").expect("parse dual stack");
assert_eq!(parsed.address_v4.to_string(), "10.2.0.2/32");
assert_eq!(
parsed.address_v6.map(|ip| ip.to_string()),
Some("fd00::2/64".to_string())
);
assert_eq!(parsed.dns, Some("10.2.0.1, 1.1.1.1".to_string()));
assert!(parsed.peer.preshared_key.is_some());
assert_eq!(parsed.peer.preshared_key.unwrap().as_str(), psk.as_str());
}
#[test]
fn test_reject_wg0_interface_name() {
let (priv_k, _) = generate_keypair();
let (_, peer_pub_k) = generate_keypair();
let conf = format!(
"[Interface]\nPrivateKey = {}\nAddress = 10.2.0.2/32\n[Peer]\nPublicKey = {}\nAllowedIPs = 0.0.0.0/0\nEndpoint = 1.2.3.4:51820\n",
priv_k.as_str(),
peer_pub_k.as_str()
);
let err = UpstreamConfigParser::parse(&conf, "wg0").unwrap_err();
assert!(err.to_string().contains("reserved name 'wg0'"));
}
#[test]
fn test_cardinality_rejections() {
let (priv_k, _) = generate_keypair();
let (_, peer_pub_k) = generate_keypair();
// 0 peers
let no_peers = format!(
"[Interface]\nPrivateKey = {}\nAddress = 10.2.0.2/32\n",
priv_k.as_str()
);
assert!(UpstreamConfigParser::parse(&no_peers, "proton0").is_err());
// 2 peers
let multi_peers = format!(
r#"
[Interface]
PrivateKey = {}
Address = 10.2.0.2/32
[Peer]
PublicKey = {}
AllowedIPs = 0.0.0.0/0
Endpoint = 1.2.3.4:51820
[Peer]
PublicKey = {}
AllowedIPs = 0.0.0.0/0
Endpoint = 5.6.7.8:51820
"#,
priv_k.as_str(),
peer_pub_k.as_str(),
peer_pub_k.as_str()
);
let err = UpstreamConfigParser::parse(&multi_peers, "proton0").unwrap_err();
assert!(err.to_string().contains("exactly one [Peer] section"));
// Missing [Interface]
let no_iface = format!(
"[Peer]\nPublicKey = {}\nAllowedIPs = 0.0.0.0/0\nEndpoint = 1.2.3.4:51820\n",
peer_pub_k.as_str()
);
assert!(UpstreamConfigParser::parse(&no_iface, "proton0").is_err());
}
#[test]
fn test_missing_and_malformed_fields_rejections() {
let (_, peer_pub_k) = generate_keypair();
// Missing PrivateKey
let no_priv = format!(
"[Interface]\nAddress = 10.2.0.2/32\n[Peer]\nPublicKey = {}\nAllowedIPs = 0.0.0.0/0\nEndpoint = 1.2.3.4:51820\n",
peer_pub_k.as_str()
);
assert!(UpstreamConfigParser::parse(&no_priv, "proton0").is_err());
// Malformed PrivateKey
let bad_priv = format!(
"[Interface]\nPrivateKey = not-a-key\nAddress = 10.2.0.2/32\n[Peer]\nPublicKey = {}\nAllowedIPs = 0.0.0.0/0\nEndpoint = 1.2.3.4:51820\n",
peer_pub_k.as_str()
);
assert!(UpstreamConfigParser::parse(&bad_priv, "proton0").is_err());
// Missing Address
let (priv_k, _) = generate_keypair();
let no_addr = format!(
"[Interface]\nPrivateKey = {}\n[Peer]\nPublicKey = {}\nAllowedIPs = 0.0.0.0/0\nEndpoint = 1.2.3.4:51820\n",
priv_k.as_str(),
peer_pub_k.as_str()
);
assert!(UpstreamConfigParser::parse(&no_addr, "proton0").is_err());
// Malformed Address
let bad_addr = format!(
"[Interface]\nPrivateKey = {}\nAddress = 999.999.999.999/99\n[Peer]\nPublicKey = {}\nAllowedIPs = 0.0.0.0/0\nEndpoint = 1.2.3.4:51820\n",
priv_k.as_str(),
peer_pub_k.as_str()
);
assert!(UpstreamConfigParser::parse(&bad_addr, "proton0").is_err());
// Missing PublicKey
let no_pub = format!(
"[Interface]\nPrivateKey = {}\nAddress = 10.2.0.2/32\n[Peer]\nAllowedIPs = 0.0.0.0/0\nEndpoint = 1.2.3.4:51820\n",
priv_k.as_str()
);
assert!(UpstreamConfigParser::parse(&no_pub, "proton0").is_err());
// Missing Endpoint
let no_ep = format!(
"[Interface]\nPrivateKey = {}\nAddress = 10.2.0.2/32\n[Peer]\nPublicKey = {}\nAllowedIPs = 0.0.0.0/0\n",
priv_k.as_str(),
peer_pub_k.as_str()
);
assert!(UpstreamConfigParser::parse(&no_ep, "proton0").is_err());
// Missing AllowedIPs
let no_aips = format!(
"[Interface]\nPrivateKey = {}\nAddress = 10.2.0.2/32\n[Peer]\nPublicKey = {}\nEndpoint = 1.2.3.4:51820\n",
priv_k.as_str(),
peer_pub_k.as_str()
);
assert!(UpstreamConfigParser::parse(&no_aips, "proton0").is_err());
// Unknown section
let unknown_sec = format!(
"[Interface]\nPrivateKey = {}\nAddress = 10.2.0.2/32\n[Unknown]\nKey = Val\n[Peer]\nPublicKey = {}\nAllowedIPs = 0.0.0.0/0\nEndpoint = 1.2.3.4:51820\n",
priv_k.as_str(),
peer_pub_k.as_str()
);
assert!(UpstreamConfigParser::parse(&unknown_sec, "proton0").is_err());
}
@@ -3,7 +3,9 @@
use chrono::Utc;
use ipnet::IpNet;
use nx9_wg_core::crypto::{generate_keypair, generate_preshared_key};
use nx9_wg_core::types::wireguard::{Interface, Peer, PeerProfile, PeerState, PeerType};
use nx9_wg_core::types::wireguard::{
Interface, InterfaceRole, Peer, PeerProfile, PeerState, PeerType,
};
use nx9_wireguard::{
ClientConfigBuilder, SimulatedWireGuardEngine, WireGuardEngine, generate_qr_ascii,
generate_qr_data_url, generate_qr_png_bytes, generate_qr_svg,
@@ -23,9 +25,10 @@ async fn test_wireguard_engine_lifecycle_and_telemetry() {
let iface = Interface {
id: Uuid::new_v4(),
name: "wg0".to_string(),
role: InterfaceRole::Overlay,
private_key: srv_priv,
public_key: srv_pub.clone(),
listen_port: 51820,
listen_port: Some(51820),
address_v4: IpNet::from_str("10.0.0.1/24").unwrap(),
address_v6: None,
mtu: Some(1420),
@@ -138,9 +141,10 @@ fn test_client_config_and_qr_codes() {
let iface = Interface {
id: Uuid::new_v4(),
name: "wg0".to_string(),
role: InterfaceRole::Overlay,
private_key: srv_priv,
public_key: srv_pub,
listen_port: 51820,
listen_port: Some(51820),
address_v4: IpNet::from_str("10.0.0.1/24").unwrap(),
address_v6: None,
mtu: Some(1420),