use nono::undo::{NetworkAuditDecision, NetworkAuditEvent, NetworkAuditMode};
use std::sync::{Arc, Mutex};
use std::time::{SystemTime, UNIX_EPOCH};
use tracing::{info, warn};
const MAX_AUDIT_EVENTS: usize = 4096;
pub type SharedAuditLog = Arc<Mutex<Vec<NetworkAuditEvent>>>;
#[derive(Debug, Clone, Copy)]
pub enum ProxyMode {
Connect,
Reverse,
External,
}
impl std::fmt::Display for ProxyMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ProxyMode::Connect => write!(f, "connect"),
ProxyMode::Reverse => write!(f, "reverse"),
ProxyMode::External => write!(f, "external"),
}
}
}
#[must_use]
pub fn new_audit_log() -> SharedAuditLog {
Arc::new(Mutex::new(Vec::new()))
}
#[must_use]
pub fn drain_audit_events(audit_log: &SharedAuditLog) -> Vec<NetworkAuditEvent> {
match audit_log.lock() {
Ok(mut events) => events.drain(..).collect(),
Err(e) => {
warn!(
"Network audit log mutex poisoned while draining events: {}",
e
);
Vec::new()
}
}
}
fn now_unix_millis() -> u64 {
match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(duration) => {
let millis = duration.as_millis();
if millis > u128::from(u64::MAX) {
warn!("System clock millis exceeded u64::MAX; clamping audit timestamp");
u64::MAX
} else {
millis as u64
}
}
Err(e) => {
warn!(
"System clock before UNIX_EPOCH while generating audit timestamp: {}",
e
);
0
}
}
}
fn map_mode(mode: ProxyMode) -> NetworkAuditMode {
match mode {
ProxyMode::Connect => NetworkAuditMode::Connect,
ProxyMode::Reverse => NetworkAuditMode::Reverse,
ProxyMode::External => NetworkAuditMode::External,
}
}
fn push_event(audit_log: Option<&SharedAuditLog>, event: NetworkAuditEvent) {
let Some(audit_log) = audit_log else {
return;
};
match audit_log.lock() {
Ok(mut events) => {
if events.len() < MAX_AUDIT_EVENTS {
events.push(event);
} else {
warn!(
"Network audit buffer full ({} events); dropping event",
MAX_AUDIT_EVENTS
);
}
}
Err(e) => {
warn!(
"Network audit log mutex poisoned while recording event: {}",
e
);
}
}
}
pub fn log_allowed(
audit_log: Option<&SharedAuditLog>,
mode: ProxyMode,
host: &str,
port: u16,
method: &str,
) {
info!(
target: "nono_proxy::audit",
mode = %mode,
host = host,
port = port,
method = method,
decision = "allow",
"proxy request allowed"
);
push_event(
audit_log,
NetworkAuditEvent {
timestamp_unix_ms: now_unix_millis(),
mode: map_mode(mode),
decision: NetworkAuditDecision::Allow,
target: host.to_string(),
port: Some(port),
method: Some(method.to_string()),
path: None,
status: None,
reason: None,
},
);
}
pub fn log_denied(
audit_log: Option<&SharedAuditLog>,
mode: ProxyMode,
host: &str,
port: u16,
reason: &str,
) {
info!(
target: "nono_proxy::audit",
mode = %mode,
host = host,
port = port,
decision = "deny",
reason = reason,
"proxy request denied"
);
push_event(
audit_log,
NetworkAuditEvent {
timestamp_unix_ms: now_unix_millis(),
mode: map_mode(mode),
decision: NetworkAuditDecision::Deny,
target: host.to_string(),
port: Some(port),
method: None,
path: None,
status: None,
reason: Some(reason.to_string()),
},
);
}
pub fn log_reverse_proxy(
audit_log: Option<&SharedAuditLog>,
service: &str,
method: &str,
path: &str,
status: u16,
) {
info!(
target: "nono_proxy::audit",
mode = "reverse",
service = service,
method = method,
path = path,
status = status,
"reverse proxy response"
);
push_event(
audit_log,
NetworkAuditEvent {
timestamp_unix_ms: now_unix_millis(),
mode: NetworkAuditMode::Reverse,
decision: NetworkAuditDecision::Allow,
target: service.to_string(),
port: None,
method: Some(method.to_string()),
path: Some(path.to_string()),
status: Some(status),
reason: None,
},
);
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn log_allowed_records_event() {
let log = new_audit_log();
log_allowed(
Some(&log),
ProxyMode::Connect,
"api.openai.com",
443,
"CONNECT",
);
let events = drain_audit_events(&log);
assert_eq!(events.len(), 1);
let event = &events[0];
assert_eq!(event.mode, NetworkAuditMode::Connect);
assert_eq!(event.decision, NetworkAuditDecision::Allow);
assert_eq!(event.target, "api.openai.com");
assert_eq!(event.port, Some(443));
assert_eq!(event.method.as_deref(), Some("CONNECT"));
assert!(event.timestamp_unix_ms > 0);
}
#[test]
fn log_denied_records_reason() {
let log = new_audit_log();
log_denied(
Some(&log),
ProxyMode::External,
"169.254.169.254",
80,
"blocked by metadata deny list",
);
let events = drain_audit_events(&log);
assert_eq!(events.len(), 1);
let event = &events[0];
assert_eq!(event.mode, NetworkAuditMode::External);
assert_eq!(event.decision, NetworkAuditDecision::Deny);
assert_eq!(
event.reason.as_deref(),
Some("blocked by metadata deny list")
);
}
}