1use nono::undo::{NetworkAuditDecision, NetworkAuditEvent, NetworkAuditMode};
8use std::sync::{Arc, Mutex};
9use std::time::{SystemTime, UNIX_EPOCH};
10use tracing::{info, warn};
11
12const MAX_AUDIT_EVENTS: usize = 4096;
14
15pub type SharedAuditLog = Arc<Mutex<Vec<NetworkAuditEvent>>>;
17
18#[derive(Debug, Clone, Copy)]
20pub enum ProxyMode {
21 Connect,
23 Reverse,
25 External,
27}
28
29impl std::fmt::Display for ProxyMode {
30 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31 match self {
32 ProxyMode::Connect => write!(f, "connect"),
33 ProxyMode::Reverse => write!(f, "reverse"),
34 ProxyMode::External => write!(f, "external"),
35 }
36 }
37}
38
39#[must_use]
41pub fn new_audit_log() -> SharedAuditLog {
42 Arc::new(Mutex::new(Vec::new()))
43}
44
45#[must_use]
47pub fn drain_audit_events(audit_log: &SharedAuditLog) -> Vec<NetworkAuditEvent> {
48 match audit_log.lock() {
49 Ok(mut events) => events.drain(..).collect(),
50 Err(e) => {
51 warn!(
52 "Network audit log mutex poisoned while draining events: {}",
53 e
54 );
55 Vec::new()
56 }
57 }
58}
59
60fn now_unix_millis() -> u64 {
61 match SystemTime::now().duration_since(UNIX_EPOCH) {
62 Ok(duration) => {
63 let millis = duration.as_millis();
64 if millis > u128::from(u64::MAX) {
65 warn!("System clock millis exceeded u64::MAX; clamping audit timestamp");
66 u64::MAX
67 } else {
68 millis as u64
69 }
70 }
71 Err(e) => {
72 warn!(
73 "System clock before UNIX_EPOCH while generating audit timestamp: {}",
74 e
75 );
76 0
77 }
78 }
79}
80
81fn map_mode(mode: ProxyMode) -> NetworkAuditMode {
82 match mode {
83 ProxyMode::Connect => NetworkAuditMode::Connect,
84 ProxyMode::Reverse => NetworkAuditMode::Reverse,
85 ProxyMode::External => NetworkAuditMode::External,
86 }
87}
88
89fn push_event(audit_log: Option<&SharedAuditLog>, event: NetworkAuditEvent) {
90 let Some(audit_log) = audit_log else {
91 return;
92 };
93
94 match audit_log.lock() {
95 Ok(mut events) => {
96 if events.len() < MAX_AUDIT_EVENTS {
97 events.push(event);
98 } else {
99 warn!(
100 "Network audit buffer full ({} events); dropping event",
101 MAX_AUDIT_EVENTS
102 );
103 }
104 }
105 Err(e) => {
106 warn!(
107 "Network audit log mutex poisoned while recording event: {}",
108 e
109 );
110 }
111 }
112}
113
114pub fn log_allowed(
116 audit_log: Option<&SharedAuditLog>,
117 mode: ProxyMode,
118 host: &str,
119 port: u16,
120 method: &str,
121) {
122 info!(
123 target: "nono_proxy::audit",
124 mode = %mode,
125 host = host,
126 port = port,
127 method = method,
128 decision = "allow",
129 "proxy request allowed"
130 );
131
132 push_event(
133 audit_log,
134 NetworkAuditEvent {
135 timestamp_unix_ms: now_unix_millis(),
136 mode: map_mode(mode),
137 decision: NetworkAuditDecision::Allow,
138 target: host.to_string(),
139 port: Some(port),
140 method: Some(method.to_string()),
141 path: None,
142 status: None,
143 reason: None,
144 },
145 );
146}
147
148pub fn log_denied(
150 audit_log: Option<&SharedAuditLog>,
151 mode: ProxyMode,
152 host: &str,
153 port: u16,
154 reason: &str,
155) {
156 info!(
157 target: "nono_proxy::audit",
158 mode = %mode,
159 host = host,
160 port = port,
161 decision = "deny",
162 reason = reason,
163 "proxy request denied"
164 );
165
166 push_event(
167 audit_log,
168 NetworkAuditEvent {
169 timestamp_unix_ms: now_unix_millis(),
170 mode: map_mode(mode),
171 decision: NetworkAuditDecision::Deny,
172 target: host.to_string(),
173 port: Some(port),
174 method: None,
175 path: None,
176 status: None,
177 reason: Some(reason.to_string()),
178 },
179 );
180}
181
182pub fn log_reverse_proxy(
184 audit_log: Option<&SharedAuditLog>,
185 service: &str,
186 method: &str,
187 path: &str,
188 status: u16,
189) {
190 info!(
191 target: "nono_proxy::audit",
192 mode = "reverse",
193 service = service,
194 method = method,
195 path = path,
196 status = status,
197 "reverse proxy response"
198 );
199
200 push_event(
201 audit_log,
202 NetworkAuditEvent {
203 timestamp_unix_ms: now_unix_millis(),
204 mode: NetworkAuditMode::Reverse,
205 decision: NetworkAuditDecision::Allow,
206 target: service.to_string(),
207 port: None,
208 method: Some(method.to_string()),
209 path: Some(path.to_string()),
210 status: Some(status),
211 reason: None,
212 },
213 );
214}
215
216#[cfg(test)]
217#[allow(clippy::unwrap_used)]
218mod tests {
219 use super::*;
220
221 #[test]
222 fn log_allowed_records_event() {
223 let log = new_audit_log();
224
225 log_allowed(
226 Some(&log),
227 ProxyMode::Connect,
228 "api.openai.com",
229 443,
230 "CONNECT",
231 );
232
233 let events = drain_audit_events(&log);
234 assert_eq!(events.len(), 1);
235 let event = &events[0];
236 assert_eq!(event.mode, NetworkAuditMode::Connect);
237 assert_eq!(event.decision, NetworkAuditDecision::Allow);
238 assert_eq!(event.target, "api.openai.com");
239 assert_eq!(event.port, Some(443));
240 assert_eq!(event.method.as_deref(), Some("CONNECT"));
241 assert!(event.timestamp_unix_ms > 0);
242 }
243
244 #[test]
245 fn log_denied_records_reason() {
246 let log = new_audit_log();
247
248 log_denied(
249 Some(&log),
250 ProxyMode::External,
251 "169.254.169.254",
252 80,
253 "blocked by metadata deny list",
254 );
255
256 let events = drain_audit_events(&log);
257 assert_eq!(events.len(), 1);
258 let event = &events[0];
259 assert_eq!(event.mode, NetworkAuditMode::External);
260 assert_eq!(event.decision, NetworkAuditDecision::Deny);
261 assert_eq!(
262 event.reason.as_deref(),
263 Some("blocked by metadata deny list")
264 );
265 }
266}