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>
This commit is contained in:
@@ -1,65 +1,174 @@
|
||||
//! CLI-level end-to-end loopback (no TUN): mint certs via [`aura_pki::AuraCa`], build proto
|
||||
//! Client/Server configs, [`AuraServer::bind`] on `127.0.0.1:0`, [`AuraClient::connect`], and
|
||||
//! exchange packets via the [`PacketConnection`] API, asserting integrity.
|
||||
//! 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 is the full CLI integration path short of the privileged TUN device: it proves the crate's
|
||||
//! wiring of aura-pki + aura-proto + aura-transport works end to end without root or external
|
||||
//! network.
|
||||
//! 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::{ClientConfig, PacketConnection, ServerConfig};
|
||||
use aura_transport::{AuraClient, AuraServer};
|
||||
use aura_proto::PacketConnection;
|
||||
use aura_transport::{dial, MultiServer, TransportMode};
|
||||
|
||||
const SERVER_NAME: &str = "localhost";
|
||||
const CAMOUFLAGE_SNI: &str = "cdn.example.com";
|
||||
|
||||
/// 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_loopback_packet_exchange() {
|
||||
// PKI: CA + server cert (SAN localhost) + client cert.
|
||||
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_pem = ca.ca_cert_pem();
|
||||
|
||||
let server_cfg = ServerConfig {
|
||||
ca_cert_pem: ca_pem.clone(),
|
||||
server_cert_pem: server_cert.cert_pem.clone(),
|
||||
server_key_pem: server_cert.key_pem.clone(),
|
||||
};
|
||||
let client_cfg = ClientConfig {
|
||||
ca_cert_pem: ca_pem.clone(),
|
||||
client_cert_pem: client_cert.cert_pem.clone(),
|
||||
client_key_pem: client_cert.key_pem.clone(),
|
||||
server_name: SERVER_NAME.to_string(),
|
||||
};
|
||||
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();
|
||||
|
||||
// Bind on an OS-assigned loopback port.
|
||||
let server = AuraServer::bind(
|
||||
"127.0.0.1:0".parse().unwrap(),
|
||||
&server_cert.cert_pem,
|
||||
&server_cert.key_pem,
|
||||
server_cfg,
|
||||
// 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(),
|
||||
)
|
||||
.expect("bind server");
|
||||
let server_addr = server.local_addr().expect("local_addr");
|
||||
.await
|
||||
.expect("bind MultiServer");
|
||||
|
||||
// Accept + connect concurrently.
|
||||
let accept = tokio::spawn(async move { server.accept().await });
|
||||
let connect =
|
||||
tokio::spawn(
|
||||
async move { AuraClient::connect(server_addr, CAMOUFLAGE_SNI, client_cfg).await },
|
||||
);
|
||||
// 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 server_conn = accept.await.expect("accept join").expect("accept");
|
||||
let client_conn = connect.await.expect("connect join").expect("connect");
|
||||
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");
|
||||
|
||||
// Mutual auth established the client's verified CN on the server side.
|
||||
assert_eq!(server_conn.peer_id(), Some("cli-client"));
|
||||
// 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: Arc<dyn PacketConnection> = Arc::new(server_conn);
|
||||
let client_conn: Arc<dyn PacketConnection> = Arc::new(client_conn);
|
||||
let server_conn = accepted.conn;
|
||||
|
||||
// Client -> server.
|
||||
for pkt in [
|
||||
@@ -78,4 +187,6 @@ async fn cli_loopback_packet_exchange() {
|
||||
let got = client_conn.recv_packet().await.expect("client recv");
|
||||
assert_eq!(got, pkt);
|
||||
}
|
||||
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user