server/lib/src/refusals.rs
//! ★ **Every refusal leaves an audit row** (lane HOL-NCSC, NCSC P1 item 5).
//!
//! The import door writes its refusals into its own sink; what refuses outside a door that
//! holds one (a dial [`crate::egress`] stops, an importer the duties refuse, a TLS handshake a
//! door refuses) writes here, into the server's one audit sink, which [`crate::Holger::start`]
//! hands over once it is open. Before that (and in a CLI verb that opens no server) a refusal
//! is logged only.
//!
//! Refused TLS handshakes come from the network, so they are held to
//! [`HANDSHAKE_ROWS_PER_MINUTE`] rows a minute; the rest of that minute is one summary row.
use std::sync::{Arc, Mutex, RwLock};
use crate::audit::{AuditAction, AuditEvent, AuditLog};
use crate::lock::{LockOrRecover as _, RwLockOrRecover as _};
static SINK: RwLock<Option<Arc<dyn AuditLog>>> = RwLock::new(None);
/// The audit sink refusals are written to (the server's own, at its start).
pub fn set_sink(log: Arc<dyn AuditLog>) {
*SINK.write_or_recover() = Some(log);
}
/// **Write the refusal of `what`** (`egress`, `import authorization`, `TLS handshake`, …):
/// who asked (`actor`, empty = the server itself), from where (`source`), and the refusal
/// with its code (`detail`). Best effort: a sink that fails is logged, never a panic.
pub fn record(what: &str, actor: &str, source: &str, detail: &str) {
let Some(log) = SINK.read_or_recover().clone() else {
log::debug!("refused (no audit sink yet): {what}: {detail}");
return;
};
record_to(&log, what, actor, source, detail);
}
/// [`record`] into `log`, a door's own sink (two servers in one process each keep theirs).
pub fn record_to(log: &Arc<dyn AuditLog>, what: &str, actor: &str, source: &str, detail: &str) {
let actor = if actor.trim().is_empty() {
"holger"
} else {
actor
};
let ev = AuditEvent::new(actor, AuditAction::Other, "", what, source, 403, 0)
.with_detail(format!("refused: {what}: {detail}"));
if let Err(e) = log.record(ev).and_then(|()| log.flush()) {
log::error!("refused: {what}: {detail} — and the audit row could not be written: {e:?}");
}
}
/// How many refused TLS handshakes one minute writes row by row.
pub const HANDSHAKE_ROWS_PER_MINUTE: u32 = 60;
/// `(minute, rows written, rows held back)`.
static HANDSHAKES: Mutex<(i64, u32, u32)> = Mutex::new((0, 0, 0));
/// **A TLS handshake `door` refused from `peer`**, said `why`; at most
/// [`HANDSHAKE_ROWS_PER_MINUTE`] rows a minute, then one row saying how many were held back.
pub fn handshake_refused(log: Option<&Arc<dyn AuditLog>>, door: &str, peer: &str, why: &str) {
let write_row = |what: &str, actor: &str, source: &str, detail: &str| match log {
Some(l) => record_to(l, what, actor, source, detail),
None => record(what, actor, source, detail),
};
let minute = crate::audit::now_nanos() / 60_000_000_000;
let (write, held) = {
let mut w = HANDSHAKES.lock_or_recover();
let mut held = None;
if w.0 != minute {
if w.2 > 0 {
held = Some(w.2);
}
*w = (minute, 0, 0);
}
if w.1 < HANDSHAKE_ROWS_PER_MINUTE {
w.1 += 1;
(true, held)
} else {
w.2 += 1;
(false, held)
}
};
if let Some(n) = held {
write_row("TLS handshake", "anonymous", "", &format!("TLS handshake refused: {n} more refused handshakes in the last minute were not written row by row"));
}
if write {
write_row(
"TLS handshake",
"anonymous",
peer,
&format!("TLS handshake refused at the {door} from {peer}: {why}"),
);
}
}