Files
AuraVPN/crates/aura-cli/tests/loopback.rs
T
xah30 d5b9a8611d feat(cli): select transport in config; server MultiServer + client dial handover
- aura-cli config gains [transport] (order + per-transport ports + obfuscate/
  masquerade); server binds all enabled transports via MultiServer, client uses
  dial() with UDP->TCP->QUIC handover. Config examples updated; backward-compatible
  (defaults to udp,tcp,quic). 21 cli tests incl. a real-UDP-transport loopback.
- docs/sing-box.md: integration approach note (process-bridge now; native Go
  outbound for phones, with crypto-library mapping + KAT requirement).
- Normalize rustfmt across the v2 transport files (tcp/dial/udp contract).

Whole workspace: 97 tests pass, clippy -D warnings clean, fmt clean. Deploy flow
(pki init/issue-server/issue-client) validated with the release binary.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 21:41:59 +03:00

193 lines
6.7 KiB
Rust

//! 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<dyn PacketConnection>, 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::<Vec<u8>>(),
] {
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);
}