cb89312a27
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>
140 lines
5.3 KiB
Rust
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
|
|
}
|