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:
xah30
2026-05-25 21:41:59 +03:00
parent d72fbe8d68
commit d5b9a8611d
15 changed files with 682 additions and 94 deletions
+154 -43
View File
@@ -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);
}