Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

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}"),
        );
    }
}