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:
@@ -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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user