d5b9a8611d
- 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>
193 lines
6.7 KiB
Rust
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);
|
|
}
|