feat: complete nx9-wg v0.8.0 platform

This commit is contained in:
thakares committed 2026-08-17 14:25:45 +05:30
1 parent c75e5c4e71
commit c8a9b7cde6
52 files changed
+7751 -725

No files matched your search

+10
View File
@@ -19,5 +19,15 @@ qrcode.workspace = true
image.workspace = true
async-trait = "0.1"
[target.'cfg(target_os = "linux")'.dependencies]
rtnetlink = { workspace = true }
genetlink = { workspace = true }
netlink-packet-wireguard = { workspace = true }
netlink-packet-core = { workspace = true }
netlink-packet-generic = { workspace = true }
netlink-proto = { workspace = true }
netlink-sys = { workspace = true }
futures = { workspace = true }
[dev-dependencies]
tempfile.workspace = true
+17 -12
View File
@@ -143,12 +143,25 @@ impl WireGuardEngine for SimulatedWireGuardEngine {
}
}
/// Linux Native WireGuard Engine using kernel netlink / interfaces.
// ── Native Linux WireGuard Engine ─────────────────────────────────────────────
//
// On Linux: the real implementation lives in native_linux.rs and uses
// RTNETLINK + WireGuard Generic Netlink to communicate with the kernel.
//
// On non-Linux platforms: a thin simulation wrapper is provided so that
// the workspace remains portable and tests remain functional.
#[cfg(target_os = "linux")]
pub use crate::native_linux::NativeLinuxWireGuardEngine;
/// Non-Linux fallback: NativeLinuxWireGuardEngine delegates to simulation.
#[cfg(not(target_os = "linux"))]
#[derive(Debug, Clone, Default)]
pub struct NativeLinuxWireGuardEngine {
simulated_fallback: SimulatedWireGuardEngine,
}
#[cfg(not(target_os = "linux"))]
impl NativeLinuxWireGuardEngine {
pub fn new() -> Self {
Self {
@@ -156,24 +169,16 @@ impl NativeLinuxWireGuardEngine {
}
}
/// Check if Linux kernel WireGuard module / interface support is available.
/// Check if Linux kernel WireGuard 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
}
false
}
}
#[cfg(not(target_os = "linux"))]
#[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
+15
View File
@@ -24,6 +24,21 @@ pub enum WireGuardError {
#[error("permission denied: {0}")]
PermissionDenied(String),
#[error("interface not found: {0}")]
InterfaceNotFound(String),
#[error("wrong interface type: expected wireguard, found {0}")]
WrongInterfaceType(String),
#[error("unsupported: {0}")]
Unsupported(String),
#[error("invalid endpoint: {0}")]
InvalidEndpoint(String),
#[error("invalid allowed IP: {0}")]
InvalidAllowedIp(String),
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
+2
View File
@@ -3,6 +3,8 @@
pub mod config_builder;
pub mod engine;
pub mod error;
#[cfg(target_os = "linux")]
mod native_linux;
pub mod qr;
pub use config_builder::ClientConfigBuilder;
+540
View File
@@ -0,0 +1,540 @@
//! Native Linux WireGuard engine using RTNETLINK and WireGuard Generic Netlink.
//!
//! This module communicates directly with the Linux kernel to manage WireGuard
//! interfaces. It uses:
//!
//! - **RTNETLINK** for network link lifecycle (create, delete, list interfaces)
//! - **WireGuard Generic Netlink** for device configuration and telemetry
//!
//! No external commands (wg, ip, wg-quick, nft, sysctl) are ever executed.
use crate::engine::{LiveInterfaceStats, LivePeerStats, WireGuardEngine};
use crate::error::{Result, WireGuardError};
use base64::Engine as _;
use chrono::NaiveDateTime;
use futures::stream::{StreamExt, TryStreamExt};
use genetlink::GenetlinkHandle;
use ipnet::IpNet;
use netlink_packet_core::{NLM_F_ACK, NLM_F_DUMP, NLM_F_REQUEST, NetlinkMessage, NetlinkPayload};
use netlink_packet_generic::GenlMessage;
use netlink_packet_wireguard::{
WireguardAddressFamily, WireguardAllowedIp, WireguardAllowedIpAttr, WireguardAttribute,
WireguardCmd, WireguardDeviceFlags, WireguardMessage, WireguardPeer, WireguardPeerAttribute,
WireguardPeerFlags,
};
use nx9_wg_core::types::wireguard::{Interface, Peer, PeerState};
use rtnetlink::LinkWireguard;
use rtnetlink::packet_route::link::{InfoKind, LinkAttribute, LinkInfo};
use std::net::{IpAddr, SocketAddr};
/// Linux Native WireGuard Engine using kernel RTNETLINK and Generic Netlink.
#[derive(Debug, Clone, Default)]
pub struct NativeLinuxWireGuardEngine;
impl NativeLinuxWireGuardEngine {
pub fn new() -> Self {
Self
}
/// Check if Linux kernel WireGuard module and Generic Netlink support is available.
pub fn is_supported() -> bool {
// Check for the WireGuard kernel module or network dev procfs
std::path::Path::new("/sys/module/wireguard").exists()
|| std::path::Path::new("/proc/net/dev").exists()
}
}
// ── RTNETLINK Interface Lifecycle ─────────────────────────────────────────────
/// Create a new RTNETLINK connection and return the handle.
async fn rtnetlink_handle() -> Result<(rtnetlink::Handle, tokio::task::JoinHandle<()>)> {
let (connection, handle, _) = rtnetlink::new_connection().map_err(|e| {
WireGuardError::Netlink(format!("failed to create rtnetlink connection: {e}"))
})?;
let join = tokio::spawn(connection);
Ok((handle, join))
}
/// Ensure a WireGuard interface exists with the given name.
///
/// - If the interface already exists and is a WireGuard link, this is a no-op.
/// - If the interface already exists but is NOT a WireGuard link, returns an error.
/// - If the interface does not exist, it is created as a WireGuard link and brought up.
async fn ensure_link(name: &str) -> Result<()> {
let (handle, _conn_task) = rtnetlink_handle().await?;
// Try to find existing interface by name
let mut links = handle.link().get().match_name(name.to_string()).execute();
match links.try_next().await {
Ok(Some(link)) => {
let mut is_wireguard = false;
for nla in &link.attributes {
if let LinkAttribute::LinkInfo(infos) = nla {
for info in infos {
if let LinkInfo::Kind(InfoKind::Wireguard) = info {
is_wireguard = true;
}
}
}
}
if is_wireguard {
tracing::debug!(interface = %name, "WireGuard interface already exists");
Ok(())
} else {
Err(WireGuardError::WrongInterfaceType(format!(
"interface '{name}' exists but is not a WireGuard interface"
)))
}
}
Ok(None) | Err(_) => {
// Interface does not exist — create it and bring it up
tracing::info!(interface = %name, "Creating WireGuard interface via RTNETLINK");
let add_msg = LinkWireguard::new(name).up().build();
handle.link().add(add_msg).execute().await.map_err(|e| {
let msg = format!("{e}");
if msg.contains("permission")
|| msg.contains("EPERM")
|| msg.contains("Operation not permitted")
{
WireGuardError::PermissionDenied(format!(
"insufficient privileges to create WireGuard interface '{name}': {e}"
))
} else {
WireGuardError::Netlink(format!(
"failed to create WireGuard interface '{name}': {e}"
))
}
})?;
tracing::info!(interface = %name, "WireGuard interface created and brought up");
Ok(())
}
}
}
/// Delete a WireGuard interface by name.
async fn delete_link(name: &str) -> Result<()> {
let (handle, _conn_task) = rtnetlink_handle().await?;
let mut links = handle.link().get().match_name(name.to_string()).execute();
match links.try_next().await {
Ok(Some(link)) => {
let index = link.header.index;
handle.link().del(index).execute().await.map_err(|e| {
WireGuardError::Netlink(format!(
"failed to delete interface '{name}' (index {index}): {e}"
))
})?;
tracing::info!(interface = %name, "WireGuard interface deleted via RTNETLINK");
Ok(())
}
Ok(None) => {
tracing::debug!(interface = %name, "Interface not found for deletion");
Ok(())
}
Err(e) => Err(WireGuardError::Netlink(format!(
"failed to look up interface '{name}': {e}"
))),
}
}
/// List all WireGuard interface names using RTNETLINK link dump.
async fn list_wireguard_links() -> Result<Vec<String>> {
let (handle, _conn_task) = rtnetlink_handle().await?;
let mut links = handle.link().get().execute();
let mut wg_names = Vec::new();
while let Some(link) = links
.try_next()
.await
.map_err(|e| WireGuardError::Netlink(format!("failed to dump links: {e}")))?
{
let mut name = None;
let mut is_wireguard = false;
for nla in &link.attributes {
match nla {
LinkAttribute::IfName(n) => name = Some(n.clone()),
LinkAttribute::LinkInfo(infos) => {
for info in infos {
if let LinkInfo::Kind(InfoKind::Wireguard) = info {
is_wireguard = true;
}
}
}
_ => {}
}
}
if let (true, Some(n)) = (is_wireguard, name) {
wg_names.push(n);
}
}
Ok(wg_names)
}
// ── WireGuard Generic Netlink Operations ─────────────────────────────────────
/// Create a WireGuard Generic Netlink connection.
async fn wireguard_genl_handle() -> Result<(GenetlinkHandle, tokio::task::JoinHandle<()>)> {
let (connection, handle, _) = genetlink::new_connection().map_err(|e| {
WireGuardError::Netlink(format!("failed to create genetlink connection: {e}"))
})?;
let join = tokio::spawn(connection);
Ok((handle, join))
}
/// Configure a WireGuard device via Generic Netlink SET_DEVICE.
///
/// Sets the private key, listen port, and synchronizes the active peer set.
/// Uses `WireguardDeviceFlags::ReplacePeers` to atomically replace all peers.
async fn configure_device(interface: &Interface, peers: &[Peer]) -> Result<()> {
let (mut handle, _conn_task) = wireguard_genl_handle().await?;
// Decode the private key from base64 to 32 bytes
let private_key_bytes = decode_base64_key(interface.private_key.as_str())
.map_err(|e| WireGuardError::Key(format!("invalid interface private key: {e}")))?;
// Build the device attributes
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),
];
// Build peer configurations for active peers only
let mut wg_peers = Vec::new();
for peer in peers.iter().filter(|p| p.state == PeerState::Active) {
let mut peer_attrs: Vec<WireguardPeerAttribute> = Vec::new();
// Public key (required)
let pub_key_bytes = decode_base64_key(peer.public_key.as_str())
.map_err(|e| WireGuardError::Key(format!("invalid peer public key: {e}")))?;
peer_attrs.push(WireguardPeerAttribute::PublicKey(pub_key_bytes));
// Preshared key (optional)
if let Some(ref psk) = peer.preshared_key {
let psk_bytes = decode_base64_key(psk.as_str())
.map_err(|e| WireGuardError::Key(format!("invalid peer preshared key: {e}")))?;
peer_attrs.push(WireguardPeerAttribute::PresharedKey(psk_bytes));
}
// Endpoint (optional)
if let Some(ref endpoint_str) = peer.endpoint {
let endpoint = parse_endpoint(endpoint_str)?;
peer_attrs.push(WireguardPeerAttribute::Endpoint(endpoint));
}
// Persistent keepalive (optional)
if let Some(keepalive) = peer.persistent_keepalive {
peer_attrs.push(WireguardPeerAttribute::PersistentKeepalive(keepalive));
}
// Allowed IPs
let allowed_ips = parse_allowed_ips(&peer.allowed_ips)?;
if !allowed_ips.is_empty() {
peer_attrs.push(WireguardPeerAttribute::Flags(
WireguardPeerFlags::ReplaceAllowedIps,
));
peer_attrs.push(WireguardPeerAttribute::AllowedIps(allowed_ips));
}
wg_peers.push(WireguardPeer(peer_attrs));
}
device_attrs.push(WireguardAttribute::Peers(wg_peers));
// Build and send the SET_DEVICE message
let genlmsg = GenlMessage::from_payload(WireguardMessage {
cmd: WireguardCmd::SetDevice,
attributes: device_attrs,
});
let mut nlmsg = NetlinkMessage::from(genlmsg);
nlmsg.header.flags = NLM_F_REQUEST | NLM_F_ACK;
nlmsg.finalize();
let mut response = handle.request(nlmsg).await.map_err(|e| {
let msg = format!("{e}");
if msg.contains("not found") || msg.contains("No such") {
WireGuardError::Unsupported(
"WireGuard Generic Netlink family not available — is the wireguard kernel module loaded?".to_string(),
)
} else {
WireGuardError::Netlink(format!("failed to send WireGuard SET_DEVICE: {e}"))
}
})?;
// Check for errors in the response stream
while let Some(res) = response.next().await {
let msg = res.map_err(|e| WireGuardError::Netlink(format!("decode error: {e}")))?;
if let NetlinkPayload::Error(err) = msg.payload
&& let Some(code) = err.code
{
let code_val = code.get();
if code_val == -1 {
return Err(WireGuardError::PermissionDenied(
"insufficient privileges to configure WireGuard device".to_string(),
));
}
return Err(WireGuardError::Netlink(format!(
"kernel rejected WireGuard device configuration (errno={code_val})"
)));
}
}
tracing::debug!(
interface = %interface.name,
active_peers = peers.iter().filter(|p| p.state == PeerState::Active).count(),
"WireGuard device configured via Generic Netlink"
);
Ok(())
}
/// Query a WireGuard device via Generic Netlink GET_DEVICE and return live stats.
async fn query_device(name: &str) -> Result<Option<LiveInterfaceStats>> {
let (mut handle, _conn_task) = wireguard_genl_handle().await?;
let genlmsg = GenlMessage::from_payload(WireguardMessage {
cmd: WireguardCmd::GetDevice,
attributes: vec![WireguardAttribute::IfName(name.to_string())],
});
let mut nlmsg = NetlinkMessage::from(genlmsg);
nlmsg.header.flags = NLM_F_REQUEST | NLM_F_DUMP;
nlmsg.finalize();
let mut response = handle.request(nlmsg).await.map_err(|e| {
let msg = format!("{e}");
if msg.contains("No such device") || msg.contains("ENODEV") {
WireGuardError::InterfaceNotFound(format!("interface '{name}' not found"))
} else if msg.contains("not found") || msg.contains("No such") {
WireGuardError::Unsupported(
"WireGuard Generic Netlink family not available — is the wireguard kernel module loaded?".to_string(),
)
} else {
WireGuardError::Netlink(format!("failed to query WireGuard device '{name}': {e}"))
}
})?;
let mut public_key = String::new();
let mut listen_port: u16 = 0;
let mut fwmark: u32 = 0;
let mut live_peers: Vec<LivePeerStats> = Vec::new();
let mut found = false;
while let Some(res) = response.next().await {
let msg = res.map_err(|e| WireGuardError::Netlink(format!("decode error: {e}")))?;
match msg.payload {
NetlinkPayload::Error(err) => {
if let Some(code) = err.code {
let code_val = code.get();
// ENODEV = -19 means device not found
if code_val == -19 {
return Ok(None);
}
if code_val == -1 {
return Err(WireGuardError::PermissionDenied(
"insufficient privileges to query WireGuard device".to_string(),
));
}
return Err(WireGuardError::Netlink(format!(
"kernel error querying WireGuard device (errno={code_val})"
)));
}
}
NetlinkPayload::InnerMessage(genl) => {
found = true;
for attr in genl.payload.attributes {
match attr {
WireguardAttribute::PublicKey(key) => {
public_key = base64::engine::general_purpose::STANDARD.encode(key);
}
WireguardAttribute::ListenPort(port) => listen_port = port,
WireguardAttribute::Fwmark(fw) => fwmark = fw,
WireguardAttribute::Peers(peers) => {
for peer in peers {
let mut peer_pubkey = String::new();
let mut peer_endpoint: Option<String> = None;
let mut rx_bytes: u64 = 0;
let mut tx_bytes: u64 = 0;
let mut last_handshake: Option<NaiveDateTime> = None;
let mut allowed_ips_strs: Vec<String> = Vec::new();
let mut persistent_keepalive: Option<u16> = None;
for attr in peer.0 {
match attr {
WireguardPeerAttribute::PublicKey(key) => {
peer_pubkey = base64::engine::general_purpose::STANDARD
.encode(key);
}
WireguardPeerAttribute::Endpoint(ep) => {
peer_endpoint = Some(format!("{ep}"));
}
WireguardPeerAttribute::RxBytes(rx) => rx_bytes = rx,
WireguardPeerAttribute::TxBytes(tx) => tx_bytes = tx,
WireguardPeerAttribute::LastHandshake(ts) => {
let secs = ts.seconds;
let nsecs = ts.nano_seconds;
if secs > 0 {
last_handshake = chrono::DateTime::from_timestamp(
secs,
nsecs.clamp(0, 999_999_999) as u32,
)
.map(|dt| dt.naive_utc());
}
}
WireguardPeerAttribute::AllowedIps(ips) => {
for ip_entry in ips {
let mut addr: Option<IpAddr> = None;
let mut prefix: u8 = 0;
for ip_attr in ip_entry.0 {
match ip_attr {
WireguardAllowedIpAttr::IpAddr(a) => {
addr = Some(a);
}
WireguardAllowedIpAttr::Cidr(c) => {
prefix = c;
}
_ => {}
}
}
if let Some(a) = addr {
allowed_ips_strs.push(format!("{a}/{prefix}"));
}
}
}
WireguardPeerAttribute::PersistentKeepalive(ka)
if ka > 0 =>
{
persistent_keepalive = Some(ka);
}
_ => {}
}
}
live_peers.push(LivePeerStats {
public_key: peer_pubkey,
endpoint: peer_endpoint,
rx_bytes,
tx_bytes,
last_handshake_at: last_handshake,
allowed_ips: allowed_ips_strs,
persistent_keepalive,
});
}
}
_ => {}
}
}
}
_ => {}
}
}
if !found {
return Ok(None);
}
Ok(Some(LiveInterfaceStats {
name: name.to_string(),
public_key,
listen_port,
fwmark,
peers: live_peers,
}))
}
// ── Conversion Utilities ─────────────────────────────────────────────────────
/// Decode a base64-encoded WireGuard key into exactly 32 bytes.
fn decode_base64_key(b64: &str) -> std::result::Result<[u8; 32], String> {
let bytes = base64::engine::general_purpose::STANDARD
.decode(b64)
.map_err(|e| format!("invalid base64: {e}"))?;
if bytes.len() != 32 {
return Err(format!("key must be exactly 32 bytes, got {}", bytes.len()));
}
let mut arr = [0u8; 32];
arr.copy_from_slice(&bytes);
Ok(arr)
}
/// Parse a comma-separated list of CIDR addresses into WireGuard allowed-IP NLAs.
fn parse_allowed_ips(csv: &str) -> Result<Vec<WireguardAllowedIp>> {
let mut result = Vec::new();
for cidr_str in csv.split(',') {
let trimmed = cidr_str.trim();
if trimmed.is_empty() {
continue;
}
let net: IpNet = trimmed.parse().map_err(|e| {
WireGuardError::InvalidAllowedIp(format!("invalid CIDR '{trimmed}': {e}"))
})?;
let family = match net {
IpNet::V4(_) => WireguardAddressFamily::Ipv4,
IpNet::V6(_) => WireguardAddressFamily::Ipv6,
};
result.push(WireguardAllowedIp(vec![
WireguardAllowedIpAttr::Family(family),
WireguardAllowedIpAttr::IpAddr(net.addr()),
WireguardAllowedIpAttr::Cidr(net.prefix_len()),
]));
}
Ok(result)
}
/// Parse an endpoint string ("ip:port" or "[ipv6]:port") into a SocketAddr.
fn parse_endpoint(s: &str) -> Result<SocketAddr> {
if let Ok(addr) = s.parse::<SocketAddr>() {
return Ok(addr);
}
if let Some(idx) = s.rfind(':') {
let host = &s[..idx];
let port_str = &s[idx + 1..];
if let (Ok(ip), Ok(port)) = (host.parse::<IpAddr>(), port_str.parse::<u16>()) {
return Ok(SocketAddr::new(ip, port));
}
}
Err(WireGuardError::InvalidEndpoint(format!(
"cannot parse endpoint '{s}'"
)))
}
// ── WireGuardEngine Trait Implementation ─────────────────────────────────────
#[async_trait::async_trait]
impl WireGuardEngine for NativeLinuxWireGuardEngine {
async fn sync_interface(&self, interface: &Interface, peers: &[Peer]) -> Result<()> {
// 1. Ensure the WireGuard link exists
ensure_link(&interface.name).await?;
// 2. Configure the WireGuard device (private key, listen port, peers)
configure_device(interface, peers).await?;
tracing::info!(
interface = %interface.name,
active_peers = peers.iter().filter(|p| p.state == PeerState::Active).count(),
"WireGuard interface synchronized via native netlink"
);
Ok(())
}
async fn delete_interface(&self, name: &str) -> Result<()> {
delete_link(name).await
}
async fn get_interface_stats(&self, name: &str) -> Result<Option<LiveInterfaceStats>> {
query_device(name).await
}
async fn list_interfaces(&self) -> Result<Vec<String>> {
list_wireguard_links().await
}
}
@@ -0,0 +1,157 @@
//! Unit tests for native Linux WireGuard conversion utilities and error invariants.
//!
//! These tests verify CIDR parsing, endpoint parsing, key decoding, and peer
//! filtering without requiring CAP_NET_ADMIN or kernel mutation.
use base64::Engine as _;
use nx9_wg_core::types::wireguard::PeerState;
use nx9_wireguard::WireGuardError;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
#[test]
fn test_ipv4_cidr_parsing() {
let net: ipnet::IpNet = "10.0.0.2/32".parse().unwrap();
assert_eq!(net.addr(), IpAddr::V4(Ipv4Addr::new(10, 0, 0, 2)));
assert_eq!(net.prefix_len(), 32);
}
#[test]
fn test_ipv6_cidr_parsing() {
let net: ipnet::IpNet = "fd00::2/128".parse().unwrap();
assert!(net.addr().is_ipv6());
assert_eq!(net.prefix_len(), 128);
}
#[test]
fn test_multiple_allowed_ips_parsing() {
let csv = "10.0.0.2/32, fd00::2/128";
let nets: Vec<ipnet::IpNet> = csv
.split(',')
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.map(|s| s.parse::<ipnet::IpNet>().unwrap())
.collect();
assert_eq!(nets.len(), 2);
assert!(nets[0].addr().is_ipv4());
assert!(nets[1].addr().is_ipv6());
}
#[test]
fn test_empty_allowed_ips() {
let csv = "";
let nets: Vec<ipnet::IpNet> = csv
.split(',')
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.filter_map(|s| s.parse::<ipnet::IpNet>().ok())
.collect();
assert!(nets.is_empty());
}
#[test]
fn test_invalid_cidr_rejected() {
let result = "invalid/cidr".parse::<ipnet::IpNet>();
assert!(result.is_err());
}
#[test]
fn test_ipv4_endpoint_parsing() {
let addr: SocketAddr = "198.51.100.2:45000".parse().unwrap();
assert_eq!(addr.ip(), IpAddr::V4(Ipv4Addr::new(198, 51, 100, 2)));
assert_eq!(addr.port(), 45000);
}
#[test]
fn test_ipv6_endpoint_parsing() {
let addr: SocketAddr = "[2001:db8::1]:51820".parse().unwrap();
assert!(addr.ip().is_ipv6());
assert_eq!(addr.port(), 51820);
}
#[test]
fn test_invalid_endpoint_rejected() {
let result = "not-an-endpoint".parse::<SocketAddr>();
assert!(result.is_err());
}
#[test]
fn test_base64_key_decode_valid() {
let key_bytes = [0xAAu8; 32];
let b64 = base64::engine::general_purpose::STANDARD.encode(key_bytes);
let decoded = base64::engine::general_purpose::STANDARD
.decode(&b64)
.unwrap();
assert_eq!(decoded.len(), 32);
let mut arr = [0u8; 32];
arr.copy_from_slice(&decoded);
assert_eq!(arr, key_bytes);
}
#[test]
fn test_base64_key_decode_wrong_length() {
let short_key = [0xBBu8; 16];
let b64 = base64::engine::general_purpose::STANDARD.encode(short_key);
let decoded = base64::engine::general_purpose::STANDARD
.decode(&b64)
.unwrap();
assert_ne!(decoded.len(), 32);
}
#[test]
fn test_base64_key_decode_invalid_base64() {
let result = base64::engine::general_purpose::STANDARD.decode("not!valid!base64!!!");
assert!(result.is_err());
}
#[test]
fn test_peer_state_filtering() {
let states = [
PeerState::Active,
PeerState::Disabled,
PeerState::Revoked,
PeerState::Expired,
];
let active_count = states.iter().filter(|s| **s == PeerState::Active).count();
assert_eq!(active_count, 1, "only Active peers should be synchronized");
}
#[test]
fn test_error_display_no_key_leakage() {
let err = WireGuardError::Key("invalid base64".to_string());
let display = format!("{err}");
assert!(!display.contains("secret"));
assert!(!display.contains("private"));
assert!(display.contains("invalid base64"));
}
#[test]
fn test_error_variants_exist() {
let _ = format!("{}", WireGuardError::InterfaceNotFound("wg0".into()));
let _ = format!("{}", WireGuardError::WrongInterfaceType("eth0".into()));
let _ = format!("{}", WireGuardError::Unsupported("no kernel module".into()));
let _ = format!("{}", WireGuardError::InvalidEndpoint("bad:ep".into()));
let _ = format!("{}", WireGuardError::InvalidAllowedIp("bad/cidr".into()));
}
#[test]
fn test_prefix_length_preservation() {
let cases = [
("10.0.0.0/8", 8),
("10.0.0.0/16", 16),
("10.0.0.0/24", 24),
("10.0.0.1/32", 32),
("fd00::/64", 64),
("fd00::1/128", 128),
("0.0.0.0/0", 0),
("::/0", 0),
];
for (cidr, expected_prefix) in cases {
let net: ipnet::IpNet = cidr.parse().unwrap();
assert_eq!(
net.prefix_len(),
expected_prefix,
"prefix mismatch for {cidr}"
);
}
}