use base64::{engine::general_purpose::STANDARD, Engine as _};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
use std::time::Duration;
use tokio::process::Command;
use x25519_dalek::{PublicKey, StaticSecret};
use zlayer_overlay::config::{OverlayConfig, PeerInfo};
use zlayer_overlay::transport::OverlayTransport;
#[tokio::test]
async fn test_native_key_generation_produces_valid_keys() {
let (private_key, public_key) = OverlayTransport::generate_keys()
.await
.expect("generate_keys should succeed");
assert_eq!(
private_key.len(),
44,
"Private key should be 44 characters (32 bytes base64-encoded), got {}",
private_key.len()
);
assert_eq!(
public_key.len(),
44,
"Public key should be 44 characters (32 bytes base64-encoded), got {}",
public_key.len()
);
let priv_bytes = STANDARD
.decode(&private_key)
.expect("Private key must be valid base64");
let pub_bytes = STANDARD
.decode(&public_key)
.expect("Public key must be valid base64");
assert_eq!(
priv_bytes.len(),
32,
"Decoded private key must be exactly 32 bytes"
);
assert_eq!(
pub_bytes.len(),
32,
"Decoded public key must be exactly 32 bytes"
);
let secret =
StaticSecret::from(<[u8; 32]>::try_from(priv_bytes.as_slice()).expect("32-byte slice"));
let expected_public = PublicKey::from(&secret);
assert_eq!(
pub_bytes.as_slice(),
expected_public.as_bytes(),
"Public key must be the x25519 derivation of the private key"
);
let (private_key_2, _) = OverlayTransport::generate_keys()
.await
.expect("second generate_keys should succeed");
assert_ne!(
private_key, private_key_2,
"Two consecutive key generations must produce distinct private keys"
);
}
#[tokio::test]
async fn test_native_keys_compatible_with_wg_tool() {
let wg_available = Command::new("which")
.arg("wg")
.output()
.await
.is_ok_and(|o| o.status.success());
if !wg_available {
eprintln!("SKIP: `wg` binary not found; skipping key compatibility test (wg is optional)");
return;
}
let (native_priv, native_pub) = OverlayTransport::generate_keys()
.await
.expect("native generate_keys should succeed");
let wg_genkey_output = Command::new("wg")
.arg("genkey")
.output()
.await
.expect("wg genkey should execute");
assert!(
wg_genkey_output.status.success(),
"wg genkey failed: {}",
String::from_utf8_lossy(&wg_genkey_output.stderr)
);
let wg_priv = String::from_utf8(wg_genkey_output.stdout)
.expect("wg genkey output should be valid UTF-8")
.trim()
.to_string();
let wg_pubkey_output = {
let mut child = std::process::Command::new("wg")
.arg("pubkey")
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.expect("wg pubkey should spawn");
{
use std::io::Write;
let stdin = child.stdin.as_mut().expect("stdin should be available");
stdin
.write_all(wg_priv.as_bytes())
.expect("writing to stdin should succeed");
}
child.wait_with_output().expect("wg pubkey should complete")
};
assert!(
wg_pubkey_output.status.success(),
"wg pubkey failed: {}",
String::from_utf8_lossy(&wg_pubkey_output.stderr)
);
let wg_pub = String::from_utf8(wg_pubkey_output.stdout)
.expect("wg pubkey output should be valid UTF-8")
.trim()
.to_string();
assert_eq!(
native_priv.len(),
wg_priv.len(),
"Native and wg private keys must have the same length"
);
assert_eq!(
native_pub.len(),
wg_pub.len(),
"Native and wg public keys must have the same length"
);
let native_priv_bytes = STANDARD
.decode(&native_priv)
.expect("native private key must be valid base64");
let wg_priv_bytes = STANDARD
.decode(&wg_priv)
.expect("wg private key must be valid base64");
assert_eq!(native_priv_bytes.len(), 32);
assert_eq!(wg_priv_bytes.len(), 32);
let native_pub_bytes = STANDARD
.decode(&native_pub)
.expect("native public key must be valid base64");
let wg_pub_bytes = STANDARD
.decode(&wg_pub)
.expect("wg public key must be valid base64");
assert_eq!(native_pub_bytes.len(), 32);
assert_eq!(wg_pub_bytes.len(), 32);
let wg_secret =
StaticSecret::from(<[u8; 32]>::try_from(wg_priv_bytes.as_slice()).expect("32-byte slice"));
let derived_pub = PublicKey::from(&wg_secret);
assert_eq!(
derived_pub.as_bytes(),
wg_pub_bytes.as_slice(),
"x25519-dalek derivation of the wg-generated private key must match wg pubkey output"
);
}
#[tokio::test]
async fn test_overlay_config_and_peer_config_format() {
let (private_key, public_key) = OverlayTransport::generate_keys()
.await
.expect("key generation should succeed");
let config = OverlayConfig {
local_endpoint: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 10)), 51820),
private_key: private_key.clone(),
public_key: public_key.clone(),
overlay_cidr: "10.200.0.1/16".to_string(),
cluster_cidr: None,
peer_discovery_interval: Duration::from_secs(30),
#[cfg(feature = "nat")]
nat: zlayer_overlay::nat::NatConfig::default(),
};
assert_eq!(config.local_endpoint.port(), 51820);
assert_eq!(config.overlay_cidr, "10.200.0.1/16");
assert_eq!(config.private_key, private_key);
assert_eq!(config.public_key, public_key);
let peer = PeerInfo::new(
public_key.clone(),
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 20)), 51820),
"10.200.0.2/32",
Duration::from_secs(25),
);
let peer_config = peer.to_peer_config();
assert!(
peer_config.contains("[Peer]"),
"Peer config must contain [Peer] section header"
);
assert!(
peer_config.contains(&format!("PublicKey = {public_key}")),
"Peer config must contain the correct public key"
);
assert!(
peer_config.contains("Endpoint = 192.168.1.20:51820"),
"Peer config must contain the correct endpoint"
);
assert!(
peer_config.contains("AllowedIPs = 10.200.0.2/32"),
"Peer config must contain the correct allowed IPs"
);
assert!(
peer_config.contains("PersistentKeepalive = 25"),
"Peer config must contain the correct keepalive interval"
);
let full_config = format!(
"[Interface]\nPrivateKey = {}\nListenPort = {}\n{}",
config.private_key,
config.local_endpoint.port(),
peer_config,
);
assert!(
full_config.starts_with("[Interface]"),
"Full config must start with [Interface] section"
);
assert!(
full_config.contains(&format!("PrivateKey = {private_key}")),
"Full config must contain the private key"
);
assert!(
full_config.contains("ListenPort = 51820"),
"Full config must contain the listen port"
);
assert!(
full_config.contains("[Peer]"),
"Full config must contain [Peer] section"
);
}
#[tokio::test]
#[ignore = "requires root or CAP_NET_ADMIN"]
async fn test_overlay_interface_lifecycle() {
let iface_name = "wg-test-life";
let (private_key, public_key) = OverlayTransport::generate_keys()
.await
.expect("key generation should succeed");
let config = OverlayConfig {
local_endpoint: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 51830),
private_key,
public_key,
overlay_cidr: "10.250.0.1/24".to_string(),
cluster_cidr: None,
peer_discovery_interval: Duration::from_secs(30),
#[cfg(feature = "nat")]
nat: zlayer_overlay::nat::NatConfig::default(),
};
let mut manager = OverlayTransport::new(config, iface_name.to_string());
let _ = Command::new("ip")
.args(["link", "del", "dev", iface_name])
.output()
.await;
manager
.create_interface()
.await
.expect("create_interface should succeed");
let link_output = Command::new("ip")
.args(["link", "show", "dev", iface_name])
.output()
.await
.expect("ip link show should execute");
assert!(
link_output.status.success(),
"Interface {} should exist after create_interface, stderr: {}",
iface_name,
String::from_utf8_lossy(&link_output.stderr)
);
let link_stdout = String::from_utf8_lossy(&link_output.stdout);
assert!(
link_stdout.contains(iface_name),
"ip link show output should contain the interface name"
);
manager
.configure(&[])
.await
.expect("configure_interface should succeed with no peers");
let link_output = Command::new("ip")
.args(["link", "show", "dev", iface_name])
.output()
.await
.expect("ip link show should execute");
let link_stdout = String::from_utf8_lossy(&link_output.stdout);
assert!(
link_stdout.contains("UP") || link_stdout.contains("up"),
"Interface should be UP after configure_interface, got: {link_stdout}",
);
let addr_output = Command::new("ip")
.args(["addr", "show", "dev", iface_name])
.output()
.await
.expect("ip addr show should execute");
let addr_stdout = String::from_utf8_lossy(&addr_output.stdout);
assert!(
addr_stdout.contains("10.250.0.1"),
"Interface should have overlay IP 10.250.0.1 assigned, got: {addr_stdout}",
);
manager.shutdown();
tokio::time::sleep(Duration::from_millis(200)).await;
let link_output = Command::new("ip")
.args(["link", "show", "dev", iface_name])
.output()
.await
.expect("ip link show should execute");
assert!(
!link_output.status.success(),
"Interface {iface_name} should no longer exist after shutdown",
);
}
#[tokio::test]
#[ignore = "requires root or CAP_NET_ADMIN"]
#[allow(clippy::too_many_lines)]
async fn test_dual_overlay_connectivity() {
let iface_a = "wg-test-a";
let iface_b = "wg-test-b";
let port_a: u16 = 51840;
let port_b: u16 = 51841;
let ip_a = "10.251.0.1";
let ip_b = "10.251.0.2";
let subnet = "/24";
let (priv_a, pub_a) = OverlayTransport::generate_keys()
.await
.expect("key generation for A should succeed");
let (priv_b, pub_b) = OverlayTransport::generate_keys()
.await
.expect("key generation for B should succeed");
let config_a = OverlayConfig {
local_endpoint: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port_a),
private_key: priv_a,
public_key: pub_a.clone(),
overlay_cidr: format!("{ip_a}{subnet}"),
cluster_cidr: None,
peer_discovery_interval: Duration::from_secs(30),
#[cfg(feature = "nat")]
nat: zlayer_overlay::nat::NatConfig::default(),
};
let config_b = OverlayConfig {
local_endpoint: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port_b),
private_key: priv_b,
public_key: pub_b.clone(),
overlay_cidr: format!("{ip_b}{subnet}"),
cluster_cidr: None,
peer_discovery_interval: Duration::from_secs(30),
#[cfg(feature = "nat")]
nat: zlayer_overlay::nat::NatConfig::default(),
};
let mut manager_a = OverlayTransport::new(config_a, iface_a.to_string());
let mut manager_b = OverlayTransport::new(config_b, iface_b.to_string());
let _ = Command::new("ip")
.args(["link", "del", "dev", iface_a])
.output()
.await;
let _ = Command::new("ip")
.args(["link", "del", "dev", iface_b])
.output()
.await;
manager_a
.create_interface()
.await
.expect("create_interface A should succeed");
manager_b
.create_interface()
.await
.expect("create_interface B should succeed");
let peer_b_for_a = PeerInfo::new(
pub_b.clone(),
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port_b),
&format!("{ip_b}/32"),
Duration::from_secs(25),
);
let peer_a_for_b = PeerInfo::new(
pub_a.clone(),
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port_a),
&format!("{ip_a}/32"),
Duration::from_secs(25),
);
manager_a
.configure(&[peer_b_for_a])
.await
.expect("configure_interface A with peer B should succeed");
manager_b
.configure(&[peer_a_for_b])
.await
.expect("configure_interface B with peer A should succeed");
tokio::time::sleep(Duration::from_millis(500)).await;
let ping_output = Command::new("ping")
.args([
"-c", "3", "-W", "5", "-I", iface_a, ip_b, ])
.output()
.await
.expect("ping command should execute");
let ping_stdout = String::from_utf8_lossy(&ping_output.stdout);
let ping_stderr = String::from_utf8_lossy(&ping_output.stderr);
assert!(
ping_output.status.success(),
"Ping from {iface_a} ({ip_a}) to {iface_b} ({ip_b}) should succeed.\nstdout: {ping_stdout}\nstderr: {ping_stderr}",
);
assert!(
!ping_stdout.contains("100% packet loss"),
"Ping should not have 100% packet loss.\nstdout: {ping_stdout}",
);
let ping_reverse = Command::new("ping")
.args(["-c", "3", "-W", "5", "-I", iface_b, ip_a])
.output()
.await
.expect("reverse ping should execute");
assert!(
ping_reverse.status.success(),
"Reverse ping from {} ({}) to {} ({}) should succeed.\nstdout: {}\nstderr: {}",
iface_b,
ip_b,
iface_a,
ip_a,
String::from_utf8_lossy(&ping_reverse.stdout),
String::from_utf8_lossy(&ping_reverse.stderr),
);
manager_a.shutdown();
manager_b.shutdown();
tokio::time::sleep(Duration::from_millis(200)).await;
let check_a = Command::new("ip")
.args(["link", "show", "dev", iface_a])
.output()
.await
.expect("ip link show should execute");
assert!(
!check_a.status.success(),
"Interface {iface_a} should be removed after shutdown",
);
let check_b = Command::new("ip")
.args(["link", "show", "dev", iface_b])
.output()
.await
.expect("ip link show should execute");
assert!(
!check_b.status.success(),
"Interface {iface_b} should be removed after shutdown",
);
}
#[tokio::test]
async fn test_overlay_config_and_peer_config_format_ipv6() {
let (private_key, public_key) = OverlayTransport::generate_keys()
.await
.expect("key generation should succeed");
let config = OverlayConfig {
local_endpoint: SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 51820),
private_key: private_key.clone(),
public_key: public_key.clone(),
overlay_cidr: "fd00:200::1/48".to_string(),
cluster_cidr: None,
peer_discovery_interval: Duration::from_secs(30),
#[cfg(feature = "nat")]
nat: zlayer_overlay::nat::NatConfig::default(),
};
assert_eq!(config.local_endpoint.port(), 51820);
assert!(config.local_endpoint.ip().is_ipv6());
assert_eq!(config.overlay_cidr, "fd00:200::1/48");
assert_eq!(config.private_key, private_key);
assert_eq!(config.public_key, public_key);
let peer = PeerInfo::new(
public_key.clone(),
SocketAddr::new(
IpAddr::V6(Ipv6Addr::new(0xfd00, 0x200, 0, 0, 0, 0, 0, 0x20)),
51820,
),
"fd00:200::2/128",
Duration::from_secs(25),
);
let peer_config = peer.to_peer_config();
assert!(
peer_config.contains("[Peer]"),
"Peer config must contain [Peer] section header"
);
assert!(
peer_config.contains(&format!("PublicKey = {public_key}")),
"Peer config must contain the correct public key"
);
assert!(
peer_config.contains("Endpoint = [fd00:200::20]:51820"),
"Peer config must contain the correctly formatted IPv6 endpoint, got: {peer_config}"
);
assert!(
peer_config.contains("AllowedIPs = fd00:200::2/128"),
"Peer config must contain the correct IPv6 allowed IPs"
);
assert!(
peer_config.contains("PersistentKeepalive = 25"),
"Peer config must contain the correct keepalive interval"
);
}
#[tokio::test]
async fn test_overlay_config_mixed_v4_v6_peers() {
let (private_key, public_key) = OverlayTransport::generate_keys()
.await
.expect("key generation should succeed");
let (_, peer_pub_v4) = OverlayTransport::generate_keys()
.await
.expect("key generation for v4 peer should succeed");
let (_, peer_pub_v6) = OverlayTransport::generate_keys()
.await
.expect("key generation for v6 peer should succeed");
let peer_v4 = PeerInfo::new(
peer_pub_v4.clone(),
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 20)), 51820),
"10.200.0.2/32",
Duration::from_secs(25),
);
let peer_v6 = PeerInfo::new(
peer_pub_v6.clone(),
SocketAddr::new(
IpAddr::V6(Ipv6Addr::new(0xfd00, 0x200, 0, 0, 0, 0, 0, 0x20)),
51820,
),
"fd00:200::20/128",
Duration::from_secs(25),
);
let config_v4 = peer_v4.to_peer_config();
let config_v6 = peer_v6.to_peer_config();
assert!(
config_v4.contains("Endpoint = 192.168.1.20:51820"),
"IPv4 peer should have plain IP:port endpoint"
);
assert!(
config_v4.contains("AllowedIPs = 10.200.0.2/32"),
"IPv4 peer should have IPv4 allowed IPs"
);
assert!(
config_v6.contains("Endpoint = [fd00:200::20]:51820"),
"IPv6 peer should have [IP]:port endpoint"
);
assert!(
config_v6.contains("AllowedIPs = fd00:200::20/128"),
"IPv6 peer should have IPv6 allowed IPs"
);
assert!(config_v4.contains(&format!("PublicKey = {peer_pub_v4}")));
assert!(config_v6.contains(&format!("PublicKey = {peer_pub_v6}")));
let full_config = format!(
"[Interface]\nPrivateKey = {}\nListenPort = {}\n{}\n{}",
private_key, 51820, config_v4, config_v6,
);
assert!(full_config.starts_with("[Interface]"));
assert_eq!(
full_config.matches("[Peer]").count(),
2,
"Full config should contain exactly 2 [Peer] sections"
);
let _ = public_key; }
#[tokio::test]
#[ignore = "requires root or CAP_NET_ADMIN"]
async fn test_overlay_interface_lifecycle_ipv6() {
let iface_name = "wg-test-v6";
let (private_key, public_key) = OverlayTransport::generate_keys()
.await
.expect("key generation should succeed");
let config = OverlayConfig {
local_endpoint: SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 51850),
private_key,
public_key,
overlay_cidr: "fd00:250::1/48".to_string(),
cluster_cidr: None,
peer_discovery_interval: Duration::from_secs(30),
#[cfg(feature = "nat")]
nat: zlayer_overlay::nat::NatConfig::default(),
};
let mut manager = OverlayTransport::new(config, iface_name.to_string());
let _ = Command::new("ip")
.args(["link", "del", "dev", iface_name])
.output()
.await;
manager
.create_interface()
.await
.expect("create_interface should succeed with IPv6 config");
let link_output = Command::new("ip")
.args(["link", "show", "dev", iface_name])
.output()
.await
.expect("ip link show should execute");
assert!(
link_output.status.success(),
"Interface {} should exist after create_interface, stderr: {}",
iface_name,
String::from_utf8_lossy(&link_output.stderr)
);
manager
.configure(&[])
.await
.expect("configure_interface should succeed with IPv6 and no peers");
let link_output = Command::new("ip")
.args(["link", "show", "dev", iface_name])
.output()
.await
.expect("ip link show should execute");
let link_stdout = String::from_utf8_lossy(&link_output.stdout);
assert!(
link_stdout.contains("UP") || link_stdout.contains("up"),
"Interface should be UP after configure_interface, got: {link_stdout}",
);
let addr_output = Command::new("ip")
.args(["-6", "addr", "show", "dev", iface_name])
.output()
.await
.expect("ip -6 addr show should execute");
let addr_stdout = String::from_utf8_lossy(&addr_output.stdout);
assert!(
addr_stdout.contains("fd00:250::1"),
"Interface should have IPv6 overlay address fd00:250::1 assigned, got: {addr_stdout}",
);
manager.shutdown();
tokio::time::sleep(Duration::from_millis(200)).await;
let link_output = Command::new("ip")
.args(["link", "show", "dev", iface_name])
.output()
.await
.expect("ip link show should execute");
assert!(
!link_output.status.success(),
"Interface {iface_name} should no longer exist after shutdown",
);
}