Files
AuraVPN/crates/aura-cli/src/client.rs
T
xah30 cb89312a27 feat(cli): implement Wave 4 — aura binary (PKI, server/client, admin, bench)
aura-cli: clap command tree (pki init/issue-server/issue-client/revoke/list,
server, client, route add/list/remove, status, bench-crypto); TOML config with
~ expansion and split-tunnel rules -> RouteTable; JSON-over-Unix-socket admin
IPC; server/client data paths wiring transport + tunnel (TUN run needs root).
config/{server,client}.toml.example. 15 tests (pki roundtrip, config parse,
admin-socket roundtrip, loopback connection). Verified the real binary: --help,
bench-crypto, and a full CA->server->client cert workflow.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 18:36:13 +03:00

140 lines
5.3 KiB
Rust

//! `aura client`: connect to an Aura server and route host traffic through the tunnel.
//!
//! ## v1 data path
//! 1. Load `client.toml`, read the `[pki]` PEM files, build [`aura_proto::ClientConfig`].
//! 2. Build a shared [`RouteTable`] from `[tunnel.split]` (default action + direct/vpn CIDR rules);
//! record domain rules for resolution.
//! 3. [`AuraClient::connect`] to `[client] server_addr`, presenting `[client] sni` as the outer
//! (mimicry) hostname.
//! 4. Resolve any split-tunnel domain rules via [`AuraDns`] into host routes (best-effort).
//! 5. Create the local TUN ([`AuraTun::create`]) on `[tunnel] local_ip/prefix` and run
//! [`AuraRouter`] to bridge the TUN and the connection.
//! 6. Start the admin IPC listener over the same shared [`RouteTable`] + [`Stats`].
//!
//! ## Privilege / scope notes (NOT auto-tested)
//! * Creating the TUN ([`AuraTun::create`]) needs **root**; the live router path runs only in a
//! privileged execution, so it is not covered by unit tests (the loopback test covers the
//! connection path short of the TUN).
//! * Domain resolution performs real DNS queries and so is not unit-tested either.
use std::path::Path;
use std::sync::Arc;
use anyhow::Context;
use aura_transport::AuraClient;
use aura_tunnel::{AuraDns, AuraRouter, AuraTun};
use tokio::sync::RwLock;
use crate::admin::{self, AdminState, Stats};
use crate::config::ClientConfigFile;
/// Entry point for `aura client --config <PATH>` (and optional `--admin-socket`).
pub async fn run(config_path: &Path, admin_socket: &str) -> anyhow::Result<()> {
let cfg = ClientConfigFile::load(config_path)?;
let server_addr = cfg.server_socket_addr()?;
let local_ip = cfg.local_ip()?;
let proto_cfg = cfg.to_proto()?;
let (table, domains) = cfg.build_route_table()?;
tracing::info!(
name = %cfg.client.name,
%server_addr,
sni = %cfg.client.sni,
%local_ip,
dns = ?cfg.tunnel.dns,
mimicry_padding = cfg.mimicry.padding,
"starting Aura client"
);
// Snapshot the configured CIDR rules for the admin mirror before moving the table behind the
// lock. (We rebuild the parsed CIDRs from the config rather than reaching into the table.)
let cidr_mirror = collect_cidr_rules(&cfg);
let routes = Arc::new(RwLock::new(table));
let stats = Arc::new(Stats::new());
// Connect (outer QUIC + inner Aura mutual-auth handshake).
let conn = AuraClient::connect(server_addr, &cfg.client.sni, proto_cfg)
.await
.context("connecting to Aura server")?;
let peer = conn.peer_id().map(str::to_owned);
stats.set_peer_id(peer.clone());
tracing::info!(peer = ?peer, "connected and authenticated to server");
// Resolve split-tunnel domain rules into host routes (best-effort; failures are logged).
if !domains.is_empty() {
match AuraDns::new(Arc::clone(&routes)).await {
Ok(mut dns) => {
for (domain, action) in &domains {
match dns.resolve_and_register(domain, *action).await {
Ok(ips) => {
tracing::info!(
domain,
count = ips.len(),
?action,
"resolved domain rule"
)
}
Err(e) => {
tracing::warn!(domain, error = %e, "failed to resolve domain rule")
}
}
}
}
Err(e) => tracing::warn!(error = %e, "could not start resolver for domain rules"),
}
}
// Admin IPC over the shared table/stats.
let admin_state = AdminState::new(
Arc::clone(&routes),
Arc::clone(&stats),
cidr_mirror,
domains.clone(),
);
let admin_path = admin_socket.to_string();
tokio::spawn(async move {
if let Err(e) = admin::serve(&admin_path, admin_state).await {
tracing::error!(error = %e, "admin IPC listener exited");
}
});
// Create the TUN and run the router (needs root).
let tun = AuraTun::create(
&cfg.tunnel.tun_name,
local_ip,
cfg.tunnel.prefix,
cfg.tunnel.mtu,
)
.await
.context("creating TUN device (needs root)")?;
tracing::info!(tun = %cfg.tunnel.tun_name, "TUN device up; routing traffic");
let router = AuraRouter::new(tun, routes, conn.into_dyn());
router.run().await.context("router run loop")?;
Ok(())
}
/// Re-parse the `[tunnel.split]` CIDR rules into `(IpNetwork, RouteAction)` pairs for the admin
/// mirror. Invalid CIDRs were already rejected by [`ClientConfigFile::build_route_table`], so this
/// silently skips any that somehow fail to re-parse.
fn collect_cidr_rules(
cfg: &ClientConfigFile,
) -> Vec<(ipnetwork::IpNetwork, aura_tunnel::RouteAction)> {
use aura_tunnel::RouteAction;
let mut out = Vec::new();
for (rules, action) in [
(&cfg.tunnel.split.direct, RouteAction::Direct),
(&cfg.tunnel.split.vpn, RouteAction::Vpn),
] {
for rule in rules {
if let Some(cidr) = &rule.cidr {
if let Ok(net) = cidr.parse() {
out.push((net, action));
}
}
}
}
out
}