//! Daemon-level encrypted transport proofs. //! //! These tests deliberately use the public client/server runtime entrypoints: //! an application UDP socket talks to a client runtime, which talks through an //! authenticated encrypted listener to a per-session UDP backend socket. use std::{ fs, net::SocketAddr, os::unix::fs::PermissionsExt, path::Path, process::Command, sync::{ atomic::{AtomicU16, Ordering}, Arc, }, time::Duration, }; use tempfile::TempDir; use tokio::{ net::UdpSocket, sync::mpsc, time::{sleep, timeout}, }; use wgcloak::{ client, config::{self, Config, TransportMode}, control, server, transport::{ quic::QuicClientConnector, tls_tcp::client_tls_config, TlsTcpConnector, TransportConnector, TransportFuture, TransportSession, }, }; const SECRET_ONE: &str = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; const SECRET_TWO: &str = "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"; const WRONG_SECRET: &str = "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"; fn free_udp_port() -> u16 { std::net::UdpSocket::bind("127.0.0.1:0") .unwrap() .local_addr() .unwrap() .port() } fn free_tcp_port() -> u16 { std::net::TcpListener::bind("127.0.0.1:0") .unwrap() .local_addr() .unwrap() .port() } fn private_runtime_dir(root: &Path) -> std::path::PathBuf { let path = root.join("runtime"); fs::create_dir(&path).unwrap(); fs::set_permissions(&path, fs::Permissions::from_mode(0o700)).unwrap(); path } fn make_identity(dir: &TempDir) -> (std::path::PathBuf, std::path::PathBuf) { let cert = dir.path().join("cert.der"); let key = dir.path().join("key.der"); let pem_cert = dir.path().join("cert.pem"); let pem_key = dir.path().join("key.pem"); let result = Command::new("openssl") .args(["req", "-x509", "-newkey", "rsa:2048", "-nodes", "-keyout"]) .arg(&pem_key) .args(["-out"]) .arg(&pem_cert) .args([ "-subj", "/CN=localhost", "-addext", "subjectAltName=DNS:localhost,IP:127.0.0.1", "-addext", "basicConstraints=critical,CA:FALSE", "-addext", "extendedKeyUsage=serverAuth", "-days", "1", ]) .output() .unwrap(); assert!(result.status.success(), "openssl failed: {:?}", result); let result = Command::new("openssl") .args(["x509", "-in"]) .arg(&pem_cert) .args(["-outform", "DER", "-out"]) .arg(&cert) .output() .unwrap(); assert!(result.status.success()); let result = Command::new("openssl") .args(["pkcs8", "-topk8", "-nocrypt", "-in"]) .arg(&pem_key) .args(["-outform", "DER", "-out"]) .arg(&key) .output() .unwrap(); assert!(result.status.success()); (cert, key) } fn server_config( cert: &Path, key: &Path, backend: SocketAddr, quic_port: u16, tls_port: u16, quic: bool, tls_tcp: bool, ) -> config::ServerConfig { Config::from_str(&format!( "[server]\nlisten='127.0.0.1'\n[backend]\ntarget='{backend}'\n[transports.direct_udp]\nenabled=false\n[transports.quic]\nenabled={quic}\nport={quic_port}\n[transports.http3]\nenabled=false\n[transports.tls_tcp]\nenabled={tls_tcp}\nport={tls_port}\n[tls]\ncertificate='{}'\nprivate_key='{}'\n[[auth.clients]]\nid='one'\nsecret='{SECRET_ONE}'\n[[auth.clients]]\nid='two'\nsecret='{SECRET_TWO}'\n[limits]\nhandshake_timeout_ms=2000\n", cert.display(), key.display(), )) .unwrap() .into_server() .unwrap() } fn client_config( id: &str, secret: &str, mode: &str, port: u16, local_port: u16, ) -> config::ClientConfig { Config::from_str(&format!( "[client]\nserver='localhost'\nid='{id}'\nsecret='{secret}'\n[local]\nlisten='127.0.0.1:{local_port}'\n[transport]\nmode='{mode}'\n[transport.auto]\nprobe_timeout_ms=500\nretries=1\n[transport.quic]\nport={port}\n[transport.tls_tcp]\nport={port}\n[network]\nmax_datagram_size=1200\n" )) .unwrap() .into_client() .unwrap() } fn quic_connector(config: config::ClientConfig, cert: &Path, key: &Path) -> QuicClientConnector { let certs = wgcloak::transport::quic::load_tls_identity(cert, key) .map(|(certs, _)| certs) .unwrap(); let mut roots = rustls::RootCertStore::empty(); roots.add(certs[0].clone()).unwrap(); let tls = quinn::ClientConfig::with_root_certificates(Arc::new(roots)).unwrap(); QuicClientConnector::with_client_config(config, tls) } fn tls_connector(config: config::ClientConfig, cert: &Path) -> TlsTcpConnector { let certs = wgcloak::transport::tls_tcp::load_certificates(cert).unwrap(); let mut roots = rustls::RootCertStore::empty(); roots.add(certs[0].clone()).unwrap(); TlsTcpConnector::with_client_config(config, (*client_tls_config(roots)).clone()) } struct RotatingQuicConnector { config: config::ClientConfig, cert: std::path::PathBuf, key: std::path::PathBuf, port: Arc, } impl TransportConnector for RotatingQuicConnector { fn connect<'a>(&'a self) -> TransportFuture<'a, Box> { Box::pin(async move { let mut config = self.config.clone(); config.transport.quic.port = self.port.load(Ordering::Relaxed); quic_connector(config, &self.cert, &self.key) .connect() .await }) } } async fn wait_for(path: &Path, command: &str, predicate: impl Fn(&str) -> bool) -> String { let mut last = None; timeout(Duration::from_secs(10), async { loop { if let Ok(response) = control::send_command(path, command).await { if predicate(&response) { return response; } last = Some(response); } sleep(Duration::from_millis(25)).await; } }) .await .unwrap_or_else(|_| { panic!( "control {} {command} did not reach expected state: {last:?}", path.display() ) }) } async fn echo_backend(socket: UdpSocket) { echo_backend_observed(socket, None).await; } async fn echo_backend_observed( socket: UdpSocket, observed: Option>, ) { let mut buf = [0u8; 2048]; while let Ok((len, peer)) = socket.recv_from(&mut buf).await { if let Some(observed) = &observed { let _ = observed.send(peer); } let _ = socket.send_to(&buf[..len], peer).await; } } async fn stop(path: &Path, task: tokio::task::JoinHandle>) { let response = control::send_command(path, "shutdown").await.unwrap(); assert!(response.contains("\"ok\":true")); timeout(Duration::from_secs(5), task) .await .expect("runtime did not stop") .unwrap() .unwrap(); } #[tokio::test] async fn explicit_quic_runtime_forwards_two_clients_and_reports_identity() { let dir = tempfile::tempdir().unwrap(); let (cert, key) = make_identity(&dir); let runtime = private_runtime_dir(dir.path()); let backend = UdpSocket::bind("127.0.0.1:0").await.unwrap(); let backend_addr = backend.local_addr().unwrap(); let (peer_tx, mut peer_rx) = mpsc::unbounded_channel(); let backend_task = tokio::spawn(echo_backend_observed(backend, Some(peer_tx))); let quic_port = free_udp_port(); let server_path = runtime.join("server.sock"); let server_task = tokio::spawn(server::run_with_control_path( server_config( &cert, &key, backend_addr, quic_port, free_udp_port(), true, false, ), server_path.clone(), )); wait_for(&server_path, "status", |s| s.contains("transports=quic")).await; let mut clients = Vec::new(); for (id, secret) in [("one", SECRET_ONE), ("two", SECRET_TWO)] { let local = free_udp_port(); let app = UdpSocket::bind("127.0.0.1:0").await.unwrap(); let cfg = client_config(id, secret, "quic", quic_port, local); let control_path = runtime.join(format!("client-{id}.sock")); let connector = Arc::new(quic_connector(cfg.clone(), &cert, &key)) as Arc; let task = tokio::spawn(client::run_with_transport_connector_and_control_path( cfg, control_path.clone(), TransportMode::Quic, connector, )); wait_for(&control_path, "stats", |s| { s.contains("\"state\":\"connected\"") }) .await; clients.push((local, app, control_path, task)); } wait_for(&server_path, "stats", |s| { s.contains("\"active_sessions_by_transport\":{\"quic\":2") && s.contains("\"one\":1") && s.contains("\"two\":1") }) .await; for (local, app, _, _) in &clients { app.send_to(b"encrypted-echo", ("127.0.0.1", *local)) .await .unwrap(); let mut reply = [0u8; 64]; let (len, _) = timeout(Duration::from_secs(5), app.recv_from(&mut reply)) .await .unwrap() .unwrap(); assert_eq!(&reply[..len], b"encrypted-echo"); } let peer_one = timeout(Duration::from_secs(2), peer_rx.recv()) .await .unwrap() .unwrap(); let peer_two = timeout(Duration::from_secs(2), peer_rx.recv()) .await .unwrap() .unwrap(); assert_ne!( peer_one, peer_two, "each encrypted session needs its own backend socket" ); for (_, _, control_path, task) in clients { stop(&control_path, task).await; } stop(&server_path, server_task).await; backend_task.abort(); } #[tokio::test] async fn explicit_tls_runtime_forwards_and_rejects_hostname_mismatch() { let dir = tempfile::tempdir().unwrap(); let (cert, key) = make_identity(&dir); let runtime = private_runtime_dir(dir.path()); let backend = UdpSocket::bind("127.0.0.1:0").await.unwrap(); let backend_addr = backend.local_addr().unwrap(); let backend_task = tokio::spawn(echo_backend(backend)); let tls_port = free_tcp_port(); let server_path = runtime.join("server.sock"); let server_task = tokio::spawn(server::run_with_control_path( server_config( &cert, &key, backend_addr, free_udp_port(), tls_port, false, true, ), server_path.clone(), )); wait_for(&server_path, "status", |s| s.contains("transports=tls_tcp")).await; let local = free_udp_port(); let app = UdpSocket::bind("127.0.0.1:0").await.unwrap(); let cfg = client_config("one", SECRET_ONE, "tls_tcp", tls_port, local); let client_path = runtime.join("client.sock"); let connector = Arc::new(tls_connector(cfg.clone(), &cert)) as Arc; let client_task = tokio::spawn(client::run_with_transport_connector_and_control_path( cfg.clone(), client_path.clone(), TransportMode::TlsTcp, connector, )); wait_for(&client_path, "stats", |s| { s.contains("\"state\":\"connected\"") }) .await; app.send_to(b"tls-echo", ("127.0.0.1", local)) .await .unwrap(); let mut reply = [0u8; 64]; let (len, _) = timeout(Duration::from_secs(5), app.recv_from(&mut reply)) .await .unwrap() .unwrap(); assert_eq!(&reply[..len], b"tls-echo"); wait_for(&server_path, "stats", |s| { s.contains("\"active_sessions_by_transport\":{\"tls_tcp\":1") && s.contains("\"active_sessions_by_client_id\":{\"one\":1") }) .await; // A trusted certificate with an unrelated hostname must still fail normal // hostname verification; trusting the issuing certificate is not enough. let mut mismatch = cfg.clone(); mismatch.client.server = "not-localhost".to_owned(); let result = tls_connector(mismatch, &cert).connect().await; assert!(matches!( result, Err(wgcloak::transport::TransportError::Io(_)) )); stop(&client_path, client_task).await; stop(&server_path, server_task).await; backend_task.abort(); } #[tokio::test] async fn encrypted_runtime_cancellation_releases_server_sessions() { let dir = tempfile::tempdir().unwrap(); let (cert, key) = make_identity(&dir); let runtime = private_runtime_dir(dir.path()); let backend = UdpSocket::bind("127.0.0.1:0").await.unwrap(); let backend_addr = backend.local_addr().unwrap(); let backend_task = tokio::spawn(echo_backend(backend)); let quic_port = free_udp_port(); let server_path = runtime.join("server.sock"); let server_task = tokio::spawn(server::run_with_control_path( server_config( &cert, &key, backend_addr, quic_port, free_udp_port(), true, false, ), server_path.clone(), )); wait_for(&server_path, "status", |s| s.contains("transports=quic")).await; let local = free_udp_port(); let cfg = client_config("one", SECRET_ONE, "quic", quic_port, local); let client_path = runtime.join("client.sock"); let connector = Arc::new(quic_connector(cfg.clone(), &cert, &key)) as Arc; let client_task = tokio::spawn(client::run_with_transport_connector_and_control_path( cfg, client_path.clone(), TransportMode::Quic, connector, )); wait_for(&client_path, "stats", |s| { s.contains("\"state\":\"connected\"") }) .await; wait_for(&server_path, "stats", |s| { s.contains("\"active_sessions\":1") }) .await; stop(&client_path, client_task).await; wait_for(&server_path, "stats", |s| { s.contains("\"active_sessions\":0") }) .await; stop(&server_path, server_task).await; backend_task.abort(); } #[tokio::test] async fn wrong_secret_is_authentication_rejection_without_fallback() { let dir = tempfile::tempdir().unwrap(); let (cert, key) = make_identity(&dir); let backend = UdpSocket::bind("127.0.0.1:0").await.unwrap(); let backend_addr = backend.local_addr().unwrap(); let backend_task = tokio::spawn(echo_backend(backend)); let quic_port = free_udp_port(); let server_path = private_runtime_dir(dir.path()).join("server.sock"); let server_task = tokio::spawn(server::run_with_control_path( server_config( &cert, &key, backend_addr, quic_port, free_udp_port(), true, false, ), server_path.clone(), )); wait_for(&server_path, "status", |s| s.contains("transports=quic")).await; let cfg = client_config("one", WRONG_SECRET, "quic", quic_port, free_udp_port()); let result = timeout( Duration::from_secs(5), QuicClientConnector::with_client_config(cfg.clone(), { let certs = wgcloak::transport::quic::load_tls_identity(&cert, &key) .unwrap() .0; let mut roots = rustls::RootCertStore::empty(); roots.add(certs[0].clone()).unwrap(); quinn::ClientConfig::with_root_certificates(Arc::new(roots)).unwrap() }) .connect(), ) .await .unwrap(); assert!(matches!( result, Err(wgcloak::transport::TransportError::AuthenticationRejected( _ )) )); stop(&server_path, server_task).await; backend_task.abort(); } #[tokio::test] async fn quic_runtime_reconnects_after_server_restart_and_cleans_on_cancel() { let dir = tempfile::tempdir().unwrap(); let (cert, key) = make_identity(&dir); let runtime = private_runtime_dir(dir.path()); let backend = UdpSocket::bind("127.0.0.1:0").await.unwrap(); let backend_addr = backend.local_addr().unwrap(); let backend_task = tokio::spawn(echo_backend(backend)); let quic_port = free_udp_port(); let server_path = runtime.join("server.sock"); let first_server = tokio::spawn(server::run_with_control_path( server_config( &cert, &key, backend_addr, quic_port, free_udp_port(), true, false, ), server_path.clone(), )); wait_for(&server_path, "status", |s| s.contains("transports=quic")).await; let local = free_udp_port(); let app = UdpSocket::bind("127.0.0.1:0").await.unwrap(); let cfg = client_config("one", SECRET_ONE, "quic", quic_port, local); let client_path = runtime.join("client.sock"); let rotating_port = Arc::new(AtomicU16::new(quic_port)); let connector = Arc::new(RotatingQuicConnector { config: cfg.clone(), cert: cert.clone(), key: key.clone(), port: rotating_port.clone(), }) as Arc; let client_task = tokio::spawn(client::run_with_transport_connector_and_control_path( cfg, client_path.clone(), TransportMode::Quic, connector, )); wait_for(&client_path, "stats", |s| { s.contains("\"state\":\"connected\"") }) .await; app.send_to(b"before-restart", ("127.0.0.1", local)) .await .unwrap(); let mut reply = [0u8; 64]; let (len, _) = timeout(Duration::from_secs(5), app.recv_from(&mut reply)) .await .unwrap() .unwrap(); assert_eq!(&reply[..len], b"before-restart"); control::send_command(&server_path, "shutdown") .await .unwrap(); timeout(Duration::from_secs(5), first_server) .await .unwrap() .unwrap() .unwrap(); let replacement_port = free_udp_port(); rotating_port.store(replacement_port, Ordering::Relaxed); let replacement_path = runtime.join("server-replacement.sock"); let replacement = tokio::spawn(server::run_with_control_path( server_config( &cert, &key, backend_addr, replacement_port, free_udp_port(), true, false, ), replacement_path.clone(), )); wait_for(&replacement_path, "status", |s| { s.contains("transports=quic") }) .await; wait_for(&client_path, "stats", |s| { s.contains("\"state\":\"connected\"") && s.contains("\"reconnections\":1") }) .await; app.send_to(b"after-restart", ("127.0.0.1", local)) .await .unwrap(); let (len, _) = timeout(Duration::from_secs(5), app.recv_from(&mut reply)) .await .unwrap() .unwrap(); assert_eq!(&reply[..len], b"after-restart"); stop(&client_path, client_task).await; stop(&replacement_path, replacement).await; backend_task.abort(); }