8f0cf1f017
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>
111 lines
4.5 KiB
Rust
111 lines
4.5 KiB
Rust
//! 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}"
|
|
);
|
|
}
|
|
}
|
|
}
|