//! CLI-level end-to-end loopback (no TUN, no root): build the CLI's `server.toml` / `client.toml` //! structs from real TOML, derive the transport wiring through the **CLI config helpers** //! ([`ServerConfigFile::transport_endpoints`] / [`ClientConfigFile::dial_config`]), bind a real //! [`aura_transport::MultiServer`], [`aura_transport::dial`] it, and exchange packets over the //! returned [`PacketConnection`] — asserting integrity and the negotiated transport mode. //! //! This proves the CLI builds correct `Endpoints` / `DialConfig` from config and that the new //! multi-transport server + dialer connect end to end. It is the full CLI integration path short of //! the privileged TUN device (which needs root and is therefore exercised only in a live run). //! //! UDP-only is used so the test can learn a single free loopback port up front (the custom-UDP //! backend is single-peer-per-accept in v1, which is exactly one client here). The fallback/handover //! logic itself is unit-tested in `aura-transport`; here we prove the CLI feeds it correct configs. use std::path::PathBuf; use std::sync::Arc; use aura_cli::config::{ClientConfigFile, ServerConfigFile}; use aura_pki::AuraCa; use aura_proto::PacketConnection; use aura_transport::{dial, MultiServer, TransportMode}; const SERVER_NAME: &str = "localhost"; /// A unique temp directory for this test process (no `tempfile` dependency in the workspace). fn temp_dir(tag: &str) -> PathBuf { let mut dir = std::env::temp_dir(); dir.push(format!( "aura-cli-test-{tag}-{}-{}", std::process::id(), std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_nanos() )); std::fs::create_dir_all(&dir).expect("create temp dir"); dir } /// Grab a currently-free UDP port on loopback by binding `:0` and immediately releasing it. On the /// loopback interface in a test process the window before we rebind it is negligible and /// deterministic enough for CI. fn free_udp_port() -> u16 { let sock = std::net::UdpSocket::bind("127.0.0.1:0").expect("bind ephemeral udp"); sock.local_addr().expect("local_addr").port() // `sock` drops here, freeing the port for MultiServer to re-bind. } #[tokio::test] async fn cli_config_drives_multiserver_and_dial() { let dir = temp_dir("loopback"); // PKI: CA + server cert (SAN localhost) + client cert, written to PEM files the CLI config reads. let ca = AuraCa::generate("Aura CLI Test CA").expect("generate CA"); let server_cert = ca.issue_server_cert(SERVER_NAME).expect("server cert"); let client_cert = ca.issue_client_cert("cli-client").expect("client cert"); let ca_path = dir.join("ca.crt"); let srv_cert_path = dir.join("server.crt"); let srv_key_path = dir.join("server.key"); let cli_cert_path = dir.join("client.crt"); let cli_key_path = dir.join("client.key"); std::fs::write(&ca_path, ca.ca_cert_pem()).unwrap(); std::fs::write(&srv_cert_path, &server_cert.cert_pem).unwrap(); std::fs::write(&srv_key_path, &server_cert.key_pem).unwrap(); std::fs::write(&cli_cert_path, &client_cert.cert_pem).unwrap(); std::fs::write(&cli_key_path, &client_cert.key_pem).unwrap(); // UDP-only on a learned free loopback port. SNI must match the server cert SAN (used as the inner // handshake server_name) so mutual auth succeeds. let udp_port = free_udp_port(); let server_toml = format!( r#" [server] name = "edge-test" listen = "127.0.0.1:{udp_port}" [pki] ca_cert = "{ca}" cert = "{cert}" key = "{key}" [tunnel] pool_cidr = "10.7.0.0/24" [transport] order = ["udp"] udp_port = {udp_port} quic_port = {quic_port} obfuscate = false "#, ca = ca_path.display(), cert = srv_cert_path.display(), key = srv_key_path.display(), quic_port = udp_port + 1, ); let client_toml = format!( r#" [client] name = "laptop-test" server_addr = "127.0.0.1:{udp_port}" sni = "{sni}" [pki] ca_cert = "{ca}" cert = "{cert}" key = "{key}" [tunnel] local_ip = "10.7.0.2" [transport] order = ["udp"] udp_port = {udp_port} quic_port = {quic_port} obfuscate = false "#, sni = SERVER_NAME, ca = ca_path.display(), cert = cli_cert_path.display(), key = cli_key_path.display(), quic_port = udp_port + 1, ); let server_cfg = ServerConfigFile::parse(&server_toml).expect("parse server.toml"); let client_cfg = ClientConfigFile::parse(&client_toml).expect("parse client.toml"); // Derive the transport wiring through the actual CLI helpers (the thing under test). let endpoints = server_cfg.transport_endpoints().expect("server endpoints"); assert_eq!( endpoints.udp.unwrap().to_string(), format!("127.0.0.1:{udp_port}") ); assert!(endpoints.tcp.is_none() && endpoints.quic.is_none()); let server_proto = server_cfg.to_proto().expect("server proto cfg"); let client_proto = client_cfg.to_proto().expect("client proto cfg"); let dial_cfg = client_cfg.dial_config().expect("client dial config"); assert_eq!(dial_cfg.order, vec![TransportMode::Udp]); // Bind every enabled transport (just UDP here) via the new MultiServer. let mut server = MultiServer::bind( endpoints, server_proto, server_cfg.udp_opts(), server_cfg.tcp_opts(), ) .await .expect("bind MultiServer"); // Accept + dial concurrently. let accept = tokio::spawn(async move { server.accept().await.map(|a| (a, server)) }); let connect = tokio::spawn(async move { dial(client_proto, dial_cfg).await }); let (accepted, _server_keepalive) = accept .await .expect("accept join") .expect("MultiServer accepted a connection"); let (client_conn, mode): (Arc, TransportMode) = connect .await .expect("connect join") .expect("dial connected"); // The handover picked UDP, and the server verified the client's CN via mutual auth. assert_eq!(mode, TransportMode::Udp); assert_eq!(accepted.mode, TransportMode::Udp); assert_eq!(accepted.peer_id.as_deref(), Some("cli-client")); let server_conn = accepted.conn; // Client -> server. for pkt in [ b"ping".to_vec(), vec![0u8; 1400], (0..=255u8).collect::>(), ] { client_conn.send_packet(&pkt).await.expect("client send"); let got = server_conn.recv_packet().await.expect("server recv"); assert_eq!(got, pkt); } // Server -> client. for pkt in [b"pong".to_vec(), vec![0x5Au8; 999]] { server_conn.send_packet(&pkt).await.expect("server send"); let got = client_conn.recv_packet().await.expect("client recv"); assert_eq!(got, pkt); } let _ = std::fs::remove_dir_all(&dir); }