feat(cli): automation bundle + identity-minimization features

Reduces manual setup steps and trims user-identifying data exposed by the
server/client, in the spirit of the deployment story: an operator on the
wire sees less, and the admin types fewer commands.

New CLI subcommands:
- `aura server-init`: one shot — pki init + issue-server + writes a ready
  server.toml with auto-detected egress iface; flags --enable-knock,
  --enable-cover-traffic, --no-nat, --run-as toggle the new transport
  defenses and privilege drop.
- `aura provision-client`: issues a client cert and assembles the full
  bundle (ca.crt + client.crt + client.key + client.toml in one directory)
  ready to hand over to the client device. --id is optional (defaults to
  a fresh UUIDv4, so client identities don't have to encode anything real).

Identity / log minimization:
- `aura pki issue-client --id` is now optional — UUIDv4 by default.
- `[server]/[client] no_logs = true` filters peer_id, client_ip,
  source_addr, client_id, local_ip, user, id, assigned_ip, peer field
  values through a custom tracing FormatFields layer (events still fire
  but the identifying fields are redacted before being written).
- `[client] bridges = [...]`: secondary server addresses; build_dial_targets
  shuffles them after the primary, so blocking one IP doesn't kill the
  client.
- Auto-detect egress iface in [server.nat] (via detect_default_egress_iface);
  egress_iface in config becomes optional with graceful fallback.

Config examples updated; backward-compatible (all new sections optional with
serde defaults). Workspace: 207 tests passed (+22), clippy -D warnings clean,
fmt clean. No new workspace deps.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
xah30
2026-05-27 12:14:57 +03:00
parent 7d711d8938
commit 8f0cf1f017
15 changed files with 1749 additions and 23 deletions
+110
View File
@@ -0,0 +1,110 @@
//! Identifier-suppressing tracing layer driven by `[server] no_logs` / `[client] no_logs`.
//!
//! ## Motivation
//!
//! Russian telecom regulations now require operators to forward identifying customer data
//! (passport / INN / IP / domain / logins / geolocation) on request. To keep an Aura node from
//! becoming a treasure-trove of those exact fields in its own logs, `no_logs = true` swaps the
//! default tracing formatter for one that skips writing a configured list of "identifier" fields
//! to the log line. The event still fires (counters and rates are unaffected), but the offending
//! field is rendered as nothing in the formatted output.
//!
//! ## Mechanism
//!
//! [`init_filtered_tracing`] installs a `tracing-subscriber::fmt` subscriber whose
//! [`FormatFields`](tracing_subscriber::fmt::FormatFields) is a custom
//! [`debug_fn`](tracing_subscriber::fmt::format::debug_fn) closure. The closure inspects
//! [`Field::name`](tracing::field::Field::name) against the [`REDACTED_FIELD_NAMES`] blacklist; on
//! a match it writes nothing (not even the field name), so the resulting log line literally
//! contains no token from the redacted value. Non-blacklisted fields go through the standard
//! `"key=value "` formatting.
//!
//! The redaction set targets the specific identifiers Aura's own code emits in its accept / dial
//! paths: `peer_id` (verified client CN), `client_ip` / `local_ip` / `assigned_ip` (per-tunnel
//! addresses), `source_addr` (UDP peer), `client_id` / `id` / `user` (generic id slots).
//!
//! ## Compatibility
//!
//! When `no_logs = false` (the default), [`init_filtered_tracing`] degenerates to the same
//! `tracing_subscriber::fmt().with_env_filter(...).try_init()` call that lived in `main` before,
//! so existing log output is unchanged for operators who did not opt in.
use std::collections::HashSet;
use tracing_subscriber::fmt::format::{debug_fn, Writer};
use tracing_subscriber::fmt::FormatFields;
use tracing_subscriber::EnvFilter;
/// Field names treated as personally-identifying and dropped from formatted output when
/// `no_logs = true`. Matches the field keys Aura emits in `tracing::info!` / `warn!` macros
/// across the server-accept and client-dial paths.
pub const REDACTED_FIELD_NAMES: &[&str] = &[
"peer_id",
"client_ip",
"source_addr",
"client_id",
"local_ip",
"user",
"id",
"assigned_ip",
"peer",
];
/// Install the global tracing subscriber, honouring `no_logs`.
///
/// * `no_logs = false`: standard `fmt` subscriber + `EnvFilter` (default `info`).
/// * `no_logs = true`: same filter and formatter shell, but the per-field writer skips
/// [`REDACTED_FIELD_NAMES`] so identifying fields never reach the output stream.
///
/// Calls `try_init` so re-initialisation (e.g. in embedded use or repeated test setup) is a
/// no-op rather than a panic.
pub fn init_filtered_tracing(no_logs: bool) {
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
if no_logs {
let _ = tracing_subscriber::fmt()
.with_env_filter(filter)
.fmt_fields(redacting_field_formatter())
.try_init();
} else {
let _ = tracing_subscriber::fmt().with_env_filter(filter).try_init();
}
}
/// Build a [`FormatFields`] that writes every field through the default rendering EXCEPT those
/// whose name matches [`REDACTED_FIELD_NAMES`] — those are silently dropped. Exposed so the
/// integration tests can swap the writer for an in-memory buffer and assert the redaction.
pub fn redacting_field_formatter() -> impl for<'w> FormatFields<'w> + 'static {
let redacted: HashSet<&'static str> = REDACTED_FIELD_NAMES.iter().copied().collect();
debug_fn(move |w: &mut Writer<'_>, field, value| {
if redacted.contains(field.name()) {
// Drop the field entirely. The default formatter emits `key=value` separated by spaces,
// so a no-op preserves that overall structure for the remaining fields.
return Ok(());
}
write!(w, "{}={:?} ", field.name(), value)
})
}
#[cfg(test)]
mod tests {
use super::*;
/// The blacklist captures every identifier the spec calls out.
#[test]
fn redacted_set_covers_spec_identifiers() {
for name in [
"peer_id",
"client_ip",
"source_addr",
"client_id",
"local_ip",
"user",
"id",
] {
assert!(
REDACTED_FIELD_NAMES.contains(&name),
"missing redaction for {name}"
);
}
}
}