9b98004424
Closes the v3.1 unlinkability gap and resists volume/timing correlation:
1) Per-hop client cert (identity-unlinkable hops). [[client.circuit.hops]]
now accepts {addr, cert_path, key_path, [server_name]} per hop — each
hop sees a different CN, so a relay and an exit cannot correlate the
same client by certificate. Old flat `hops = ["ip:port"]` form still
parses (serde untagged enum) and falls back to [pki] cert/key.
`aura provision-client --circuit-hops N` mints N fresh UUIDv4 certs.
2) Cell padding. CellPaddingConn wrapper pads every outgoing packet to a
fixed size (default 1280 bytes; `cell_size = N` configurable) before
it hits the inner AEAD. Format: u16_be(real_len) || pkt || zero_pad.
On-wire sizes become constant -> defeats volume/timing fingerprints.
Opt-in via [client.circuit] cell_padding = true and the mirror
[server] cell_padding_for_circuit_clients = true.
3) 3-hop support. dial_circuit now accepts N >= 2 hops; iterative
ExtendBridge nests N-1 forwarders and N handshakes. Client owns the
full chain via CircuitConnection (forwarders abort on drop).
New integration test multihop_v3_2_three_hops_end_to_end runs three
in-process actors (A relay -> B relay -> C exit) on loopback and
verifies peer_id == C's CN.
4) CIDR whitelist. [server.relay] allow_extend_to entries accept
"10.0.0.0/24" (subnet, any port), "10.0.0.0/24:443" (subnet + port),
"[2001:db8::/32]:443" (IPv6 with port), as well as exact IP:port.
Empty list keeps the v3.1 open-relay (warn).
19 new tests; workspace 276 passed (+19), clippy -D warnings clean, fmt clean.
257 baseline tests untouched; all v2 / v3.1 / LE configs work as before.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
511 lines
21 KiB
Rust
511 lines
21 KiB
Rust
//! v3.1 multi-hop / onion-routing integration test.
|
|
//!
|
|
//! Drives three actors on loopback in one process:
|
|
//!
|
|
//! * **Exit** — a vanilla [`UdpServer`] bound on a free UDP port. Its cert SAN is
|
|
//! `"localhost-exit"`. The server's accept task echoes the first three received packets back to
|
|
//! the sender, then drops.
|
|
//!
|
|
//! * **Relay** — another [`UdpServer`] on a free port, cert SAN `"localhost-relay"`. Its accept
|
|
//! task:
|
|
//! 1. accepts one connection (running its own outer Aura mutual-auth handshake with the
|
|
//! client),
|
|
//! 2. uses [`crate::relay::rendezvous`] to read the client's `ExtendBridge` envelope and open
|
|
//! a `connect()`ed UDP socket to the exit,
|
|
//! 3. spawns [`crate::relay::run_bridge`] to ferry bytes between the client and the bridge.
|
|
//!
|
|
//! * **Client** — calls [`circuit::dial_circuit_with_relay_name`] with
|
|
//! `relay_server_name = Some("localhost-relay")` and `proto_cfg.server_name = "localhost-exit"`.
|
|
//! The returned [`circuit::CircuitConnection`] should have `peer_id() == Some("localhost-exit")`
|
|
//! — the core multi-hop invariant: the **inner** handshake authenticated the exit's cert
|
|
//! through the relay opaquely, even though the outer hop authenticated the relay's cert.
|
|
//!
|
|
//! The test then exchanges three packets of varying sizes through the circuit and asserts that
|
|
//! every echoed reply matches.
|
|
|
|
use std::net::SocketAddr;
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
|
|
use aura_cli::circuit;
|
|
use aura_cli::relay::{self, RendezvousOutcome};
|
|
use aura_pki::AuraCa;
|
|
use aura_proto::{ClientConfig, PacketConnection, ServerConfig};
|
|
use aura_transport::{UdpOpts, UdpServer};
|
|
|
|
const EXIT_SAN: &str = "localhost-exit";
|
|
const RELAY_SAN: &str = "localhost-relay";
|
|
const CLIENT_ID: &str = "client-multihop";
|
|
|
|
/// Reserve and immediately release a free UDP port on loopback (the window before re-bind in the
|
|
/// same process is negligible on a quiet test).
|
|
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()
|
|
}
|
|
|
|
/// Build a [`ServerConfig`] from one shared CA, with the given SAN.
|
|
fn server_cfg(ca: &AuraCa, san: &str) -> ServerConfig {
|
|
let issued = ca.issue_server_cert(san).expect("issue server cert");
|
|
ServerConfig {
|
|
ca_cert_pem: ca.ca_cert_pem(),
|
|
server_cert_pem: issued.cert_pem,
|
|
server_key_pem: issued.key_pem,
|
|
}
|
|
}
|
|
|
|
/// Build a [`ClientConfig`] from one shared CA. `server_name` is used by the **inner** handshake
|
|
/// (the exit). The outer handshake's expected SAN is overridden separately at
|
|
/// [`circuit::dial_circuit_with_relay_name`] callsite.
|
|
fn client_cfg(ca: &AuraCa, server_name: &str) -> ClientConfig {
|
|
let issued = ca.issue_client_cert(CLIENT_ID).expect("issue client cert");
|
|
ClientConfig {
|
|
ca_cert_pem: ca.ca_cert_pem(),
|
|
client_cert_pem: issued.cert_pem,
|
|
client_key_pem: issued.key_pem,
|
|
server_name: server_name.to_string(),
|
|
}
|
|
}
|
|
|
|
/// Spawn the exit server: accept one connection and echo the first three packets back.
|
|
async fn spawn_exit(server: UdpServer) {
|
|
let conn = server.accept().await.expect("exit accept");
|
|
// The dropped server keeps the master loop alive via the connection's anchor.
|
|
drop(server);
|
|
let conn: Arc<dyn PacketConnection> = Arc::new(conn);
|
|
for _ in 0..3 {
|
|
match conn.recv_packet().await {
|
|
Ok(pkt) => {
|
|
if conn.send_packet(&pkt).await.is_err() {
|
|
return;
|
|
}
|
|
}
|
|
Err(_) => return,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Spawn the relay server: accept one connection, run the rendezvous, and bridge to the exit.
|
|
async fn spawn_relay(server: UdpServer, whitelist: Vec<SocketAddr>) {
|
|
let conn = server.accept().await.expect("relay accept");
|
|
drop(server);
|
|
let conn: Arc<dyn PacketConnection> = Arc::new(conn);
|
|
match relay::rendezvous(&conn, &whitelist).await {
|
|
RendezvousOutcome::Bridged { bridge } => {
|
|
relay::run_bridge(conn, bridge).await;
|
|
}
|
|
RendezvousOutcome::Refused => {
|
|
// Test path that exercises whitelist refusal — the relay sent CircuitFailed
|
|
// already; just exit.
|
|
}
|
|
RendezvousOutcome::Fallback { .. } => {
|
|
// The client did not send ExtendBridge — should not happen in the happy path.
|
|
panic!("relay rendezvous fell back unexpectedly");
|
|
}
|
|
}
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn multihop_v3_1_end_to_end() {
|
|
// One shared CA. Each role gets its own server cert with its own SAN.
|
|
let ca = AuraCa::generate("Aura Multi-Hop Test CA").expect("ca");
|
|
let exit_proto = server_cfg(&ca, EXIT_SAN);
|
|
let relay_proto = server_cfg(&ca, RELAY_SAN);
|
|
let client_proto = client_cfg(&ca, EXIT_SAN);
|
|
|
|
let exit_port = free_udp_port();
|
|
let relay_port = free_udp_port();
|
|
let exit_addr: SocketAddr = format!("127.0.0.1:{exit_port}").parse().unwrap();
|
|
let relay_addr: SocketAddr = format!("127.0.0.1:{relay_port}").parse().unwrap();
|
|
|
|
// Bind both servers BEFORE spawning the client so they are ready to accept.
|
|
let exit_server =
|
|
UdpServer::bind(exit_addr, exit_proto, UdpOpts::default()).expect("bind exit");
|
|
let relay_server =
|
|
UdpServer::bind(relay_addr, relay_proto, UdpOpts::default()).expect("bind relay");
|
|
let exit_actual = exit_server.local_addr().expect("exit addr");
|
|
let relay_actual = relay_server.local_addr().expect("relay addr");
|
|
|
|
// Whitelist contains exactly the exit address.
|
|
let whitelist = vec![exit_actual];
|
|
|
|
let exit_task = tokio::spawn(spawn_exit(exit_server));
|
|
let relay_task = tokio::spawn(spawn_relay(relay_server, whitelist));
|
|
|
|
// Give the servers a beat to enter their accept loops. Not strictly required (accept is
|
|
// resumable) but makes the trace easier to follow on failure.
|
|
tokio::time::sleep(Duration::from_millis(20)).await;
|
|
|
|
// Client: dial circuit. proto_cfg.server_name = "localhost-exit" so the inner handshake's
|
|
// verifier checks the exit's SAN; the outer handshake checks the relay's SAN via the explicit
|
|
// override.
|
|
let circuit_conn = tokio::time::timeout(
|
|
Duration::from_secs(30),
|
|
circuit::dial_circuit_with_relay_name(
|
|
&[relay_actual, exit_actual],
|
|
client_proto,
|
|
UdpOpts::default(),
|
|
Some(RELAY_SAN),
|
|
),
|
|
)
|
|
.await
|
|
.expect("dial_circuit did not finish within 30s")
|
|
.expect("dial_circuit succeeded");
|
|
|
|
// The core invariant: the INNER handshake authenticated the EXIT (not the relay).
|
|
assert_eq!(
|
|
circuit_conn.peer_id(),
|
|
Some(EXIT_SAN),
|
|
"circuit.peer_id() must be the exit's SAN — the inner handshake verified the exit's cert"
|
|
);
|
|
|
|
// Echo three packets of varying sizes through the circuit.
|
|
let payloads: Vec<Vec<u8>> = vec![
|
|
b"hello multi-hop".to_vec(),
|
|
vec![0xCDu8; 800],
|
|
(0..=255u8).collect(),
|
|
];
|
|
for pkt in &payloads {
|
|
circuit_conn.send_packet(pkt).await.expect("circuit send");
|
|
let echoed = tokio::time::timeout(Duration::from_secs(5), circuit_conn.recv_packet())
|
|
.await
|
|
.expect("recv timeout")
|
|
.expect("recv from exit through circuit");
|
|
assert_eq!(&echoed, pkt, "echoed payload must match");
|
|
}
|
|
|
|
// Clean shutdown — drop the client first, then wait for the actors to finish.
|
|
drop(circuit_conn);
|
|
let _ = tokio::time::timeout(Duration::from_secs(5), exit_task).await;
|
|
let _ = tokio::time::timeout(Duration::from_secs(5), relay_task).await;
|
|
}
|
|
|
|
/// A whitelist that does NOT contain the exit's address must cause `dial_circuit` to fail with an
|
|
/// error mentioning "allow_extend_to" (the reason string sent in `CircuitFailed`).
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn multihop_whitelist_rejects_disallowed_exit() {
|
|
let ca = AuraCa::generate("Aura Multi-Hop Test CA").expect("ca");
|
|
let exit_proto = server_cfg(&ca, EXIT_SAN);
|
|
let relay_proto = server_cfg(&ca, RELAY_SAN);
|
|
let client_proto = client_cfg(&ca, EXIT_SAN);
|
|
|
|
let exit_port = free_udp_port();
|
|
let relay_port = free_udp_port();
|
|
let exit_addr: SocketAddr = format!("127.0.0.1:{exit_port}").parse().unwrap();
|
|
let relay_addr: SocketAddr = format!("127.0.0.1:{relay_port}").parse().unwrap();
|
|
|
|
let exit_server =
|
|
UdpServer::bind(exit_addr, exit_proto, UdpOpts::default()).expect("bind exit");
|
|
let relay_server =
|
|
UdpServer::bind(relay_addr, relay_proto, UdpOpts::default()).expect("bind relay");
|
|
let exit_actual = exit_server.local_addr().expect("exit addr");
|
|
let relay_actual = relay_server.local_addr().expect("relay addr");
|
|
|
|
// Whitelist contains a different (fake) exit; the real exit is NOT allowed.
|
|
let fake: SocketAddr = "10.255.255.1:9".parse().unwrap();
|
|
let whitelist = vec![fake];
|
|
|
|
// Exit task: just sit there; we expect the relay never bridges to it.
|
|
let _exit_task = tokio::spawn(async move {
|
|
// Accept may never resolve; exit when test ends.
|
|
let _ = exit_server.accept().await;
|
|
});
|
|
let relay_task = tokio::spawn(spawn_relay(relay_server, whitelist));
|
|
|
|
tokio::time::sleep(Duration::from_millis(20)).await;
|
|
|
|
// dial_circuit must error with a message mentioning "allow_extend_to".
|
|
let res = tokio::time::timeout(
|
|
Duration::from_secs(15),
|
|
circuit::dial_circuit_with_relay_name(
|
|
&[relay_actual, exit_actual],
|
|
client_proto,
|
|
UdpOpts::default(),
|
|
Some(RELAY_SAN),
|
|
),
|
|
)
|
|
.await
|
|
.expect("dial_circuit_with_relay_name returned within 15s");
|
|
|
|
let err = match res {
|
|
Ok(_) => panic!("dial_circuit must fail when exit is not on the whitelist"),
|
|
Err(e) => e,
|
|
};
|
|
let msg = format!("{err:#}");
|
|
assert!(
|
|
msg.contains("allow_extend_to") || msg.contains("not in"),
|
|
"expected 'allow_extend_to' / 'not in' in error, got: {msg}"
|
|
);
|
|
|
|
let _ = tokio::time::timeout(Duration::from_secs(2), relay_task).await;
|
|
}
|
|
|
|
/// When the v3.1 relay path is **disabled** at the server, the server's accept-side never reads
|
|
/// the client's ExtendBridge envelope as a control message — instead the server would treat the
|
|
/// connection as a normal VPN client. From the client's `dial_circuit` perspective the relay
|
|
/// never sends `CircuitReady`, so the client times out (`READY_TIMEOUT_SECS`-bounded).
|
|
///
|
|
/// This test exercises that exact fallback: we run a `UdpServer` with NO rendezvous task,
|
|
/// accept the connection, and just keep it open. The client's `dial_circuit` must return an Err
|
|
/// whose message mentions a timeout / CircuitReady.
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn multihop_back_compat_relay_disabled() {
|
|
let ca = AuraCa::generate("Aura Multi-Hop Test CA").expect("ca");
|
|
let exit_proto = server_cfg(&ca, EXIT_SAN);
|
|
let relay_proto = server_cfg(&ca, RELAY_SAN);
|
|
let client_proto = client_cfg(&ca, EXIT_SAN);
|
|
|
|
let exit_port = free_udp_port();
|
|
let relay_port = free_udp_port();
|
|
let exit_addr: SocketAddr = format!("127.0.0.1:{exit_port}").parse().unwrap();
|
|
let relay_addr: SocketAddr = format!("127.0.0.1:{relay_port}").parse().unwrap();
|
|
|
|
let exit_server =
|
|
UdpServer::bind(exit_addr, exit_proto, UdpOpts::default()).expect("bind exit");
|
|
let relay_server =
|
|
UdpServer::bind(relay_addr, relay_proto, UdpOpts::default()).expect("bind relay");
|
|
let exit_actual = exit_server.local_addr().expect("exit addr");
|
|
let relay_actual = relay_server.local_addr().expect("relay addr");
|
|
|
|
// Exit task: idle.
|
|
let _exit_task = tokio::spawn(async move {
|
|
let _ = exit_server.accept().await;
|
|
});
|
|
// Relay task: just accept and keep the connection alive WITHOUT running the rendezvous. This
|
|
// models a v2 server that does not know about `ExtendBridge`. The client's incoming
|
|
// `ExtendBridge` envelope is just an opaque payload from the server's perspective.
|
|
let relay_task = tokio::spawn(async move {
|
|
let conn = relay_server.accept().await.expect("relay accept");
|
|
// Hold the connection until the test ends.
|
|
tokio::time::sleep(Duration::from_secs(20)).await;
|
|
drop(conn);
|
|
});
|
|
|
|
tokio::time::sleep(Duration::from_millis(20)).await;
|
|
|
|
// The client must time out waiting for CircuitReady.
|
|
let res = tokio::time::timeout(
|
|
Duration::from_secs(20),
|
|
circuit::dial_circuit_with_relay_name(
|
|
&[relay_actual, exit_actual],
|
|
client_proto,
|
|
UdpOpts::default(),
|
|
Some(RELAY_SAN),
|
|
),
|
|
)
|
|
.await
|
|
.expect("dial_circuit returned within 20s");
|
|
|
|
let err = match res {
|
|
Ok(_) => panic!("dial_circuit must fail when the relay never sends CircuitReady"),
|
|
Err(e) => e,
|
|
};
|
|
let msg = format!("{err:#}");
|
|
assert!(
|
|
msg.contains("timeout") || msg.contains("CircuitReady"),
|
|
"expected timeout / CircuitReady in error, got: {msg}"
|
|
);
|
|
|
|
relay_task.abort();
|
|
}
|
|
|
|
// ---- v3.2: 3-hop + per-hop client certs + cell padding -----------------------------------------
|
|
|
|
use aura_cli::cells::CellPaddingConn;
|
|
use aura_cli::circuit::HopConfig;
|
|
|
|
const ENTRY_SAN: &str = "localhost-entry";
|
|
const MIDDLE_SAN: &str = "localhost-middle";
|
|
const CLIENT_ID_ENTRY: &str = "client-entry";
|
|
const CLIENT_ID_MIDDLE: &str = "client-middle";
|
|
const CLIENT_ID_EXIT: &str = "client-exit";
|
|
|
|
/// Build a [`ClientConfig`] with the given CN and expected server SAN. v3.2: a different cert /
|
|
/// CN per hop is the identity-unlinkable design.
|
|
fn client_cfg_with_cn(ca: &AuraCa, cn: &str, server_name: &str) -> ClientConfig {
|
|
let issued = ca.issue_client_cert(cn).expect("issue client cert");
|
|
ClientConfig {
|
|
ca_cert_pem: ca.ca_cert_pem(),
|
|
client_cert_pem: issued.cert_pem,
|
|
client_key_pem: issued.key_pem,
|
|
server_name: server_name.to_string(),
|
|
}
|
|
}
|
|
|
|
/// v3.2 3-hop end-to-end: `client → A (entry-relay) → B (middle-relay) → C (exit)`. Each hop is
|
|
/// a real Aura UdpServer on loopback. The client uses a **different** client cert per hop
|
|
/// (identity-unlinkable). The exit echoes three packets which the client must receive back
|
|
/// through three layers of AEAD encryption.
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn multihop_v3_2_three_hops_end_to_end() {
|
|
let ca = AuraCa::generate("Aura v3.2 3-hop Test CA").expect("ca");
|
|
|
|
let entry_proto = server_cfg(&ca, ENTRY_SAN);
|
|
let middle_proto = server_cfg(&ca, MIDDLE_SAN);
|
|
let exit_proto = server_cfg(&ca, EXIT_SAN);
|
|
|
|
let entry_port = free_udp_port();
|
|
let middle_port = free_udp_port();
|
|
let exit_port = free_udp_port();
|
|
let entry_addr: SocketAddr = format!("127.0.0.1:{entry_port}").parse().unwrap();
|
|
let middle_addr: SocketAddr = format!("127.0.0.1:{middle_port}").parse().unwrap();
|
|
let exit_addr: SocketAddr = format!("127.0.0.1:{exit_port}").parse().unwrap();
|
|
|
|
let entry_server =
|
|
UdpServer::bind(entry_addr, entry_proto, UdpOpts::default()).expect("bind entry");
|
|
let middle_server =
|
|
UdpServer::bind(middle_addr, middle_proto, UdpOpts::default()).expect("bind middle");
|
|
let exit_server =
|
|
UdpServer::bind(exit_addr, exit_proto, UdpOpts::default()).expect("bind exit");
|
|
let entry_actual = entry_server.local_addr().expect("entry addr");
|
|
let middle_actual = middle_server.local_addr().expect("middle addr");
|
|
let exit_actual = exit_server.local_addr().expect("exit addr");
|
|
|
|
// Whitelists per hop (CIDR-aware): entry allows middle; middle allows exit. Both can be exact
|
|
// entries here; this test exercises the literal-IP:port path.
|
|
let entry_whitelist = vec![middle_actual];
|
|
let middle_whitelist = vec![exit_actual];
|
|
|
|
let exit_task = tokio::spawn(spawn_exit(exit_server));
|
|
let middle_task = tokio::spawn(spawn_relay(middle_server, middle_whitelist));
|
|
let entry_task = tokio::spawn(spawn_relay(entry_server, entry_whitelist));
|
|
|
|
tokio::time::sleep(Duration::from_millis(50)).await;
|
|
|
|
// Per-hop client configs: distinct CN per hop, distinct server_name per hop.
|
|
let hops = vec![
|
|
HopConfig {
|
|
addr: entry_actual,
|
|
proto_cfg: client_cfg_with_cn(&ca, CLIENT_ID_ENTRY, ENTRY_SAN),
|
|
},
|
|
HopConfig {
|
|
addr: middle_actual,
|
|
proto_cfg: client_cfg_with_cn(&ca, CLIENT_ID_MIDDLE, MIDDLE_SAN),
|
|
},
|
|
HopConfig {
|
|
addr: exit_actual,
|
|
proto_cfg: client_cfg_with_cn(&ca, CLIENT_ID_EXIT, EXIT_SAN),
|
|
},
|
|
];
|
|
|
|
let circuit_conn = tokio::time::timeout(
|
|
Duration::from_secs(60),
|
|
circuit::dial_circuit(&hops, UdpOpts::default()),
|
|
)
|
|
.await
|
|
.expect("dial_circuit did not finish within 60s")
|
|
.expect("dial_circuit succeeded");
|
|
|
|
// peer_id is the exit's SAN — the innermost handshake authenticated the exit cert through
|
|
// every relay opaquely.
|
|
assert_eq!(
|
|
circuit_conn.peer_id(),
|
|
Some(EXIT_SAN),
|
|
"circuit.peer_id() must be the exit's SAN through 3 hops"
|
|
);
|
|
|
|
// Echo three packets — through THREE AEAD layers.
|
|
let payloads: Vec<Vec<u8>> = vec![
|
|
b"hello 3-hop".to_vec(),
|
|
vec![0x77u8; 600],
|
|
(0..200u8).collect(),
|
|
];
|
|
for pkt in &payloads {
|
|
circuit_conn.send_packet(pkt).await.expect("circuit send");
|
|
let echoed = tokio::time::timeout(Duration::from_secs(10), circuit_conn.recv_packet())
|
|
.await
|
|
.expect("recv timeout")
|
|
.expect("recv from exit through 3-hop circuit");
|
|
assert_eq!(&echoed, pkt, "echoed payload must match");
|
|
}
|
|
|
|
drop(circuit_conn);
|
|
let _ = tokio::time::timeout(Duration::from_secs(5), exit_task).await;
|
|
let _ = tokio::time::timeout(Duration::from_secs(5), middle_task).await;
|
|
let _ = tokio::time::timeout(Duration::from_secs(5), entry_task).await;
|
|
}
|
|
|
|
/// v3.2: smoke-test the [`CellPaddingConn`] wrap around a 2-hop circuit. The exit also wraps its
|
|
/// `Accepted.conn` in a `CellPaddingConn`; the bytes the client sends are padded cells, ferried
|
|
/// opaquely through the relay, and unwrapped by the exit. We exchange three payloads of varying
|
|
/// (small) sizes through the padded layer.
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn multihop_v3_2_cell_padding_smoke() {
|
|
let ca = AuraCa::generate("Aura v3.2 cell-padding Test CA").expect("ca");
|
|
let exit_proto = server_cfg(&ca, EXIT_SAN);
|
|
let relay_proto = server_cfg(&ca, RELAY_SAN);
|
|
let client_proto = client_cfg(&ca, EXIT_SAN);
|
|
|
|
let exit_port = free_udp_port();
|
|
let relay_port = free_udp_port();
|
|
let exit_addr: SocketAddr = format!("127.0.0.1:{exit_port}").parse().unwrap();
|
|
let relay_addr: SocketAddr = format!("127.0.0.1:{relay_port}").parse().unwrap();
|
|
|
|
let exit_server =
|
|
UdpServer::bind(exit_addr, exit_proto, UdpOpts::default()).expect("bind exit");
|
|
let relay_server =
|
|
UdpServer::bind(relay_addr, relay_proto, UdpOpts::default()).expect("bind relay");
|
|
let exit_actual = exit_server.local_addr().expect("exit addr");
|
|
let relay_actual = relay_server.local_addr().expect("relay addr");
|
|
|
|
let whitelist = vec![exit_actual];
|
|
|
|
// Exit echoes three CELL-PADDED packets back. The CellPaddingConn wrap on the exit's side
|
|
// means recv_packet returns the original (unpadded) payload, and send_packet pads it again.
|
|
let cell_size = 512;
|
|
let exit_task = tokio::spawn(async move {
|
|
let conn = exit_server.accept().await.expect("exit accept");
|
|
drop(exit_server);
|
|
let conn: Arc<dyn PacketConnection> = Arc::new(conn);
|
|
let wrapped = Arc::new(CellPaddingConn::new(conn, cell_size));
|
|
for _ in 0..3 {
|
|
match wrapped.recv_packet().await {
|
|
Ok(pkt) => {
|
|
if wrapped.send_packet(&pkt).await.is_err() {
|
|
return;
|
|
}
|
|
}
|
|
Err(_) => return,
|
|
}
|
|
}
|
|
});
|
|
let relay_task = tokio::spawn(spawn_relay(relay_server, whitelist));
|
|
|
|
tokio::time::sleep(Duration::from_millis(20)).await;
|
|
|
|
let circuit_conn = tokio::time::timeout(
|
|
Duration::from_secs(30),
|
|
circuit::dial_circuit_with_relay_name(
|
|
&[relay_actual, exit_actual],
|
|
client_proto,
|
|
UdpOpts::default(),
|
|
Some(RELAY_SAN),
|
|
),
|
|
)
|
|
.await
|
|
.expect("dial_circuit did not finish within 30s")
|
|
.expect("dial_circuit succeeded");
|
|
|
|
// Wrap the client side in CellPaddingConn so its sends become cells.
|
|
let padded: Arc<dyn PacketConnection> =
|
|
Arc::new(CellPaddingConn::new(circuit_conn.into_dyn(), cell_size));
|
|
|
|
let payloads: Vec<Vec<u8>> = vec![
|
|
b"tiny".to_vec(),
|
|
vec![0xEFu8; 100],
|
|
b"another payload that fits inside cell".to_vec(),
|
|
];
|
|
for pkt in &payloads {
|
|
padded.send_packet(pkt).await.expect("padded send");
|
|
let echoed = tokio::time::timeout(Duration::from_secs(10), padded.recv_packet())
|
|
.await
|
|
.expect("recv timeout")
|
|
.expect("recv from padded exit");
|
|
assert_eq!(&echoed, pkt, "padded roundtrip preserves payload");
|
|
}
|
|
|
|
drop(padded);
|
|
let _ = tokio::time::timeout(Duration::from_secs(5), exit_task).await;
|
|
let _ = tokio::time::timeout(Duration::from_secs(5), relay_task).await;
|
|
}
|