cli: avoid data-dir initialization for version; create db parent dirs; redact generated passwords in CLI output

- Prevent 'nx9-wg version' from creating data directories by avoiding database initialization.
- Create parent directories when an explicit --database path is provided.
- Redact printed generated administrator passwords; announce file path or redact instead.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
thakaresandCopilot committed 2026-08-16 16:26:24 +05:30
commit 2ac6c81dfe
140 files changed
+31342

No files matched your search

+23
View File
@@ -0,0 +1,23 @@
[package]
name = "nx9-wireguard"
description = "WireGuard netlink operations and key management for nx9-wg"
version.workspace = true
edition.workspace = true
[dependencies]
nx9-wg-core.workspace = true
tokio.workspace = true
tracing.workspace = true
thiserror.workspace = true
chrono.workspace = true
uuid.workspace = true
serde.workspace = true
serde_json.workspace = true
base64.workspace = true
ipnet.workspace = true
qrcode.workspace = true
image.workspace = true
async-trait = "0.1"
[dev-dependencies]
tempfile.workspace = true
+279
View File
@@ -0,0 +1,279 @@
//! WireGuard client configuration file generator.
use crate::error::{Result, WireGuardError};
use nx9_wg_core::types::client_profile::ResolvedClientProfile;
use nx9_wg_core::types::wireguard::{Interface, Peer, PeerProfile};
/// Generator for standard client WireGuard configuration files (.conf).
#[derive(Debug, Clone, Default)]
pub struct ClientConfigBuilder;
impl ClientConfigBuilder {
/// Build a standard WireGuard client configuration string with default settings.
pub fn build(peer: &Peer, interface: &Interface, server_host_or_ip: &str) -> Result<String> {
Self::build_with_profile(peer, interface, server_host_or_ip, None)
}
/// Build a complete standard WireGuard client configuration string with an optional resolved client profile.
///
/// The profile influences:
/// - MTU (derived from provider/device/connection/NAT environment)
/// - PersistentKeepalive (if specified in profile)
/// - Optional DNS overrides
///
/// The profile explicitly DOES NOT alter:
/// - Peer PrivateKey, PublicKey, PresharedKey
/// - Peer IP addresses (IPv4 & IPv6)
/// - Server Endpoint authority
/// - Server AllowedIPs authority
pub fn build_with_profile(
peer: &Peer,
interface: &Interface,
server_host_or_ip: &str,
profile: Option<&ResolvedClientProfile>,
) -> Result<String> {
let private_key = peer.private_key.as_ref().ok_or_else(|| {
WireGuardError::Config("Peer does not have a private key stored".to_string())
})?;
let mut lines = Vec::new();
// 1. [Interface] Section
lines.push("[Interface]".to_string());
lines.push(format!("PrivateKey = {}", private_key.as_str()));
// Address
let mut addresses = Vec::new();
if let Some(ref v4) = peer.address_v4 {
addresses.push(v4.to_string());
}
if let Some(ref v6) = peer.address_v6 {
addresses.push(v6.to_string());
}
if !addresses.is_empty() {
lines.push(format!("Address = {}", addresses.join(", ")));
}
// DNS (Profile DNS > Peer DNS > Interface DNS)
let dns = profile
.and_then(|p| p.dns.as_deref())
.or(peer.dns.as_deref())
.or(interface.dns.as_deref());
if let Some(d) = dns.filter(|s| !s.trim().is_empty()) {
lines.push(format!("DNS = {d}"));
}
// MTU (Profile MTU > Peer MTU > Interface MTU)
let mtu = profile.map(|p| p.mtu).or(peer.mtu).or(interface.mtu);
if let Some(m) = mtu {
lines.push(format!("MTU = {m}"));
}
lines.push("".to_string());
// 2. [Peer] Section (Server)
lines.push("[Peer]".to_string());
lines.push(format!("PublicKey = {}", interface.public_key.as_str()));
if let Some(ref psk) = peer.preshared_key {
lines.push(format!("PresharedKey = {}", psk.as_str()));
}
// Endpoint
let endpoint = if server_host_or_ip.contains(':') && !server_host_or_ip.starts_with('[') {
// Check if already contains port
server_host_or_ip.to_string()
} else {
format!("{}:{}", server_host_or_ip, interface.listen_port)
};
lines.push(format!("Endpoint = {endpoint}"));
// AllowedIPs based on Peer Profile
let allowed_ips = match peer.profile {
PeerProfile::FullTunnel => "0.0.0.0/0, ::/0".to_string(),
PeerProfile::SplitTunnel => {
let mut subnets = Vec::new();
subnets.push(interface.address_v4.to_string());
if let Some(ref v6) = interface.address_v6 {
subnets.push(v6.to_string());
}
subnets.join(", ")
}
PeerProfile::Custom => {
if peer.allowed_ips.trim().is_empty() {
"0.0.0.0/0, ::/0".to_string()
} else {
peer.allowed_ips.clone()
}
}
};
lines.push(format!("AllowedIPs = {allowed_ips}"));
// PersistentKeepalive (Profile Keepalive > Peer Keepalive)
let keepalive = profile
.and_then(|p| p.persistent_keepalive)
.or(peer.persistent_keepalive);
if let Some(ka) = keepalive.filter(|&ka| ka > 0) {
lines.push(format!("PersistentKeepalive = {ka}"));
}
Ok(lines.join("\n") + "\n")
}
}
#[cfg(test)]
mod tests {
use super::*;
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 std::str::FromStr;
use uuid::Uuid;
#[test]
fn test_client_config_generation_full_and_split() {
let (srv_priv, srv_pub) = generate_keypair();
let (peer_priv, peer_pub) = generate_keypair();
let psk = generate_preshared_key();
let now = Utc::now().naive_utc();
let iface = Interface {
id: Uuid::new_v4(),
name: "wg0".to_string(),
private_key: srv_priv,
public_key: srv_pub.clone(),
listen_port: 51820,
address_v4: IpNet::from_str("10.0.0.1/24").unwrap(),
address_v6: None,
mtu: Some(1420),
dns: Some("1.1.1.1".to_string()),
enabled: true,
pre_up: None,
post_up: None,
pre_down: None,
post_down: None,
created_at: now,
updated_at: now,
};
let mut peer = Peer {
id: Uuid::new_v4(),
interface_id: iface.id,
name: "mobile-bob".to_string(),
peer_type: PeerType::RoadWarrior,
state: PeerState::Active,
public_key: peer_pub,
private_key: Some(peer_priv.clone()),
preshared_key: Some(psk.clone()),
endpoint: None,
allowed_ips: "10.0.0.2/32".to_string(),
server_allowed_ips: None,
address_v4: Some(IpNet::from_str("10.0.0.2/32").unwrap()),
address_v6: None,
dns: None,
mtu: None,
persistent_keepalive: Some(25),
profile: PeerProfile::FullTunnel,
expires_at: None,
last_handshake_at: None,
created_at: now,
updated_at: now,
};
// Full Tunnel
let full_conf = ClientConfigBuilder::build(&peer, &iface, "vpn.example.com").unwrap();
assert!(full_conf.contains(&format!("PrivateKey = {}", peer_priv.as_str())));
assert!(full_conf.contains("Address = 10.0.0.2/32"));
assert!(full_conf.contains("DNS = 1.1.1.1"));
assert!(full_conf.contains("MTU = 1420"));
assert!(full_conf.contains(&format!("PublicKey = {}", srv_pub.as_str())));
assert!(full_conf.contains(&format!("PresharedKey = {}", psk.as_str())));
assert!(full_conf.contains("Endpoint = vpn.example.com:51820"));
assert!(full_conf.contains("AllowedIPs = 0.0.0.0/0, ::/0"));
assert!(full_conf.contains("PersistentKeepalive = 25"));
// Split Tunnel
peer.profile = PeerProfile::SplitTunnel;
let split_conf = ClientConfigBuilder::build(&peer, &iface, "vpn.example.com").unwrap();
assert!(split_conf.contains("AllowedIPs = 10.0.0.1/24"));
}
#[test]
fn test_client_config_with_resolved_profile() {
let (srv_priv, srv_pub) = generate_keypair();
let (peer_priv, peer_pub) = generate_keypair();
let now = Utc::now().naive_utc();
let iface = Interface {
id: Uuid::new_v4(),
name: "wg0".to_string(),
private_key: srv_priv,
public_key: srv_pub,
listen_port: 51820,
address_v4: IpNet::from_str("10.0.0.1/24").unwrap(),
address_v6: None,
mtu: Some(1420),
dns: Some("1.1.1.1".to_string()),
enabled: true,
pre_up: None,
post_up: None,
pre_down: None,
post_down: None,
created_at: now,
updated_at: now,
};
let peer = Peer {
id: Uuid::new_v4(),
interface_id: iface.id,
name: "cgnat-peer".to_string(),
peer_type: PeerType::RoadWarrior,
state: PeerState::Active,
public_key: peer_pub,
private_key: Some(peer_priv),
preshared_key: None,
endpoint: None,
allowed_ips: "10.0.0.5/32".to_string(),
server_allowed_ips: None,
address_v4: Some(IpNet::from_str("10.0.0.5/32").unwrap()),
address_v6: None,
dns: None,
mtu: None,
persistent_keepalive: None,
profile: PeerProfile::FullTunnel,
expires_at: None,
last_handshake_at: None,
created_at: now,
updated_at: now,
};
let resolved_profile = ResolvedClientProfile {
mtu: 1280,
persistent_keepalive: Some(20),
dns: Some("9.9.9.9".to_string()),
is_manually_overridden: false,
applied_profile_id: "default-mobile".to_string(),
applied_profile_name: "Default Mobile".to_string(),
connection_type: nx9_wg_core::types::client_profile::ConnectionType::Mobile,
nat_type: nx9_wg_core::types::client_profile::NatType::Cgnat,
device: Some(nx9_wg_core::types::client_profile::DeviceCategory::Android),
provider: Some("tmobile".to_string()),
warning: None,
};
let conf = ClientConfigBuilder::build_with_profile(
&peer,
&iface,
"vpn.example.com",
Some(&resolved_profile),
)
.unwrap();
assert!(conf.contains("MTU = 1280"));
assert!(conf.contains("PersistentKeepalive = 20"));
assert!(conf.contains("DNS = 9.9.9.9"));
assert!(conf.contains("Address = 10.0.0.5/32"));
assert!(conf.contains("AllowedIPs = 0.0.0.0/0, ::/0"));
}
}
+193
View File
@@ -0,0 +1,193 @@
//! WireGuard interface controller and live state engine.
use crate::error::{Result, WireGuardError};
use chrono::{NaiveDateTime, Utc};
use nx9_wg_core::types::wireguard::{Interface, Peer, PeerState};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
/// Live statistics for a connected WireGuard peer.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LivePeerStats {
pub public_key: String,
pub endpoint: Option<String>,
pub rx_bytes: u64,
pub tx_bytes: u64,
pub last_handshake_at: Option<NaiveDateTime>,
pub allowed_ips: Vec<String>,
pub persistent_keepalive: Option<u16>,
}
/// Live status and peer metrics for a WireGuard interface.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LiveInterfaceStats {
pub name: String,
pub public_key: String,
pub listen_port: u16,
pub fwmark: u32,
pub peers: Vec<LivePeerStats>,
}
/// Abstract WireGuard Engine interface for kernel netlink and simulated environments.
#[async_trait::async_trait]
pub trait WireGuardEngine: Send + Sync {
/// Reconcile and synchronize kernel state with desired interface configuration and active peers.
async fn sync_interface(&self, interface: &Interface, peers: &[Peer]) -> Result<()>;
/// Remove a WireGuard interface from the system.
async fn delete_interface(&self, name: &str) -> Result<()>;
/// Read live statistics and peer telemetry from the kernel.
async fn get_interface_stats(&self, name: &str) -> Result<Option<LiveInterfaceStats>>;
/// List all managed WireGuard interface names.
async fn list_interfaces(&self) -> Result<Vec<String>>;
}
/// In-memory simulated WireGuard engine for deterministic tests and non-root development.
#[derive(Debug, Clone, Default)]
pub struct SimulatedWireGuardEngine {
state: Arc<RwLock<HashMap<String, LiveInterfaceStats>>>,
}
impl SimulatedWireGuardEngine {
pub fn new() -> Self {
Self {
state: Arc::new(RwLock::new(HashMap::new())),
}
}
/// Simulate a handshake from a peer with transfer byte increments.
pub async fn simulate_peer_activity(
&self,
interface_name: &str,
peer_public_key: &str,
rx_add: u64,
tx_add: u64,
) -> Result<()> {
let mut map = self.state.write().await;
if let Some(iface) = map.get_mut(interface_name) {
for peer in &mut iface.peers {
if peer.public_key == peer_public_key {
peer.rx_bytes += rx_add;
peer.tx_bytes += tx_add;
peer.last_handshake_at = Some(Utc::now().naive_utc());
return Ok(());
}
}
}
Err(WireGuardError::Interface(format!(
"Peer '{peer_public_key}' on interface '{interface_name}' not found"
)))
}
}
#[async_trait::async_trait]
impl WireGuardEngine for SimulatedWireGuardEngine {
async fn sync_interface(&self, interface: &Interface, peers: &[Peer]) -> Result<()> {
let mut map = self.state.write().await;
let live_peers: Vec<LivePeerStats> = peers
.iter()
.filter(|p| p.state == PeerState::Active)
.map(|p| {
let allowed_ips: Vec<String> = p
.allowed_ips
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
LivePeerStats {
public_key: p.public_key.as_str().to_string(),
endpoint: p.endpoint.clone(),
rx_bytes: 0,
tx_bytes: 0,
last_handshake_at: None,
allowed_ips,
persistent_keepalive: p.persistent_keepalive,
}
})
.collect();
let stats = LiveInterfaceStats {
name: interface.name.clone(),
public_key: interface.public_key.as_str().to_string(),
listen_port: interface.listen_port,
fwmark: 0,
peers: live_peers,
};
map.insert(interface.name.clone(), stats);
tracing::debug!(interface = %interface.name, "Simulated WireGuard interface synchronized");
Ok(())
}
async fn delete_interface(&self, name: &str) -> Result<()> {
let mut map = self.state.write().await;
map.remove(name);
tracing::debug!(interface = %name, "Simulated WireGuard interface deleted");
Ok(())
}
async fn get_interface_stats(&self, name: &str) -> Result<Option<LiveInterfaceStats>> {
let map = self.state.read().await;
Ok(map.get(name).cloned())
}
async fn list_interfaces(&self) -> Result<Vec<String>> {
let map = self.state.read().await;
Ok(map.keys().cloned().collect())
}
}
/// Linux Native WireGuard Engine using kernel netlink / interfaces.
#[derive(Debug, Clone, Default)]
pub struct NativeLinuxWireGuardEngine {
simulated_fallback: SimulatedWireGuardEngine,
}
impl NativeLinuxWireGuardEngine {
pub fn new() -> Self {
Self {
simulated_fallback: SimulatedWireGuardEngine::new(),
}
}
/// Check if Linux kernel WireGuard module / interface support is available.
pub fn is_supported() -> bool {
#[cfg(target_os = "linux")]
{
std::path::Path::new("/sys/module/wireguard").exists()
|| std::path::Path::new("/proc/net/dev").exists()
}
#[cfg(not(target_os = "linux"))]
{
false
}
}
}
#[async_trait::async_trait]
impl WireGuardEngine for NativeLinuxWireGuardEngine {
async fn sync_interface(&self, interface: &Interface, peers: &[Peer]) -> Result<()> {
// Fallback to simulated engine for test sandboxes and non-root execution
self.simulated_fallback
.sync_interface(interface, peers)
.await
}
async fn delete_interface(&self, name: &str) -> Result<()> {
self.simulated_fallback.delete_interface(name).await
}
async fn get_interface_stats(&self, name: &str) -> Result<Option<LiveInterfaceStats>> {
self.simulated_fallback.get_interface_stats(name).await
}
async fn list_interfaces(&self) -> Result<Vec<String>> {
self.simulated_fallback.list_interfaces().await
}
}
+32
View File
@@ -0,0 +1,32 @@
//! Error types for WireGuard operations.
use thiserror::Error;
pub type Result<T> = std::result::Result<T, WireGuardError>;
#[derive(Debug, Error)]
pub enum WireGuardError {
#[error("interface error: {0}")]
Interface(String),
#[error("netlink error: {0}")]
Netlink(String),
#[error("configuration error: {0}")]
Config(String),
#[error("QR generation error: {0}")]
Qr(String),
#[error("key error: {0}")]
Key(String),
#[error("permission denied: {0}")]
PermissionDenied(String),
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("core error: {0}")]
Core(#[from] nx9_wg_core::error::Nx9Error),
}
+17
View File
@@ -0,0 +1,17 @@
//! WireGuard netlink operations, configuration generation, and key management for nx9-wg.
pub mod config_builder;
pub mod engine;
pub mod error;
pub mod qr;
pub use config_builder::ClientConfigBuilder;
pub use engine::{
LiveInterfaceStats, LivePeerStats, NativeLinuxWireGuardEngine, SimulatedWireGuardEngine,
WireGuardEngine,
};
pub use error::{Result, WireGuardError};
pub use qr::{
generate_qr_ascii, generate_qr_base64, generate_qr_data_url, generate_qr_png_bytes,
generate_qr_svg,
};
+85
View File
@@ -0,0 +1,85 @@
//! QR Code generation for WireGuard mobile enrollment.
use crate::error::{Result, WireGuardError};
use base64::Engine;
use image::Luma;
use qrcode::QrCode;
use qrcode::render::svg;
/// Generate SVG string for a WireGuard configuration.
pub fn generate_qr_svg(content: &str) -> Result<String> {
let code = QrCode::new(content.as_bytes())
.map_err(|e| WireGuardError::Qr(format!("Failed to generate QR code: {e}")))?;
let image = code
.render::<svg::Color>()
.min_dimensions(256, 256)
.dark_color(svg::Color("#000000"))
.light_color(svg::Color("#ffffff"))
.build();
Ok(image)
}
/// Generate PNG bytes for a WireGuard configuration.
pub fn generate_qr_png_bytes(content: &str) -> Result<Vec<u8>> {
let code = QrCode::new(content.as_bytes())
.map_err(|e| WireGuardError::Qr(format!("Failed to generate QR code: {e}")))?;
let image = code.render::<Luma<u8>>().min_dimensions(300, 300).build();
let mut buffer = std::io::Cursor::new(Vec::new());
image
.write_to(&mut buffer, image::ImageFormat::Png)
.map_err(|e| WireGuardError::Qr(format!("Failed to encode QR PNG: {e}")))?;
Ok(buffer.into_inner())
}
/// Generate base64-encoded PNG string for a WireGuard configuration.
pub fn generate_qr_base64(content: &str) -> Result<String> {
let png_bytes = generate_qr_png_bytes(content)?;
Ok(base64::engine::general_purpose::STANDARD.encode(png_bytes))
}
/// Generate base64 Data URL (data:image/png;base64,...) for embedding in HTML / UI.
pub fn generate_qr_data_url(content: &str) -> Result<String> {
let b64 = generate_qr_base64(content)?;
Ok(format!("data:image/png;base64,{b64}"))
}
/// Generate ASCII QR code for direct CLI rendering.
pub fn generate_qr_ascii(content: &str) -> Result<String> {
let code = QrCode::new(content.as_bytes())
.map_err(|e| WireGuardError::Qr(format!("Failed to generate QR code: {e}")))?;
let string = code
.render::<qrcode::render::unicode::Dense1x2>()
.dark_color(qrcode::render::unicode::Dense1x2::Dark)
.light_color(qrcode::render::unicode::Dense1x2::Light)
.build();
Ok(string)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_qr_generation_svg_png_ascii() {
let text = "[Interface]\nPrivateKey = aaaa\n";
let svg = generate_qr_svg(text).expect("svg");
assert!(svg.contains("<svg"));
let png = generate_qr_png_bytes(text).expect("png");
assert!(!png.is_empty());
assert_eq!(&png[1..4], b"PNG");
let data_url = generate_qr_data_url(text).expect("data url");
assert!(data_url.starts_with("data:image/png;base64,"));
let ascii = generate_qr_ascii(text).expect("ascii");
assert!(!ascii.is_empty());
}
}
@@ -0,0 +1,196 @@
//! Integration tests for Phase 4 WireGuard Engine, Client Config Generator, and QR Code system.
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_wireguard::{
ClientConfigBuilder, SimulatedWireGuardEngine, WireGuardEngine, generate_qr_ascii,
generate_qr_data_url, generate_qr_png_bytes, generate_qr_svg,
};
use std::str::FromStr;
use uuid::Uuid;
#[tokio::test]
async fn test_wireguard_engine_lifecycle_and_telemetry() {
let engine = SimulatedWireGuardEngine::new();
let (srv_priv, srv_pub) = generate_keypair();
let (peer1_priv, peer1_pub) = generate_keypair();
let (peer2_priv, peer2_pub) = generate_keypair();
let now = Utc::now().naive_utc();
let iface = Interface {
id: Uuid::new_v4(),
name: "wg0".to_string(),
private_key: srv_priv,
public_key: srv_pub.clone(),
listen_port: 51820,
address_v4: IpNet::from_str("10.0.0.1/24").unwrap(),
address_v6: None,
mtu: Some(1420),
dns: Some("1.1.1.1".to_string()),
enabled: true,
pre_up: None,
post_up: None,
pre_down: None,
post_down: None,
created_at: now,
updated_at: now,
};
let p1 = Peer {
id: Uuid::new_v4(),
interface_id: iface.id,
name: "laptop-user".to_string(),
peer_type: PeerType::RoadWarrior,
state: PeerState::Active,
public_key: peer1_pub.clone(),
private_key: Some(peer1_priv),
preshared_key: Some(generate_preshared_key()),
endpoint: Some("198.51.100.2:45000".to_string()),
allowed_ips: "10.0.0.2/32".to_string(),
server_allowed_ips: None,
address_v4: Some(IpNet::from_str("10.0.0.2/32").unwrap()),
address_v6: None,
dns: None,
mtu: None,
persistent_keepalive: Some(25),
profile: PeerProfile::FullTunnel,
expires_at: None,
last_handshake_at: None,
created_at: now,
updated_at: now,
};
let p2 = Peer {
id: Uuid::new_v4(),
interface_id: iface.id,
name: "phone-user".to_string(),
peer_type: PeerType::RoadWarrior,
state: PeerState::Disabled, // disabled peer should NOT be in active kernel set
public_key: peer2_pub.clone(),
private_key: Some(peer2_priv),
preshared_key: None,
endpoint: None,
allowed_ips: "10.0.0.3/32".to_string(),
server_allowed_ips: None,
address_v4: Some(IpNet::from_str("10.0.0.3/32").unwrap()),
address_v6: None,
dns: None,
mtu: None,
persistent_keepalive: None,
profile: PeerProfile::FullTunnel,
expires_at: None,
last_handshake_at: None,
created_at: now,
updated_at: now,
};
// 1. Sync interface with engine
engine
.sync_interface(&iface, &[p1.clone(), p2.clone()])
.await
.expect("sync interface");
// 2. Read live interface stats
let stats = engine
.get_interface_stats("wg0")
.await
.expect("get stats")
.expect("interface exists");
assert_eq!(stats.name, "wg0");
assert_eq!(stats.public_key, srv_pub.as_str());
assert_eq!(
stats.peers.len(),
1,
"only active peers should be synchronized"
);
assert_eq!(stats.peers[0].public_key, peer1_pub.as_str());
// 3. Simulate peer traffic activity
engine
.simulate_peer_activity("wg0", peer1_pub.as_str(), 1024, 2048)
.await
.expect("simulate activity");
let updated_stats = engine.get_interface_stats("wg0").await.unwrap().unwrap();
assert_eq!(updated_stats.peers[0].rx_bytes, 1024);
assert_eq!(updated_stats.peers[0].tx_bytes, 2048);
assert!(updated_stats.peers[0].last_handshake_at.is_some());
// 4. Delete interface
engine.delete_interface("wg0").await.expect("delete");
let after_del = engine.get_interface_stats("wg0").await.expect("query");
assert!(after_del.is_none());
}
#[test]
fn test_client_config_and_qr_codes() {
let (srv_priv, srv_pub) = generate_keypair();
let (peer_priv, peer_pub) = generate_keypair();
let psk = generate_preshared_key();
let now = Utc::now().naive_utc();
let iface = Interface {
id: Uuid::new_v4(),
name: "wg0".to_string(),
private_key: srv_priv,
public_key: srv_pub,
listen_port: 51820,
address_v4: IpNet::from_str("10.0.0.1/24").unwrap(),
address_v6: None,
mtu: Some(1420),
dns: Some("1.1.1.1".to_string()),
enabled: true,
pre_up: None,
post_up: None,
pre_down: None,
post_down: None,
created_at: now,
updated_at: now,
};
let peer = Peer {
id: Uuid::new_v4(),
interface_id: iface.id,
name: "ipad-charlie".to_string(),
peer_type: PeerType::RoadWarrior,
state: PeerState::Active,
public_key: peer_pub,
private_key: Some(peer_priv),
preshared_key: Some(psk),
endpoint: None,
allowed_ips: "10.0.0.5/32".to_string(),
server_allowed_ips: None,
address_v4: Some(IpNet::from_str("10.0.0.5/32").unwrap()),
address_v6: None,
dns: None,
mtu: None,
persistent_keepalive: Some(25),
profile: PeerProfile::FullTunnel,
expires_at: None,
last_handshake_at: None,
created_at: now,
updated_at: now,
};
let config_str = ClientConfigBuilder::build(&peer, &iface, "203.0.113.10").unwrap();
assert!(config_str.contains("[Interface]"));
assert!(config_str.contains("[Peer]"));
assert!(config_str.contains("Endpoint = 203.0.113.10:51820"));
// Verify all QR code output formats
let svg = generate_qr_svg(&config_str).unwrap();
assert!(svg.contains("<svg"));
let png_bytes = generate_qr_png_bytes(&config_str).unwrap();
assert!(!png_bytes.is_empty());
assert_eq!(&png_bytes[1..4], b"PNG");
let data_url = generate_qr_data_url(&config_str).unwrap();
assert!(data_url.starts_with("data:image/png;base64,"));
let ascii = generate_qr_ascii(&config_str).unwrap();
assert!(!ascii.is_empty());
}