use std::{process::Command, sync::Arc, time::Duration}; use bytes::Bytes; use rustls::RootCertStore; use tempfile::tempdir; use tokio::time::timeout; use wgcloak::{ config::Config, protocol, transport, transport::{ AdmissionController, TlsTcpConnector, TlsTcpServer, TransportConnector, TransportError, }, }; const SECRET: &str = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; fn free_port() -> u16 { std::net::TcpListener::bind("127.0.0.1:0") .unwrap() .local_addr() .unwrap() .port() } #[tokio::test] async fn tls_tcp_authenticates_round_trips_and_classifies_failures() { if Command::new("openssl").arg("version").output().is_err() { return; } let dir = tempdir().unwrap(); let cert = dir.path().join("cert.pem"); let key = dir.path().join("key.pem"); let generated = Command::new("openssl") .args([ "req", "-x509", "-newkey", "rsa:2048", "-nodes", "-subj", "/CN=localhost", "-addext", "subjectAltName=DNS:localhost", "-addext", "basicConstraints=critical,CA:FALSE", "-days", "1", "-keyout", ]) .arg(&key) .args(["-out"]) .arg(&cert) .output() .unwrap(); assert!( generated.status.success(), "openssl failed: {:?}", generated ); let port = free_port(); let server_config = Config::from_str(&format!( "[server]\nlisten = \"127.0.0.1\"\n[backend]\ntarget = \"127.0.0.1:51820\"\n[transports.direct_udp]\nenabled = false\n[transports.quic]\nenabled = false\n[transports.http3]\nenabled = false\n[transports.tls_tcp]\nenabled = true\nport = {port}\n[limits]\nhandshake_timeout_ms = 500\n[tls]\ncertificate = \"{}\"\nprivate_key = \"{}\"\n[[auth.clients]]\nid = \"test\"\nsecret = \"{SECRET}\"\n", cert.display(), key.display() )) .unwrap() .into_server() .unwrap(); let client_config = Config::from_str(&format!( "[client]\nserver = \"localhost\"\nid = \"test\"\nsecret = \"{SECRET}\"\n[transport]\nmode = \"tls_tcp\"\n[transport.tls_tcp]\nport = {port}\nkeepalive_secs = 1\n[network]\nmax_datagram_size = 1200\n" )) .unwrap() .into_client() .unwrap(); let mismatch_config = client_config.clone(); let certs = transport::tls_tcp::load_certificates(&cert).unwrap(); let mut roots = RootCertStore::empty(); roots.add(certs[0].clone()).unwrap(); let client_tls = (*transport::tls_tcp::client_tls_config(roots)).clone(); let admission = AdmissionController::new(&server_config.limits); let server = Arc::new( TlsTcpServer::bind_with_admission(&server_config, admission.clone()) .await .unwrap(), ); let slow_server = server.clone(); let slow_handshake = tokio::spawn(async move { slow_server.accept_session().await }); let slow_peer = tokio::net::TcpStream::connect(server.local_addr().unwrap()) .await .unwrap(); tokio::time::sleep(Duration::from_millis(50)).await; let accept = { let server = server.clone(); tokio::spawn(async move { server.accept_session().await }) }; let client = TlsTcpConnector::with_client_config(client_config, client_tls.clone()); let client_session = client.connect().await.unwrap(); let server_session = accept.await.unwrap().unwrap(); assert_eq!( client_session.authenticated_keepalive_interval(), Some(Duration::from_secs(1)) ); assert_eq!(client_session.rtt(), None); assert_eq!( client_session.remote_addr().unwrap().ip(), "127.0.0.1".parse::().unwrap() ); assert_eq!(admission.active_sessions(), 1); assert_eq!(admission.pending_handshakes(), 1); client_session.send_keepalive().await.unwrap(); client_session .send_datagram(Bytes::from_static(b"tls-opaque")) .await .unwrap(); assert_eq!( timeout(Duration::from_secs(2), server_session.recv_datagram()) .await .unwrap() .unwrap(), Bytes::from_static(b"tls-opaque") ); client_session.close().await.unwrap(); server_session.close().await.unwrap(); assert!(matches!( timeout(Duration::from_secs(1), slow_handshake) .await .unwrap() .unwrap(), Err(TransportError::HandshakeTimeout) )); drop(slow_peer); assert_eq!(admission.active_sessions(), 0); assert_eq!(admission.pending_handshakes(), 0); let mut wrong_credentials = mismatch_config.clone(); wrong_credentials.client.secret.secret = Some("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB".to_owned()); let server_accept = { let server = server.clone(); tokio::spawn(async move { server.accept_session().await }) }; let wrong_client = TlsTcpConnector::with_client_config(wrong_credentials, client_tls.clone()); assert!(matches!( timeout(Duration::from_secs(2), wrong_client.connect()) .await .unwrap(), Err(TransportError::AuthenticationRejected( protocol::RejectReason::AuthenticationFailed )) )); assert!(matches!( timeout(Duration::from_secs(2), server_accept) .await .unwrap() .unwrap(), Err(TransportError::AuthenticationRejected( protocol::RejectReason::AuthenticationFailed )) )); let server_accept = { let server = server.clone(); tokio::spawn(async move { server.accept_session().await }) }; let mut mismatch_config = mismatch_config; mismatch_config.client.server = "127.0.0.1".to_owned(); let mismatch_client = TlsTcpConnector::with_client_config(mismatch_config, client_tls); assert!(matches!( timeout(Duration::from_secs(2), mismatch_client.connect()) .await .unwrap(), Err(TransportError::Io(_)) )); assert!(timeout(Duration::from_secs(2), server_accept) .await .unwrap() .unwrap() .is_err()); }