vanguards_rs/
bandguards.rs

1//! Bandwidth monitoring for detecting side-channel attacks.
2//!
3//! This module provides protection against bandwidth-based side-channel attacks
4//! by monitoring circuit bandwidth usage and detecting anomalies.
5//!
6//! # Overview
7//!
8//! The bandguards system monitors:
9//!
10//! - **Circuit bandwidth**: Read/written bytes per circuit
11//! - **Dropped cells**: Cells received but not delivered (potential attack indicator)
12//! - **Circuit age**: Old circuits that may be vulnerable
13//! - **Guard connections**: Connection state and closure correlation
14//!
15//! # Circuit State Diagram
16//!
17//! Circuits progress through the following states, with bandwidth monitoring at each stage:
18//!
19//! ```text
20//! ┌─────────────────────────────────────────────────────────────────────────┐
21//! │                      Circuit State Transitions                          │
22//! │                                                                         │
23//! │                         ┌─────────────┐                                 │
24//! │                         │  LAUNCHED   │                                 │
25//! │                         │ (created)   │                                 │
26//! │                         └──────┬──────┘                                 │
27//! │                                │                                        │
28//! │                                ▼                                        │
29//! │                         ┌─────────────┐                                 │
30//! │                         │  EXTENDED   │◀────────┐                      │
31//! │                         │ (building)  │         │                       │
32//! │                         └──────┬──────┘         │                       │
33//! │                                │                │                       │
34//! │                    ┌───────────┼───────────┐    │                       │
35//! │                    │           │           │    │                       │
36//! │                    ▼           ▼           ▼    │                       │
37//! │             ┌───────────┐ ┌─────────┐ ┌────────┴───┐                    │
38//! │             │   BUILT   │ │ FAILED  │ │ GUARD_WAIT │                    │
39//! │             │ (active)  │ │         │ │            │                    │
40//! │             └─────┬─────┘ └────┬────┘ └────────────┘                    │
41//! │                   │            │                                        │
42//! │    ┌──────────────┼────────────┘                                        │
43//! │    │              │                                                     │
44//! │    │              ▼                                                     │
45//! │    │       ┌─────────────┐                                              │
46//! │    │       │  Bandwidth  │                                              │
47//! │    │       │  Monitoring │                                              │
48//! │    │       │  (CIRC_BW)  │                                              │
49//! │    │       └──────┬──────┘                                              │
50//! │    │              │                                                     │
51//! │    │    ┌─────────┼─────────┐                                           │
52//! │    │    │         │         │                                           │
53//! │    │    ▼         ▼         ▼                                           │
54//! │    │ ┌──────┐ ┌──────┐ ┌──────────┐                                     │
55//! │    │ │Normal│ │Attack│ │Tor Bug   │                                     │
56//! │    │ │      │ │Detect│ │Workaround│                                     │
57//! │    │ └──┬───┘ └──┬───┘ └────┬─────┘                                     │
58//! │    │    │        │          │                                           │
59//! │    │    │        ▼          │                                           │
60//! │    │    │   ┌─────────┐     │                                           │
61//! │    │    │   │ CLOSE   │     │                                           │
62//! │    │    │   │ CIRCUIT │     │                                           │
63//! │    │    │   └─────────┘     │                                           │
64//! │    │    │                   │                                           │
65//! │    └────┼───────────────────┘                                           │
66//! │         │                                                               │
67//! │         ▼                                                               │
68//! │  ┌─────────────┐                                                        │
69//! │  │   CLOSED    │                                                        │
70//! │  │ (cleanup)   │                                                        │
71//! │  └─────────────┘                                                        │
72//! └─────────────────────────────────────────────────────────────────────────┘
73//! ```
74//!
75//! # Attack Detection
76//!
77//! Several attack vectors are detected and mitigated:
78//!
79//! ```text
80//! ┌─────────────────────────────────────────────────────────────────────────┐
81//! │                        Attack Detection Matrix                          │
82//! │                                                                         │
83//! │  Attack Type          │ Detection Method        │ Response              │
84//! │  ─────────────────────┼─────────────────────────┼───────────────────────│
85//! │  Dropped Cells        │ read - delivered ≠ 0    │ Close circuit         │
86//! │  Excessive Bandwidth  │ bytes > max_megabytes   │ Close circuit         │
87//! │  HSDIR Abuse          │ hsdir bytes > limit     │ Close circuit         │
88//! │  Intro Abuse          │ intro bytes > limit     │ Close circuit         │
89//! │  Old Circuits         │ age > max_age_hours     │ Close circuit         │
90//! │  Guard Conn Kill      │ conn close + circ fail  │ Log warning           │
91//! │  Network Disconnect   │ no conns for N secs     │ Log warning           │
92//! └─────────────────────────────────────────────────────────────────────────┘
93//! ```
94//!
95//! # Dropped Cell Detection
96//!
97//! Dropped cells indicate cells that were received but not delivered to the
98//! application. This can indicate a tagging attack or protocol manipulation.
99//!
100//! ```text
101//! Formula: dropped = read_bytes / CELL_PAYLOAD_SIZE
102//!                  - (delivered_read + overhead_read) / RELAY_PAYLOAD_SIZE
103//!
104//! Where:
105//!   CELL_PAYLOAD_SIZE = 509 bytes
106//!   RELAY_PAYLOAD_SIZE = 498 bytes (509 - 11 byte header)
107//! ```
108//!
109//! # Tor Bug Workarounds
110//!
111//! This module includes workarounds for known Tor bugs that can cause
112//! false positive dropped cell detection:
113//!
114//! | Bug ID | Description | Affected Circuits |
115//! |--------|-------------|-------------------|
116//! | #29699 | Intro circuits get duped cells on retries | HS_SERVICE_INTRO |
117//! | #29700 | Service rend circuits fail ntor handshake | HS_SERVICE_REND |
118//! | #29786 | Path bias circuits have dropped cell cases | PATH_BIAS_TESTING |
119//! | #29927 | Client-side dropped cells and protocol errors | HS_CLIENT_* |
120//! | #40359 | Client intro circuits with dropped cells | CIRCUIT_PADDING |
121//!
122//! # What This Module Does NOT Do
123//!
124//! - **Circuit creation**: Use Tor's circuit building
125//! - **Guard selection**: Use [`crate::vanguards`] for guard management
126//! - **Rendezvous monitoring**: Use [`crate::rendguard`] for RP tracking
127//!
128//! # Example
129//!
130//! ```rust,no_run
131//! use vanguards_rs::bandguards::{BandwidthStats, CircuitLimitResult};
132//! use vanguards_rs::config::BandguardsConfig;
133//!
134//! let mut stats = BandwidthStats::new();
135//! let config = BandguardsConfig::default();
136//!
137//! // Process circuit events
138//! stats.circ_event("123", "LAUNCHED", "HS_SERVICE_REND",
139//!                  Some("HSSR_CONNECTING"), &[], None, 1000.0);
140//! stats.circ_event("123", "BUILT", "HS_SERVICE_REND",
141//!                  Some("HSSR_CONNECTING"), &["A".repeat(40)], None, 1001.0);
142//!
143//! // Process bandwidth events
144//! stats.circbw_event("123", 1000, 500, 800, 400, 100, 50, 1002.0);
145//!
146//! // Check for attacks
147//! match stats.check_circuit_limits("123", &config) {
148//!     CircuitLimitResult::Ok => println!("Circuit OK"),
149//!     CircuitLimitResult::DroppedCells { dropped_cells } => {
150//!         println!("Attack detected: {} dropped cells", dropped_cells);
151//!     }
152//!     _ => {}
153//! }
154//! ```
155//!
156//! # Security Considerations
157//!
158//! - Dropped cell detection may have false positives due to Tor bugs
159//! - Configure appropriate thresholds for your threat model
160//! - Monitor logs for attack patterns
161//! - Consider enabling `close_circuits` only after testing
162//!
163//! # See Also
164//!
165//! - [`crate::config::BandguardsConfig`] - Configuration options
166//! - [`crate::control`] - Event handling and circuit closure
167//! - [Python vanguards bandguards](https://github.com/mikeperry-tor/vanguards) - Original implementation
168//! - [Tor Bug Tracker](https://gitlab.torproject.org/tpo/core/tor/-/issues) - Bug references
169
170use std::collections::HashMap;
171
172use crate::config::BandguardsConfig;
173
174/// Cell payload size in bytes.
175pub const CELL_PAYLOAD_SIZE: u64 = 509;
176
177/// Relay header size in bytes.
178pub const RELAY_HEADER_SIZE: u64 = 11;
179
180/// Relay payload size (cell payload minus relay header).
181pub const RELAY_PAYLOAD_SIZE: u64 = CELL_PAYLOAD_SIZE - RELAY_HEADER_SIZE;
182
183/// Seconds per hour.
184const SECS_PER_HOUR: u64 = 3600;
185
186/// Bytes per kilobyte.
187const BYTES_PER_KB: u64 = 1024;
188
189/// Bytes per megabyte.
190const BYTES_PER_MB: u64 = 1024 * BYTES_PER_KB;
191
192/// Maximum lag between guard connection close and circuit destroy events.
193pub const MAX_CIRC_DESTROY_LAG_SECS: u64 = 2;
194
195/// Per-circuit bandwidth statistics for attack detection.
196///
197/// Tracks all bandwidth-related information for a single circuit,
198/// including read/written bytes, delivered/overhead bytes, and
199/// circuit state information.
200///
201/// # Circuit Tracking
202///
203/// ```text
204/// ┌─────────────────────────────────────────────────────────────────────────┐
205/// │                        BwCircuitStat Fields                              │
206/// │                                                                          │
207/// │  Identity          │ State              │ Bandwidth                     │
208/// │  ──────────────────┼────────────────────┼───────────────────────────────│
209/// │  circ_id           │ purpose            │ read_bytes                    │
210/// │  guard_fp          │ hs_state           │ sent_bytes                    │
211/// │  created_at        │ old_purpose        │ delivered_read_bytes          │
212/// │                    │ old_hs_state       │ delivered_sent_bytes          │
213/// │                    │ built              │ overhead_read_bytes           │
214/// │                    │ in_use             │ overhead_sent_bytes           │
215/// │                                                                          │
216/// │  Flags             │ Attack Detection                                   │
217/// │  ──────────────────┼────────────────────────────────────────────────────│
218/// │  is_hs             │ dropped_cells_allowed                              │
219/// │  is_service        │ possibly_destroyed_at                              │
220/// │  is_hsdir          │                                                    │
221/// │  is_serv_intro     │                                                    │
222/// └─────────────────────────────────────────────────────────────────────────┘
223/// ```
224///
225/// # Dropped Cell Detection
226///
227/// Dropped cells are detected using the formula:
228/// ```text
229/// dropped = read_bytes / CELL_PAYLOAD_SIZE - (delivered_read + overhead_read) / RELAY_PAYLOAD_SIZE
230/// ```
231///
232/// # Circuit Types
233///
234/// | Flag | Description |
235/// |------|-------------|
236/// | `is_hs` | Hidden service circuit (client or service) |
237/// | `is_service` | Service-side circuit (vs client-side) |
238/// | `is_hsdir` | HSDIR circuit for descriptor operations |
239/// | `is_serv_intro` | Service introduction circuit |
240///
241/// # Example
242///
243/// ```rust
244/// use vanguards_rs::bandguards::BwCircuitStat;
245///
246/// let mut circ = BwCircuitStat::new("123".to_string(), true);
247/// circ.read_bytes = 5090;  // 10 cells
248/// circ.delivered_read_bytes = 3984;  // 8 cells delivered
249///
250/// let dropped = circ.dropped_read_cells();
251/// println!("Dropped cells: {}", dropped);
252/// ```
253///
254/// # See Also
255///
256/// - [`BandwidthStats`] - Main tracking structure
257/// - [`CircuitLimitResult`] - Limit check results
258#[derive(Debug, Clone)]
259pub struct BwCircuitStat {
260    /// Circuit ID.
261    pub circ_id: String,
262    /// Whether this is a hidden service circuit.
263    pub is_hs: bool,
264    /// Whether this is a service-side circuit (vs client).
265    pub is_service: bool,
266    /// Whether this is an HSDIR circuit.
267    pub is_hsdir: bool,
268    /// Whether this is a service intro circuit.
269    pub is_serv_intro: bool,
270    /// Number of dropped cells allowed (for Tor bug workarounds).
271    pub dropped_cells_allowed: u64,
272    /// Current circuit purpose.
273    pub purpose: Option<String>,
274    /// Current hidden service state.
275    pub hs_state: Option<String>,
276    /// Previous circuit purpose (before PURPOSE_CHANGED).
277    pub old_purpose: Option<String>,
278    /// Previous hidden service state.
279    pub old_hs_state: Option<String>,
280    /// Whether the circuit is in use.
281    pub in_use: bool,
282    /// Whether the circuit has been built.
283    pub built: bool,
284    /// Unix timestamp when the circuit was created.
285    pub created_at: f64,
286    /// Total bytes read on this circuit.
287    pub read_bytes: u64,
288    /// Total bytes sent on this circuit.
289    pub sent_bytes: u64,
290    /// Delivered read bytes (application data).
291    pub delivered_read_bytes: u64,
292    /// Delivered sent bytes (application data).
293    pub delivered_sent_bytes: u64,
294    /// Overhead read bytes (protocol overhead).
295    pub overhead_read_bytes: u64,
296    /// Overhead sent bytes (protocol overhead).
297    pub overhead_sent_bytes: u64,
298    /// Guard fingerprint for this circuit.
299    pub guard_fp: Option<String>,
300    /// Timestamp when the circuit may have been destroyed due to guard closure.
301    pub possibly_destroyed_at: Option<f64>,
302}
303
304impl BwCircuitStat {
305    /// Creates a new circuit stat entry.
306    ///
307    /// # Arguments
308    ///
309    /// * `circ_id` - The circuit ID
310    /// * `is_hs` - Whether this is a hidden service circuit
311    pub fn new(circ_id: String, is_hs: bool) -> Self {
312        Self {
313            circ_id,
314            is_hs,
315            is_service: true,
316            is_hsdir: false,
317            is_serv_intro: false,
318            dropped_cells_allowed: 0,
319            purpose: None,
320            hs_state: None,
321            old_purpose: None,
322            old_hs_state: None,
323            in_use: false,
324            built: false,
325            created_at: std::time::SystemTime::now()
326                .duration_since(std::time::UNIX_EPOCH)
327                .unwrap_or_default()
328                .as_secs_f64(),
329            read_bytes: 0,
330            sent_bytes: 0,
331            delivered_read_bytes: 0,
332            delivered_sent_bytes: 0,
333            overhead_read_bytes: 0,
334            overhead_sent_bytes: 0,
335            guard_fp: None,
336            possibly_destroyed_at: None,
337        }
338    }
339
340    /// Returns the total bytes (read + sent) on this circuit.
341    pub fn total_bytes(&self) -> u64 {
342        self.read_bytes + self.sent_bytes
343    }
344
345    /// Calculates the number of dropped read cells.
346    ///
347    /// Dropped cells are cells that were received but not delivered to the
348    /// application. This can indicate an attack or a Tor bug.
349    ///
350    /// # Formula
351    ///
352    /// ```text
353    /// dropped = read_bytes / CELL_PAYLOAD_SIZE - (delivered_read + overhead_read) / RELAY_PAYLOAD_SIZE
354    /// ```
355    ///
356    /// # Returns
357    ///
358    /// The number of dropped cells. Can be negative due to timing issues.
359    pub fn dropped_read_cells(&self) -> i64 {
360        let cells_received = self.read_bytes / CELL_PAYLOAD_SIZE;
361        let cells_delivered =
362            (self.delivered_read_bytes + self.overhead_read_bytes) / RELAY_PAYLOAD_SIZE;
363        cells_received as i64 - cells_delivered as i64
364    }
365
366    /// Returns the circuit age in seconds.
367    pub fn age_secs(&self) -> f64 {
368        let now = std::time::SystemTime::now()
369            .duration_since(std::time::UNIX_EPOCH)
370            .unwrap_or_default()
371            .as_secs_f64();
372        now - self.created_at
373    }
374
375    /// Returns the circuit age in hours.
376    pub fn age_hours(&self) -> f64 {
377        self.age_secs() / SECS_PER_HOUR as f64
378    }
379}
380
381/// Per-guard connection statistics.
382///
383/// Tracks connection state and closure information for a single guard relay.
384#[derive(Debug, Clone)]
385pub struct BwGuardStat {
386    /// Guard fingerprint.
387    pub to_guard: String,
388    /// Number of connections killed with live circuits.
389    pub killed_conns: u32,
390    /// Timestamp of last killed connection.
391    pub killed_conn_at: f64,
392    /// Whether a killed connection is pending correlation.
393    pub killed_conn_pending: bool,
394    /// Total connections made to this guard.
395    pub conns_made: u32,
396    /// Close reasons and their counts.
397    pub close_reasons: HashMap<String, u32>,
398}
399
400impl BwGuardStat {
401    /// Creates a new guard stat entry.
402    ///
403    /// # Arguments
404    ///
405    /// * `guard_fp` - The guard's fingerprint
406    pub fn new(guard_fp: String) -> Self {
407        Self {
408            to_guard: guard_fp,
409            killed_conns: 0,
410            killed_conn_at: 0.0,
411            killed_conn_pending: false,
412            conns_made: 0,
413            close_reasons: HashMap::new(),
414        }
415    }
416
417    /// Records a close reason.
418    pub fn record_close_reason(&mut self, reason: &str) {
419        *self.close_reasons.entry(reason.to_string()).or_insert(0) += 1;
420    }
421}
422
423/// Main bandwidth monitoring state for attack detection.
424///
425/// Tracks all circuit and guard connection statistics for bandwidth monitoring.
426/// This is the primary interface for the bandguards protection system.
427///
428/// # Architecture
429///
430/// ```text
431/// ┌─────────────────────────────────────────────────────────────────────────┐
432/// │                         BandwidthStats                                   │
433/// │                                                                          │
434/// │  ┌─────────────────────────────────────────────────────────────────┐    │
435/// │  │ Circuit Tracking (circs: HashMap<String, BwCircuitStat>)        │    │
436/// │  │ • Per-circuit bandwidth statistics                              │    │
437/// │  │ • Dropped cell detection                                        │    │
438/// │  │ • Purpose and state tracking                                    │    │
439/// │  └─────────────────────────────────────────────────────────────────┘    │
440/// │                                                                          │
441/// │  ┌─────────────────────────────────────────────────────────────────┐    │
442/// │  │ Guard Tracking (guards: HashMap<String, BwGuardStat>)           │    │
443/// │  │ • Connection state per guard                                    │    │
444/// │  │ • Killed connection correlation                                 │    │
445/// │  │ • Close reason tracking                                         │    │
446/// │  └─────────────────────────────────────────────────────────────────┘    │
447/// │                                                                          │
448/// │  ┌─────────────────────────────────────────────────────────────────┐    │
449/// │  │ Connectivity Tracking                                           │    │
450/// │  │ • no_conns_since: When all guard connections were lost          │    │
451/// │  │ • no_circs_since: When all circuits started failing             │    │
452/// │  │ • network_down_since: When network liveness went down           │    │
453/// │  └─────────────────────────────────────────────────────────────────┘    │
454/// └─────────────────────────────────────────────────────────────────────────┘
455/// ```
456///
457/// # Event Handling
458///
459/// This struct processes several Tor event types:
460///
461/// | Event | Method | Purpose |
462/// |-------|--------|---------|
463/// | ORCONN | `orconn_event` | Guard connection state changes |
464/// | CIRC | `circ_event` | Circuit state changes |
465/// | CIRC_MINOR | `circ_minor_event` | Purpose changes |
466/// | CIRC_BW | `circbw_event` | Bandwidth updates |
467/// | BW | `check_connectivity` | Periodic connectivity checks |
468/// | NETWORK_LIVENESS | `network_liveness_event` | Network state changes |
469///
470/// # Example
471///
472/// ```rust
473/// use vanguards_rs::bandguards::BandwidthStats;
474/// use vanguards_rs::config::BandguardsConfig;
475///
476/// let mut stats = BandwidthStats::new();
477/// let config = BandguardsConfig::default();
478///
479/// // Track a guard connection
480/// stats.orconn_event("1", &"A".repeat(40), "CONNECTED", None, 1000.0);
481///
482/// // Track a circuit
483/// stats.circ_event("123", "LAUNCHED", "GENERAL", None, &[], None, 1000.0);
484/// stats.circ_event("123", "BUILT", "GENERAL", None, &["A".repeat(40)], None, 1001.0);
485///
486/// // Check connectivity
487/// let status = stats.check_connectivity(1002.0, &config);
488/// ```
489///
490/// # See Also
491///
492/// - [`BwCircuitStat`] - Per-circuit statistics
493/// - [`BwGuardStat`] - Per-guard statistics
494/// - [`CircuitLimitResult`] - Limit check results
495/// - [`ConnectivityStatus`] - Connectivity check results
496#[derive(Debug, Clone)]
497pub struct BandwidthStats {
498    /// Circuit statistics by circuit ID.
499    pub circs: HashMap<String, BwCircuitStat>,
500    /// Live guard connections by connection ID.
501    pub live_guard_conns: HashMap<String, BwGuardStat>,
502    /// All guard statistics by fingerprint.
503    pub guards: HashMap<String, BwGuardStat>,
504    /// Total circuits destroyed.
505    pub circs_destroyed_total: u64,
506    /// Timestamp when all connections were lost (None if connected).
507    pub no_conns_since: Option<f64>,
508    /// Timestamp when circuits started failing (None if working).
509    pub no_circs_since: Option<f64>,
510    /// Timestamp when network went down (None if up).
511    pub network_down_since: Option<f64>,
512    /// Maximum fake ID used for initial orconn-status entries.
513    pub max_fake_id: i32,
514    /// Whether we're currently disconnected (circuits failing).
515    pub disconnected_circs: bool,
516    /// Whether we're currently disconnected (no connections).
517    pub disconnected_conns: bool,
518}
519
520impl Default for BandwidthStats {
521    fn default() -> Self {
522        Self::new()
523    }
524}
525
526impl BandwidthStats {
527    /// Creates a new bandwidth stats tracker.
528    pub fn new() -> Self {
529        Self {
530            circs: HashMap::new(),
531            live_guard_conns: HashMap::new(),
532            guards: HashMap::new(),
533            circs_destroyed_total: 0,
534            no_conns_since: Some(
535                std::time::SystemTime::now()
536                    .duration_since(std::time::UNIX_EPOCH)
537                    .unwrap_or_default()
538                    .as_secs_f64(),
539            ),
540            no_circs_since: None,
541            network_down_since: None,
542            max_fake_id: -1,
543            disconnected_circs: false,
544            disconnected_conns: false,
545        }
546    }
547
548    /// Handles an ORCONN event.
549    ///
550    /// Tracks guard connection state changes. When a connection closes,
551    /// marks any circuits using that guard as possibly destroyed.
552    ///
553    /// # Arguments
554    ///
555    /// * `conn_id` - Connection ID
556    /// * `guard_fp` - Guard fingerprint
557    /// * `status` - Connection status (CONNECTED, CLOSED, FAILED)
558    /// * `reason` - Close reason (for CLOSED status)
559    /// * `arrived_at` - Event timestamp
560    pub fn orconn_event(
561        &mut self,
562        conn_id: &str,
563        guard_fp: &str,
564        status: &str,
565        reason: Option<&str>,
566        arrived_at: f64,
567    ) {
568        // Ensure guard entry exists
569        if !self.guards.contains_key(guard_fp) {
570            self.guards
571                .insert(guard_fp.to_string(), BwGuardStat::new(guard_fp.to_string()));
572        }
573
574        match status {
575            "CONNECTED" => {
576                if self.disconnected_conns {
577                    self.disconnected_conns = false;
578                }
579                self.live_guard_conns
580                    .insert(conn_id.to_string(), BwGuardStat::new(guard_fp.to_string()));
581                if let Some(guard) = self.guards.get_mut(guard_fp) {
582                    guard.conns_made += 1;
583                }
584                self.no_conns_since = None;
585            }
586            "CLOSED" | "FAILED" => {
587                // Try to fix up fake IDs
588                let actual_conn_id = self.fixup_orconn_id(conn_id, guard_fp);
589
590                if self.live_guard_conns.contains_key(&actual_conn_id) {
591                    // Mark circuits as possibly destroyed
592                    for circ in self.circs.values_mut() {
593                        if circ.in_use && circ.guard_fp.as_deref() == Some(guard_fp) {
594                            circ.possibly_destroyed_at = Some(arrived_at);
595                            if let Some(guard) = self.guards.get_mut(guard_fp) {
596                                guard.killed_conn_at = arrived_at;
597                            }
598                        }
599                    }
600
601                    self.live_guard_conns.remove(&actual_conn_id);
602
603                    if self.live_guard_conns.is_empty() && self.no_conns_since.is_none() {
604                        self.no_conns_since = Some(arrived_at);
605                    }
606                }
607
608                // Record close reason
609                if status == "CLOSED" {
610                    if let Some(r) = reason {
611                        if let Some(guard) = self.guards.get_mut(guard_fp) {
612                            guard.record_close_reason(r);
613                        }
614                    }
615                }
616            }
617            _ => {}
618        }
619    }
620
621    /// Fixes up connection IDs for fake IDs from initial orconn-status.
622    fn fixup_orconn_id(&self, conn_id: &str, guard_fp: &str) -> String {
623        // Check if this is a fake ID that needs fixing
624        if let Ok(id) = conn_id.parse::<i32>() {
625            if id <= self.max_fake_id {
626                // Look for matching guard in live connections
627                for (fake_id, stat) in &self.live_guard_conns {
628                    if stat.to_guard == guard_fp {
629                        if let Ok(fid) = fake_id.parse::<i32>() {
630                            if fid <= self.max_fake_id {
631                                return fake_id.clone();
632                            }
633                        }
634                    }
635                }
636            }
637        }
638        conn_id.to_string()
639    }
640
641    /// Handles a CIRC event.
642    ///
643    /// Tracks circuit state changes including creation, building, and closure.
644    ///
645    /// # Arguments
646    ///
647    /// * `circ_id` - Circuit ID
648    /// * `status` - Circuit status (LAUNCHED, BUILT, EXTENDED, FAILED, CLOSED)
649    /// * `purpose` - Circuit purpose
650    /// * `hs_state` - Hidden service state
651    /// * `path` - Circuit path (list of relay fingerprints)
652    /// * `remote_reason` - Remote close reason
653    /// * `arrived_at` - Event timestamp
654    ///
655    /// # Returns
656    ///
657    /// `Some(true)` if circuit was destroyed due to guard connection closure,
658    /// `Some(false)` if circuit was closed normally, `None` otherwise.
659    #[allow(clippy::too_many_arguments)]
660    pub fn circ_event(
661        &mut self,
662        circ_id: &str,
663        status: &str,
664        purpose: &str,
665        hs_state: Option<&str>,
666        path: &[String],
667        remote_reason: Option<&str>,
668        arrived_at: f64,
669    ) -> Option<bool> {
670        // Handle circuit failures for connectivity tracking
671        if status == "FAILED"
672            && self.no_circs_since.is_none()
673            && self.any_circuits_pending(Some(circ_id))
674        {
675            self.no_circs_since = Some(arrived_at);
676        }
677
678        // Handle circuit closure
679        if status == "FAILED" || status == "CLOSED" {
680            if let Some(circ) = self.circs.remove(circ_id) {
681                if circ.in_use && circ.possibly_destroyed_at.is_some() {
682                    if let Some(destroyed_at) = circ.possibly_destroyed_at {
683                        if arrived_at - destroyed_at <= MAX_CIRC_DESTROY_LAG_SECS as f64
684                            && remote_reason == Some("CHANNEL_CLOSED")
685                        {
686                            // Circuit was destroyed due to guard connection closure
687                            if let Some(guard_fp) = &circ.guard_fp {
688                                if let Some(guard) = self.guards.get_mut(guard_fp) {
689                                    guard.killed_conn_at = 0.0;
690                                    guard.killed_conns += 1;
691                                }
692                            }
693                            self.circs_destroyed_total += 1;
694                            return Some(true);
695                        }
696                    }
697                }
698                return Some(false);
699            }
700            return None;
701        }
702
703        // Create circuit entry if needed
704        let is_hs = hs_state.is_some() || purpose.starts_with("HS");
705        if !self.circs.contains_key(circ_id) {
706            let mut circ = BwCircuitStat::new(circ_id.to_string(), is_hs);
707
708            // Set service/client based on purpose
709            if purpose.starts_with("HS_CLIENT") {
710                circ.is_service = false;
711            } else if purpose.starts_with("HS_SERVICE") {
712                circ.is_service = true;
713            }
714
715            // Set HSDIR and intro flags
716            if purpose == "HS_CLIENT_HSDIR" || purpose == "HS_SERVICE_HSDIR" {
717                circ.is_hsdir = true;
718            } else if purpose == "HS_SERVICE_INTRO" {
719                circ.is_serv_intro = true;
720            }
721
722            self.circs.insert(circ_id.to_string(), circ);
723        }
724
725        // Update circuit state
726        if let Some(circ) = self.circs.get_mut(circ_id) {
727            circ.purpose = Some(purpose.to_string());
728            circ.hs_state = hs_state.map(|s| s.to_string());
729
730            // Handle BUILT and GUARD_WAIT
731            if status == "BUILT" || status == "GUARD_WAIT" {
732                circ.built = true;
733
734                if self.disconnected_circs {
735                    self.disconnected_circs = false;
736                }
737                self.no_circs_since = None;
738
739                // Mark as in_use if HS purpose
740                if purpose.starts_with("HS_CLIENT") || purpose.starts_with("HS_SERVICE") {
741                    circ.in_use = true;
742                    if !path.is_empty() {
743                        circ.guard_fp = Some(path[0].clone());
744                    }
745                }
746            } else if status == "EXTENDED" {
747                if self.disconnected_circs {
748                    self.disconnected_circs = false;
749                }
750                self.no_circs_since = None;
751            }
752        }
753
754        None
755    }
756
757    /// Handles a CIRC_MINOR event (purpose changes).
758    ///
759    /// Tracks circuit purpose changes, particularly from HS_VANGUARDS to
760    /// actual HS purposes.
761    ///
762    /// # Arguments
763    ///
764    /// * `circ_id` - Circuit ID
765    /// * `event_type` - Event type (PURPOSE_CHANGED, etc.)
766    /// * `purpose` - New circuit purpose
767    /// * `hs_state` - New hidden service state
768    /// * `old_purpose` - Previous circuit purpose
769    /// * `old_hs_state` - Previous hidden service state
770    /// * `path` - Circuit path
771    #[allow(clippy::too_many_arguments)]
772    pub fn circ_minor_event(
773        &mut self,
774        circ_id: &str,
775        event_type: &str,
776        purpose: &str,
777        hs_state: Option<&str>,
778        old_purpose: Option<&str>,
779        old_hs_state: Option<&str>,
780        path: &[String],
781    ) {
782        if let Some(circ) = self.circs.get_mut(circ_id) {
783            circ.purpose = Some(purpose.to_string());
784            circ.hs_state = hs_state.map(|s| s.to_string());
785            circ.old_purpose = old_purpose.map(|s| s.to_string());
786            circ.old_hs_state = old_hs_state.map(|s| s.to_string());
787
788            // Update service/client flag
789            if purpose.starts_with("HS_CLIENT") {
790                circ.is_service = false;
791            } else if purpose.starts_with("HS_SERVICE") {
792                circ.is_service = true;
793            }
794
795            // Update HSDIR and intro flags
796            if purpose == "HS_CLIENT_HSDIR" || purpose == "HS_SERVICE_HSDIR" {
797                circ.is_hsdir = true;
798            } else if purpose == "HS_SERVICE_INTRO" {
799                circ.is_serv_intro = true;
800            }
801
802            // PURPOSE_CHANGED from HS_VANGUARDS -> in_use
803            if event_type == "PURPOSE_CHANGED" && old_purpose == Some("HS_VANGUARDS") {
804                circ.in_use = true;
805                if !path.is_empty() {
806                    circ.guard_fp = Some(path[0].clone());
807                }
808            }
809        }
810    }
811
812    /// Handles a CIRC_BW event (bandwidth update).
813    ///
814    /// Updates circuit bandwidth statistics and checks limits.
815    ///
816    /// # Arguments
817    ///
818    /// * `circ_id` - Circuit ID
819    /// * `read` - Bytes read
820    /// * `written` - Bytes written
821    /// * `delivered_read` - Delivered read bytes
822    /// * `delivered_written` - Delivered written bytes
823    /// * `overhead_read` - Overhead read bytes
824    /// * `overhead_written` - Overhead written bytes
825    /// * `arrived_at` - Event timestamp
826    #[allow(clippy::too_many_arguments)]
827    pub fn circbw_event(
828        &mut self,
829        circ_id: &str,
830        read: u64,
831        written: u64,
832        delivered_read: u64,
833        delivered_written: u64,
834        overhead_read: u64,
835        overhead_written: u64,
836        _arrived_at: f64,
837    ) {
838        // Circuit bandwidth means circuits are working
839        if self.disconnected_circs {
840            self.disconnected_circs = false;
841        }
842        self.no_circs_since = None;
843
844        if let Some(circ) = self.circs.get_mut(circ_id) {
845            circ.read_bytes += read;
846            circ.sent_bytes += written;
847            circ.delivered_read_bytes += delivered_read;
848            circ.delivered_sent_bytes += delivered_written;
849            circ.overhead_read_bytes += overhead_read;
850            circ.overhead_sent_bytes += overhead_written;
851        }
852    }
853
854    /// Checks circuit limits and returns circuits that should be closed.
855    ///
856    /// Checks for:
857    /// - Dropped cells (potential attack)
858    /// - Maximum bytes exceeded
859    /// - Maximum HSDIR bytes exceeded
860    /// - Maximum service intro bytes exceeded
861    ///
862    /// # Arguments
863    ///
864    /// * `circ_id` - Circuit ID to check
865    /// * `config` - Bandguards configuration
866    ///
867    /// # Returns
868    ///
869    /// A [`CircuitLimitResult`] indicating whether the circuit should be closed
870    /// and why.
871    pub fn check_circuit_limits(
872        &self,
873        circ_id: &str,
874        config: &BandguardsConfig,
875    ) -> CircuitLimitResult {
876        let circ = match self.circs.get(circ_id) {
877            Some(c) => c,
878            None => return CircuitLimitResult::Ok,
879        };
880
881        // Check dropped cells
882        let dropped = circ.dropped_read_cells();
883        if dropped > circ.dropped_cells_allowed as i64 {
884            // Check for Tor bug workarounds
885            let tor_bug = self.check_tor_bug_workaround(circ, dropped);
886            if let Some(bug_id) = tor_bug {
887                return CircuitLimitResult::TorBug {
888                    bug_id,
889                    dropped_cells: dropped,
890                };
891            }
892
893            if circ.built {
894                return CircuitLimitResult::DroppedCells {
895                    dropped_cells: dropped,
896                };
897            }
898        }
899
900        // Check max bytes
901        if config.circ_max_megabytes > 0
902            && circ.total_bytes() > config.circ_max_megabytes * BYTES_PER_MB
903        {
904            return CircuitLimitResult::MaxBytesExceeded {
905                bytes: circ.total_bytes(),
906                limit: config.circ_max_megabytes * BYTES_PER_MB,
907            };
908        }
909
910        // Check HSDIR bytes
911        if config.circ_max_hsdesc_kilobytes > 0
912            && circ.is_hsdir
913            && circ.total_bytes() > config.circ_max_hsdesc_kilobytes as u64 * BYTES_PER_KB
914        {
915            return CircuitLimitResult::HsdirBytesExceeded {
916                bytes: circ.total_bytes(),
917                limit: config.circ_max_hsdesc_kilobytes as u64 * BYTES_PER_KB,
918            };
919        }
920
921        // Check service intro bytes
922        if config.circ_max_serv_intro_kilobytes > 0
923            && circ.is_serv_intro
924            && circ.total_bytes() > config.circ_max_serv_intro_kilobytes as u64 * BYTES_PER_KB
925        {
926            return CircuitLimitResult::ServIntroBytesExceeded {
927                bytes: circ.total_bytes(),
928                limit: config.circ_max_serv_intro_kilobytes as u64 * BYTES_PER_KB,
929            };
930        }
931
932        CircuitLimitResult::Ok
933    }
934
935    /// Checks for Tor bug workarounds for dropped cells.
936    fn check_tor_bug_workaround(
937        &self,
938        circ: &BwCircuitStat,
939        _dropped: i64,
940    ) -> Option<&'static str> {
941        let purpose = circ.purpose.as_deref().unwrap_or("");
942        let hs_state = circ.hs_state.as_deref().unwrap_or("");
943        let old_purpose = circ.old_purpose.as_deref().unwrap_or("");
944        let old_hs_state = circ.old_hs_state.as_deref().unwrap_or("");
945
946        // Bug #29699: Intro circuits can get duped cells on retries
947        if purpose == "HS_SERVICE_INTRO" && hs_state == "HSSI_ESTABLISHED" {
948            return Some("#29699");
949        }
950
951        // Bug #40359: Client intro circuits
952        if purpose == "CIRCUIT_PADDING"
953            && old_purpose == "HS_CLIENT_INTRO"
954            && old_hs_state == "HSCI_INTRO_SENT"
955        {
956            return Some("#40359");
957        }
958
959        // Bug #29927: Client-side dropped cells
960        if purpose == "HS_CLIENT_REND" || (purpose == "HS_CLIENT_INTRO" && hs_state == "HSCI_DONE")
961        {
962            return Some("#29927");
963        }
964
965        // Bug #29700: Service rend circuits
966        if purpose == "HS_SERVICE_REND" && hs_state == "HSSR_CONNECTING" {
967            return Some("#29700");
968        }
969
970        // Bug #29786: Path bias testing
971        if purpose == "PATH_BIAS_TESTING" {
972            return Some("#29786");
973        }
974
975        None
976    }
977
978    /// Returns circuits that have exceeded the maximum age.
979    ///
980    /// # Arguments
981    ///
982    /// * `config` - Bandguards configuration
983    ///
984    /// # Returns
985    ///
986    /// A list of circuit IDs that should be closed due to age.
987    pub fn get_aged_circuits(&self, config: &BandguardsConfig) -> Vec<String> {
988        if config.circ_max_age_hours == 0 {
989            return Vec::new();
990        }
991
992        let max_age_secs = config.circ_max_age_hours as f64 * SECS_PER_HOUR as f64;
993        self.circs
994            .iter()
995            .filter(|(_, circ)| circ.age_secs() > max_age_secs)
996            .map(|(id, _)| id.clone())
997            .collect()
998    }
999
1000    /// Checks connectivity status and returns warnings if disconnected.
1001    ///
1002    /// # Arguments
1003    ///
1004    /// * `now` - Current timestamp
1005    /// * `config` - Bandguards configuration
1006    ///
1007    /// # Returns
1008    ///
1009    /// A [`ConnectivityStatus`] indicating the current connectivity state.
1010    pub fn check_connectivity(
1011        &mut self,
1012        now: f64,
1013        config: &BandguardsConfig,
1014    ) -> ConnectivityStatus {
1015        // Check connection disconnection
1016        if let Some(no_conns_since) = self.no_conns_since {
1017            let disconnected_secs = (now - no_conns_since) as u32;
1018
1019            if config.conn_max_disconnected_secs > 0
1020                && disconnected_secs >= config.conn_max_disconnected_secs
1021                && (!self.disconnected_conns
1022                    || disconnected_secs.is_multiple_of(config.conn_max_disconnected_secs))
1023            {
1024                self.disconnected_conns = true;
1025                return ConnectivityStatus::NoConnections {
1026                    secs: disconnected_secs,
1027                };
1028            }
1029        } else if let Some(no_circs_since) = self.no_circs_since {
1030            let disconnected_secs = (now - no_circs_since) as u32;
1031
1032            if config.circ_max_disconnected_secs > 0
1033                && disconnected_secs >= config.circ_max_disconnected_secs
1034                && self.any_circuits_pending(None)
1035                && (!self.disconnected_circs
1036                    || disconnected_secs.is_multiple_of(config.circ_max_disconnected_secs))
1037            {
1038                self.disconnected_circs = true;
1039                return ConnectivityStatus::CircuitsFailing {
1040                    secs: disconnected_secs,
1041                    network_down_secs: self.network_down_since.map(|t| (now - t) as u32),
1042                };
1043            }
1044        }
1045
1046        ConnectivityStatus::Connected
1047    }
1048
1049    /// Handles a NETWORK_LIVENESS event.
1050    ///
1051    /// # Arguments
1052    ///
1053    /// * `status` - Network status ("UP" or "DOWN")
1054    /// * `arrived_at` - Event timestamp
1055    pub fn network_liveness_event(&mut self, status: &str, arrived_at: f64) {
1056        match status {
1057            "UP" => {
1058                self.network_down_since = None;
1059            }
1060            "DOWN" => {
1061                self.network_down_since = Some(arrived_at);
1062            }
1063            _ => {}
1064        }
1065    }
1066
1067    /// Returns true if any circuits are pending (not yet built).
1068    fn any_circuits_pending(&self, except_id: Option<&str>) -> bool {
1069        self.circs
1070            .iter()
1071            .any(|(id, circ)| !circ.built && except_id.is_none_or(|e| id != e))
1072    }
1073
1074    /// Returns the number of tracked circuits.
1075    pub fn circuit_count(&self) -> usize {
1076        self.circs.len()
1077    }
1078
1079    /// Returns the number of live guard connections.
1080    pub fn live_connection_count(&self) -> usize {
1081        self.live_guard_conns.len()
1082    }
1083}
1084
1085/// Result of checking circuit limits.
1086#[derive(Debug, Clone, PartialEq)]
1087pub enum CircuitLimitResult {
1088    /// Circuit is within limits.
1089    Ok,
1090    /// Circuit has dropped cells (potential attack).
1091    DroppedCells {
1092        /// Number of dropped cells.
1093        dropped_cells: i64,
1094    },
1095    /// Dropped cells due to known Tor bug.
1096    TorBug {
1097        /// Tor bug ID.
1098        bug_id: &'static str,
1099        /// Number of dropped cells.
1100        dropped_cells: i64,
1101    },
1102    /// Circuit exceeded maximum bytes.
1103    MaxBytesExceeded {
1104        /// Current bytes.
1105        bytes: u64,
1106        /// Configured limit.
1107        limit: u64,
1108    },
1109    /// HSDIR circuit exceeded maximum bytes.
1110    HsdirBytesExceeded {
1111        /// Current bytes.
1112        bytes: u64,
1113        /// Configured limit.
1114        limit: u64,
1115    },
1116    /// Service intro circuit exceeded maximum bytes.
1117    ServIntroBytesExceeded {
1118        /// Current bytes.
1119        bytes: u64,
1120        /// Configured limit.
1121        limit: u64,
1122    },
1123}
1124
1125/// Connectivity status result.
1126#[derive(Debug, Clone, PartialEq)]
1127pub enum ConnectivityStatus {
1128    /// Connected and working.
1129    Connected,
1130    /// No guard connections.
1131    NoConnections {
1132        /// Seconds disconnected.
1133        secs: u32,
1134    },
1135    /// Circuits are failing.
1136    CircuitsFailing {
1137        /// Seconds circuits have been failing.
1138        secs: u32,
1139        /// Seconds network has been down (if known).
1140        network_down_secs: Option<u32>,
1141    },
1142}
1143
1144#[cfg(test)]
1145mod tests {
1146    use super::*;
1147
1148    #[test]
1149    fn test_bw_circuit_stat_new() {
1150        let circ = BwCircuitStat::new("123".to_string(), true);
1151        assert_eq!(circ.circ_id, "123");
1152        assert!(circ.is_hs);
1153        assert!(circ.is_service);
1154        assert!(!circ.is_hsdir);
1155        assert!(!circ.is_serv_intro);
1156        assert_eq!(circ.read_bytes, 0);
1157        assert_eq!(circ.sent_bytes, 0);
1158    }
1159
1160    #[test]
1161    fn test_total_bytes() {
1162        let mut circ = BwCircuitStat::new("123".to_string(), true);
1163        circ.read_bytes = 1000;
1164        circ.sent_bytes = 500;
1165        assert_eq!(circ.total_bytes(), 1500);
1166    }
1167
1168    #[test]
1169    fn test_dropped_read_cells() {
1170        let mut circ = BwCircuitStat::new("123".to_string(), true);
1171
1172        // 10 cells received (10 * 509 = 5090 bytes)
1173        circ.read_bytes = 5090;
1174        // 8 cells delivered (8 * 498 = 3984 bytes)
1175        circ.delivered_read_bytes = 3984;
1176        circ.overhead_read_bytes = 0;
1177
1178        // Should have 2 dropped cells
1179        assert_eq!(circ.dropped_read_cells(), 2);
1180    }
1181
1182    #[test]
1183    fn test_dropped_read_cells_with_overhead() {
1184        let mut circ = BwCircuitStat::new("123".to_string(), true);
1185
1186        // 10 cells received
1187        circ.read_bytes = 5090;
1188        // 7 delivered + 1 overhead = 8 cells accounted for
1189        circ.delivered_read_bytes = 3486; // 7 * 498
1190        circ.overhead_read_bytes = 498; // 1 * 498
1191
1192        // Should have 2 dropped cells
1193        assert_eq!(circ.dropped_read_cells(), 2);
1194    }
1195
1196    #[test]
1197    fn test_bw_guard_stat_new() {
1198        let guard = BwGuardStat::new("A".repeat(40));
1199        assert_eq!(guard.to_guard, "A".repeat(40));
1200        assert_eq!(guard.killed_conns, 0);
1201        assert_eq!(guard.conns_made, 0);
1202        assert!(guard.close_reasons.is_empty());
1203    }
1204
1205    #[test]
1206    fn test_record_close_reason() {
1207        let mut guard = BwGuardStat::new("A".repeat(40));
1208        guard.record_close_reason("DONE");
1209        guard.record_close_reason("DONE");
1210        guard.record_close_reason("ERROR");
1211
1212        assert_eq!(guard.close_reasons.get("DONE"), Some(&2));
1213        assert_eq!(guard.close_reasons.get("ERROR"), Some(&1));
1214    }
1215
1216    #[test]
1217    fn test_bandwidth_stats_new() {
1218        let stats = BandwidthStats::new();
1219        assert!(stats.circs.is_empty());
1220        assert!(stats.live_guard_conns.is_empty());
1221        assert!(stats.guards.is_empty());
1222        assert_eq!(stats.circs_destroyed_total, 0);
1223        assert!(stats.no_conns_since.is_some());
1224    }
1225
1226    #[test]
1227    fn test_orconn_event_connected() {
1228        let mut stats = BandwidthStats::new();
1229        let fp = "A".repeat(40);
1230
1231        stats.orconn_event("1", &fp, "CONNECTED", None, 1000.0);
1232
1233        assert!(stats.live_guard_conns.contains_key("1"));
1234        assert!(stats.guards.contains_key(&fp));
1235        assert_eq!(stats.guards.get(&fp).unwrap().conns_made, 1);
1236        assert!(stats.no_conns_since.is_none());
1237    }
1238
1239    #[test]
1240    fn test_orconn_event_closed() {
1241        let mut stats = BandwidthStats::new();
1242        let fp = "A".repeat(40);
1243
1244        stats.orconn_event("1", &fp, "CONNECTED", None, 1000.0);
1245        stats.orconn_event("1", &fp, "CLOSED", Some("DONE"), 1001.0);
1246
1247        assert!(!stats.live_guard_conns.contains_key("1"));
1248        assert!(stats.no_conns_since.is_some());
1249        assert_eq!(
1250            stats.guards.get(&fp).unwrap().close_reasons.get("DONE"),
1251            Some(&1)
1252        );
1253    }
1254
1255    #[test]
1256    fn test_circ_event_creates_circuit() {
1257        let mut stats = BandwidthStats::new();
1258
1259        stats.circ_event(
1260            "123",
1261            "LAUNCHED",
1262            "HS_SERVICE_REND",
1263            Some("HSSR_CONNECTING"),
1264            &[],
1265            None,
1266            1000.0,
1267        );
1268
1269        assert!(stats.circs.contains_key("123"));
1270        let circ = stats.circs.get("123").unwrap();
1271        assert!(circ.is_hs);
1272        assert!(circ.is_service);
1273    }
1274
1275    #[test]
1276    fn test_circ_event_built() {
1277        let mut stats = BandwidthStats::new();
1278        let path = vec!["A".repeat(40)];
1279
1280        stats.circ_event(
1281            "123",
1282            "LAUNCHED",
1283            "HS_SERVICE_REND",
1284            Some("HSSR_CONNECTING"),
1285            &[],
1286            None,
1287            1000.0,
1288        );
1289        stats.circ_event(
1290            "123",
1291            "BUILT",
1292            "HS_SERVICE_REND",
1293            Some("HSSR_CONNECTING"),
1294            &path,
1295            None,
1296            1001.0,
1297        );
1298
1299        let circ = stats.circs.get("123").unwrap();
1300        assert!(circ.built);
1301        assert!(circ.in_use);
1302        assert_eq!(circ.guard_fp, Some("A".repeat(40)));
1303    }
1304
1305    #[test]
1306    fn test_circbw_event() {
1307        let mut stats = BandwidthStats::new();
1308
1309        stats.circ_event("123", "LAUNCHED", "GENERAL", None, &[], None, 1000.0);
1310        stats.circbw_event("123", 1000, 500, 800, 400, 100, 50, 1001.0);
1311
1312        let circ = stats.circs.get("123").unwrap();
1313        assert_eq!(circ.read_bytes, 1000);
1314        assert_eq!(circ.sent_bytes, 500);
1315        assert_eq!(circ.delivered_read_bytes, 800);
1316        assert_eq!(circ.delivered_sent_bytes, 400);
1317        assert_eq!(circ.overhead_read_bytes, 100);
1318        assert_eq!(circ.overhead_sent_bytes, 50);
1319    }
1320
1321    #[test]
1322    fn test_check_circuit_limits_ok() {
1323        let mut stats = BandwidthStats::new();
1324        let config = BandguardsConfig::default();
1325
1326        stats.circ_event("123", "LAUNCHED", "GENERAL", None, &[], None, 1000.0);
1327
1328        let result = stats.check_circuit_limits("123", &config);
1329        assert_eq!(result, CircuitLimitResult::Ok);
1330    }
1331
1332    #[test]
1333    fn test_check_circuit_limits_max_bytes() {
1334        let mut stats = BandwidthStats::new();
1335        let config = BandguardsConfig {
1336            circ_max_megabytes: 1, // 1 MB limit
1337            ..Default::default()
1338        };
1339
1340        stats.circ_event("123", "BUILT", "GENERAL", None, &[], None, 1000.0);
1341        // Set read_bytes with matching delivered bytes to avoid dropped cell detection
1342        let bytes = 2 * BYTES_PER_MB;
1343        let delivered = (bytes / CELL_PAYLOAD_SIZE) * RELAY_PAYLOAD_SIZE;
1344        stats.circbw_event("123", bytes, 0, delivered, 0, 0, 0, 1001.0);
1345
1346        let result = stats.check_circuit_limits("123", &config);
1347        match result {
1348            CircuitLimitResult::MaxBytesExceeded { bytes: b, limit } => {
1349                assert_eq!(b, bytes);
1350                assert_eq!(limit, BYTES_PER_MB);
1351            }
1352            _ => panic!("Expected MaxBytesExceeded, got {:?}", result),
1353        }
1354    }
1355
1356    #[test]
1357    fn test_network_liveness_event() {
1358        let mut stats = BandwidthStats::new();
1359
1360        stats.network_liveness_event("DOWN", 1000.0);
1361        assert_eq!(stats.network_down_since, Some(1000.0));
1362
1363        stats.network_liveness_event("UP", 1001.0);
1364        assert_eq!(stats.network_down_since, None);
1365    }
1366
1367    #[test]
1368    fn test_connectivity_status_connected() {
1369        let mut stats = BandwidthStats::new();
1370        let config = BandguardsConfig::default();
1371
1372        // Simulate connected state
1373        stats.no_conns_since = None;
1374        stats.no_circs_since = None;
1375
1376        let status = stats.check_connectivity(1000.0, &config);
1377        assert_eq!(status, ConnectivityStatus::Connected);
1378    }
1379
1380    const BYTES_PER_KB_TEST: u64 = 1024;
1381    const CELL_DATA_RATE: f64 = RELAY_PAYLOAD_SIZE as f64 / CELL_PAYLOAD_SIZE as f64;
1382
1383    fn check_hsdir(stats: &mut BandwidthStats, config: &BandguardsConfig, circ_id: &str) -> bool {
1384        let limit = config.circ_max_hsdesc_kilobytes as u64 * BYTES_PER_KB_TEST;
1385        let mut read: u64 = CELL_PAYLOAD_SIZE;
1386
1387        while read < limit {
1388            let delivered = (CELL_DATA_RATE * CELL_PAYLOAD_SIZE as f64) as u64;
1389            stats.circbw_event(circ_id, CELL_PAYLOAD_SIZE, 0, delivered, 0, 0, 0, 1000.0);
1390            read += CELL_PAYLOAD_SIZE;
1391
1392            if let CircuitLimitResult::HsdirBytesExceeded { .. } =
1393                stats.check_circuit_limits(circ_id, config)
1394            {
1395                return true;
1396            }
1397        }
1398
1399        let delivered = (CELL_DATA_RATE * CELL_PAYLOAD_SIZE as f64) as u64;
1400        stats.circbw_event(circ_id, CELL_PAYLOAD_SIZE, 0, delivered, 0, 0, 0, 1000.0);
1401        matches!(
1402            stats.check_circuit_limits(circ_id, config),
1403            CircuitLimitResult::HsdirBytesExceeded { .. }
1404        )
1405    }
1406
1407    fn check_serv_intro(
1408        stats: &mut BandwidthStats,
1409        config: &BandguardsConfig,
1410        circ_id: &str,
1411    ) -> bool {
1412        let limit = config.circ_max_serv_intro_kilobytes as u64 * BYTES_PER_KB_TEST;
1413        let mut read: u64 = CELL_PAYLOAD_SIZE;
1414
1415        while read < limit {
1416            let delivered = (CELL_DATA_RATE * CELL_PAYLOAD_SIZE as f64) as u64;
1417            stats.circbw_event(circ_id, CELL_PAYLOAD_SIZE, 0, delivered, 0, 0, 0, 1000.0);
1418            read += CELL_PAYLOAD_SIZE;
1419
1420            if let CircuitLimitResult::ServIntroBytesExceeded { .. } =
1421                stats.check_circuit_limits(circ_id, config)
1422            {
1423                return true;
1424            }
1425        }
1426
1427        let delivered = (CELL_DATA_RATE * CELL_PAYLOAD_SIZE as f64) as u64;
1428        stats.circbw_event(circ_id, CELL_PAYLOAD_SIZE, 0, delivered, 0, 0, 0, 1000.0);
1429        matches!(
1430            stats.check_circuit_limits(circ_id, config),
1431            CircuitLimitResult::ServIntroBytesExceeded { .. }
1432        )
1433    }
1434
1435    fn check_maxbytes(
1436        stats: &mut BandwidthStats,
1437        config: &BandguardsConfig,
1438        circ_id: &str,
1439    ) -> bool {
1440        let limit = config.circ_max_megabytes * BYTES_PER_MB;
1441        let chunk = 1000 * CELL_PAYLOAD_SIZE;
1442        let mut read: u64 = 0;
1443
1444        while read + 2 * chunk < limit {
1445            let delivered = (CELL_DATA_RATE * chunk as f64) as u64;
1446            stats.circbw_event(circ_id, chunk, chunk, delivered, 0, 0, 0, 1000.0);
1447            read += 2 * chunk;
1448
1449            if let CircuitLimitResult::MaxBytesExceeded { .. } =
1450                stats.check_circuit_limits(circ_id, config)
1451            {
1452                return true;
1453            }
1454        }
1455
1456        let delivered = (CELL_DATA_RATE * (2 * chunk) as f64) as u64;
1457        stats.circbw_event(circ_id, 2 * chunk, 0, delivered, 0, 0, 0, 1000.0);
1458        matches!(
1459            stats.check_circuit_limits(circ_id, config),
1460            CircuitLimitResult::MaxBytesExceeded { .. }
1461        )
1462    }
1463
1464    fn check_dropped_bytes(
1465        stats: &mut BandwidthStats,
1466        config: &BandguardsConfig,
1467        circ_id: &str,
1468        delivered_cells: u64,
1469        dropped_cells: u64,
1470    ) -> Option<CircuitLimitResult> {
1471        let valid_bytes = (CELL_DATA_RATE * CELL_PAYLOAD_SIZE as f64 / 2.0) as u64;
1472        for _ in 0..delivered_cells {
1473            stats.circbw_event(
1474                circ_id,
1475                CELL_PAYLOAD_SIZE,
1476                CELL_PAYLOAD_SIZE,
1477                valid_bytes,
1478                0,
1479                valid_bytes,
1480                0,
1481                1000.0,
1482            );
1483            let result = stats.check_circuit_limits(circ_id, config);
1484            if !matches!(result, CircuitLimitResult::Ok) {
1485                return Some(result);
1486            }
1487        }
1488
1489        for _ in 0..dropped_cells {
1490            stats.circbw_event(
1491                circ_id,
1492                CELL_PAYLOAD_SIZE,
1493                CELL_PAYLOAD_SIZE,
1494                0,
1495                0,
1496                0,
1497                0,
1498                1000.0,
1499            );
1500            let result = stats.check_circuit_limits(circ_id, config);
1501            if !matches!(result, CircuitLimitResult::Ok) {
1502                return Some(result);
1503            }
1504        }
1505
1506        None
1507    }
1508
1509    #[test]
1510    fn test_circuit_built_failed_closed_removed_from_map() {
1511        let mut stats = BandwidthStats::new();
1512
1513        stats.circ_event("1", "LAUNCHED", "HS_VANGUARDS", None, &[], None, 1000.0);
1514        stats.circ_event("1", "BUILT", "HS_VANGUARDS", None, &[], None, 1001.0);
1515        assert!(stats.circs.contains_key("1"));
1516
1517        stats.circ_event("1", "FAILED", "HS_VANGUARDS", None, &[], None, 1002.0);
1518        assert!(!stats.circs.contains_key("1"));
1519
1520        stats.circ_event("1", "CLOSED", "HS_VANGUARDS", None, &[], None, 1003.0);
1521        assert!(!stats.circs.contains_key("1"));
1522    }
1523
1524    #[test]
1525    fn test_circuit_built_closed_removed_from_map() {
1526        let mut stats = BandwidthStats::new();
1527
1528        stats.circ_event("2", "LAUNCHED", "HS_VANGUARDS", None, &[], None, 1000.0);
1529        stats.circ_event("2", "BUILT", "HS_VANGUARDS", None, &[], None, 1001.0);
1530        assert!(stats.circs.contains_key("2"));
1531
1532        stats.circ_event("2", "CLOSED", "HS_VANGUARDS", None, &[], None, 1002.0);
1533        assert!(!stats.circs.contains_key("2"));
1534    }
1535
1536    #[test]
1537    fn test_hsdir_size_cap_exceeded_direct_service_circ() {
1538        let mut stats = BandwidthStats::new();
1539        let config = BandguardsConfig {
1540            circ_max_hsdesc_kilobytes: 30,
1541            ..Default::default()
1542        };
1543
1544        stats.circ_event(
1545            "3",
1546            "LAUNCHED",
1547            "HS_SERVICE_HSDIR",
1548            Some("HSSI_CONNECTING"),
1549            &[],
1550            None,
1551            1000.0,
1552        );
1553        stats.circ_event(
1554            "3",
1555            "BUILT",
1556            "HS_SERVICE_HSDIR",
1557            Some("HSSI_CONNECTING"),
1558            &[],
1559            None,
1560            1001.0,
1561        );
1562
1563        let circ = stats.circs.get("3").unwrap();
1564        assert!(circ.is_hsdir);
1565        assert!(circ.is_service);
1566
1567        assert!(check_hsdir(&mut stats, &config, "3"));
1568    }
1569
1570    #[test]
1571    fn test_hsdir_size_cap_disabled() {
1572        let mut stats = BandwidthStats::new();
1573        let config = BandguardsConfig {
1574            circ_max_hsdesc_kilobytes: 0,
1575            ..Default::default()
1576        };
1577
1578        stats.circ_event(
1579            "5",
1580            "LAUNCHED",
1581            "HS_SERVICE_HSDIR",
1582            Some("HSSI_CONNECTING"),
1583            &[],
1584            None,
1585            1000.0,
1586        );
1587        stats.circ_event(
1588            "5",
1589            "BUILT",
1590            "HS_SERVICE_HSDIR",
1591            Some("HSSI_CONNECTING"),
1592            &[],
1593            None,
1594            1001.0,
1595        );
1596
1597        assert!(!check_hsdir(&mut stats, &config, "5"));
1598    }
1599
1600    #[test]
1601    fn test_intro_size_cap_disabled_by_default() {
1602        let mut stats = BandwidthStats::new();
1603        let config = BandguardsConfig::default();
1604
1605        assert_eq!(config.circ_max_serv_intro_kilobytes, 0);
1606
1607        stats.circ_event(
1608            "6",
1609            "LAUNCHED",
1610            "HS_SERVICE_INTRO",
1611            Some("HSSI_CONNECTING"),
1612            &[],
1613            None,
1614            1000.0,
1615        );
1616        stats.circ_event(
1617            "6",
1618            "BUILT",
1619            "HS_SERVICE_INTRO",
1620            Some("HSSI_CONNECTING"),
1621            &[],
1622            None,
1623            1001.0,
1624        );
1625
1626        let circ = stats.circs.get("6").unwrap();
1627        assert!(circ.is_serv_intro);
1628        assert!(circ.is_service);
1629
1630        assert!(!check_serv_intro(&mut stats, &config, "6"));
1631    }
1632
1633    #[test]
1634    fn test_intro_size_cap_exceeded() {
1635        let mut stats = BandwidthStats::new();
1636        let config = BandguardsConfig {
1637            circ_max_serv_intro_kilobytes: 1024,
1638            ..Default::default()
1639        };
1640
1641        stats.circ_event(
1642            "7",
1643            "LAUNCHED",
1644            "HS_SERVICE_INTRO",
1645            Some("HSSI_CONNECTING"),
1646            &[],
1647            None,
1648            1000.0,
1649        );
1650        stats.circ_event(
1651            "7",
1652            "BUILT",
1653            "HS_SERVICE_INTRO",
1654            Some("HSSI_CONNECTING"),
1655            &[],
1656            None,
1657            1001.0,
1658        );
1659
1660        assert!(check_serv_intro(&mut stats, &config, "7"));
1661    }
1662
1663    #[test]
1664    fn test_max_bytes_exceeded() {
1665        let mut stats = BandwidthStats::new();
1666        let config = BandguardsConfig {
1667            circ_max_megabytes: 100,
1668            ..Default::default()
1669        };
1670
1671        stats.circ_event("10", "LAUNCHED", "HS_VANGUARDS", None, &[], None, 1000.0);
1672        stats.circ_event("10", "BUILT", "HS_VANGUARDS", None, &[], None, 1001.0);
1673
1674        assert!(check_maxbytes(&mut stats, &config, "10"));
1675    }
1676
1677    #[test]
1678    fn test_max_bytes_disabled() {
1679        let mut stats = BandwidthStats::new();
1680        let config = BandguardsConfig {
1681            circ_max_megabytes: 0,
1682            ..Default::default()
1683        };
1684
1685        stats.circ_event(
1686            "11",
1687            "LAUNCHED",
1688            "HS_SERVICE_REND",
1689            Some("HSSR_CONNECTING"),
1690            &[],
1691            None,
1692            1000.0,
1693        );
1694        stats.circ_event(
1695            "11",
1696            "BUILT",
1697            "HS_SERVICE_REND",
1698            Some("HSSR_CONNECTING"),
1699            &[],
1700            None,
1701            1001.0,
1702        );
1703
1704        assert!(!check_maxbytes(&mut stats, &config, "11"));
1705    }
1706
1707    #[test]
1708    fn test_regular_reading_ok() {
1709        let mut stats = BandwidthStats::new();
1710        let config = BandguardsConfig::default();
1711
1712        stats.circ_event("20", "LAUNCHED", "HS_VANGUARDS", None, &[], None, 1000.0);
1713        stats.circ_event("20", "BUILT", "HS_VANGUARDS", None, &[], None, 1001.0);
1714
1715        let result = check_dropped_bytes(&mut stats, &config, "20", 100, 0);
1716        assert!(result.is_none());
1717    }
1718
1719    #[test]
1720    fn test_dropped_cells_before_app_data() {
1721        let mut stats = BandwidthStats::new();
1722        let config = BandguardsConfig::default();
1723
1724        stats.circ_event("21", "LAUNCHED", "HS_VANGUARDS", None, &[], None, 1000.0);
1725        stats.circ_event("21", "BUILT", "HS_VANGUARDS", None, &[], None, 1001.0);
1726
1727        let result = check_dropped_bytes(&mut stats, &config, "21", 0, 1);
1728        assert!(matches!(
1729            result,
1730            Some(CircuitLimitResult::DroppedCells { .. })
1731        ));
1732    }
1733
1734    #[test]
1735    fn test_dropped_cells_after_app_data() {
1736        let mut stats = BandwidthStats::new();
1737        let config = BandguardsConfig::default();
1738
1739        stats.circ_event("22", "LAUNCHED", "HS_VANGUARDS", None, &[], None, 1000.0);
1740        stats.circ_event("22", "BUILT", "HS_VANGUARDS", None, &[], None, 1001.0);
1741
1742        let result = check_dropped_bytes(&mut stats, &config, "22", 1000, 1);
1743        assert!(matches!(
1744            result,
1745            Some(CircuitLimitResult::DroppedCells { .. })
1746        ));
1747    }
1748
1749    #[test]
1750    fn test_dropped_cells_allowed_on_not_built_circ() {
1751        let mut stats = BandwidthStats::new();
1752        let config = BandguardsConfig::default();
1753
1754        stats.circ_event("23", "LAUNCHED", "HS_VANGUARDS", None, &[], None, 1000.0);
1755        stats.circ_event("23", "EXTENDED", "HS_VANGUARDS", None, &[], None, 1001.0);
1756
1757        let result = check_dropped_bytes(&mut stats, &config, "23", 0, 1);
1758        assert!(result.is_none());
1759    }
1760
1761    #[test]
1762    fn test_general_circ_dropped_cells() {
1763        let mut stats = BandwidthStats::new();
1764        let config = BandguardsConfig::default();
1765
1766        stats.circ_event("24", "LAUNCHED", "GENERAL", None, &[], None, 1000.0);
1767        stats.circ_event("24", "BUILT", "GENERAL", None, &[], None, 1001.0);
1768
1769        let result = check_dropped_bytes(&mut stats, &config, "24", 1000, 1);
1770        assert!(matches!(
1771            result,
1772            Some(CircuitLimitResult::DroppedCells { .. })
1773        ));
1774    }
1775
1776    #[test]
1777    fn test_orconn_connected() {
1778        let mut stats = BandwidthStats::new();
1779        let guard_fp = "5416F3E8F80101A133B1970495B04FDBD1C7446B";
1780
1781        stats.orconn_event("11", guard_fp, "CONNECTED", None, 1000.0);
1782
1783        assert!(stats.live_guard_conns.contains_key("11"));
1784        assert!(stats.guards.contains_key(guard_fp));
1785        assert_eq!(stats.guards.get(guard_fp).unwrap().conns_made, 1);
1786    }
1787
1788    #[test]
1789    fn test_orconn_closed() {
1790        let mut stats = BandwidthStats::new();
1791        let guard_fp = "5416F3E8F80101A133B1970495B04FDBD1C7446B";
1792
1793        stats.orconn_event("11", guard_fp, "CONNECTED", None, 1000.0);
1794        assert!(stats.live_guard_conns.contains_key("11"));
1795
1796        stats.orconn_event("11", guard_fp, "CLOSED", Some("DONE"), 1001.0);
1797        assert!(!stats.live_guard_conns.contains_key("11"));
1798    }
1799
1800    #[test]
1801    fn test_no_conns_since_tracking() {
1802        let mut stats = BandwidthStats::new();
1803        let guard_fp = "5416F3E8F80101A133B1970495B04FDBD1C7446B";
1804
1805        assert!(stats.no_conns_since.is_some());
1806
1807        stats.orconn_event("1", guard_fp, "CONNECTED", None, 1000.0);
1808        assert!(stats.no_conns_since.is_none());
1809
1810        stats.orconn_event("1", guard_fp, "CLOSED", None, 1001.0);
1811        assert!(stats.no_conns_since.is_some());
1812    }
1813
1814    #[test]
1815    fn test_connectivity_check_no_connections() {
1816        let mut stats = BandwidthStats::new();
1817        let config = BandguardsConfig {
1818            conn_max_disconnected_secs: 15,
1819            ..Default::default()
1820        };
1821
1822        stats.no_conns_since = Some(1000.0);
1823
1824        let status = stats.check_connectivity(1020.0, &config);
1825        assert!(matches!(
1826            status,
1827            ConnectivityStatus::NoConnections { secs: 20 }
1828        ));
1829    }
1830
1831    #[test]
1832    fn test_connectivity_disabled() {
1833        let mut stats = BandwidthStats::new();
1834        let config = BandguardsConfig {
1835            conn_max_disconnected_secs: 0,
1836            ..Default::default()
1837        };
1838
1839        stats.no_conns_since = Some(1000.0);
1840
1841        let status = stats.check_connectivity(2000.0, &config);
1842        assert_eq!(status, ConnectivityStatus::Connected);
1843    }
1844
1845    #[test]
1846    fn test_circ_minor_purpose_changed() {
1847        let mut stats = BandwidthStats::new();
1848        let path = vec!["5416F3E8F80101A133B1970495B04FDBD1C7446B".to_string()];
1849
1850        stats.circ_event("30", "LAUNCHED", "HS_VANGUARDS", None, &[], None, 1000.0);
1851        stats.circ_event("30", "BUILT", "HS_VANGUARDS", None, &path, None, 1001.0);
1852
1853        stats.circ_minor_event(
1854            "30",
1855            "PURPOSE_CHANGED",
1856            "HS_SERVICE_REND",
1857            Some("HSSR_CONNECTING"),
1858            Some("HS_VANGUARDS"),
1859            None,
1860            &path,
1861        );
1862
1863        let circ = stats.circs.get("30").unwrap();
1864        assert_eq!(circ.purpose, Some("HS_SERVICE_REND".to_string()));
1865        assert!(circ.in_use);
1866        assert_eq!(circ.guard_fp, Some(path[0].clone()));
1867    }
1868
1869    #[test]
1870    fn test_circ_minor_cannibalized_to_hsdir() {
1871        let mut stats = BandwidthStats::new();
1872
1873        stats.circ_event("31", "LAUNCHED", "HS_VANGUARDS", None, &[], None, 1000.0);
1874        stats.circ_event("31", "BUILT", "HS_VANGUARDS", None, &[], None, 1001.0);
1875
1876        let circ = stats.circs.get("31").unwrap();
1877        assert!(!circ.is_hsdir);
1878
1879        stats.circ_minor_event(
1880            "31",
1881            "CANNIBALIZED",
1882            "HS_CLIENT_HSDIR",
1883            Some("HSCI_CONNECTING"),
1884            Some("HS_VANGUARDS"),
1885            None,
1886            &[],
1887        );
1888
1889        let circ = stats.circs.get("31").unwrap();
1890        assert!(circ.is_hsdir);
1891        assert!(!circ.is_service);
1892    }
1893
1894    #[test]
1895    fn test_circ_minor_cannibalized_to_serv_intro() {
1896        let mut stats = BandwidthStats::new();
1897
1898        stats.circ_event("32", "LAUNCHED", "HS_VANGUARDS", None, &[], None, 1000.0);
1899        stats.circ_event("32", "BUILT", "HS_VANGUARDS", None, &[], None, 1001.0);
1900
1901        let circ = stats.circs.get("32").unwrap();
1902        assert!(!circ.is_serv_intro);
1903
1904        stats.circ_minor_event(
1905            "32",
1906            "CANNIBALIZED",
1907            "HS_SERVICE_INTRO",
1908            Some("HSSI_CONNECTING"),
1909            Some("HS_VANGUARDS"),
1910            None,
1911            &[],
1912        );
1913
1914        let circ = stats.circs.get("32").unwrap();
1915        assert!(circ.is_serv_intro);
1916        assert!(circ.is_service);
1917    }
1918
1919    #[test]
1920    fn test_tor_bug_29699_workaround() {
1921        let mut stats = BandwidthStats::new();
1922        let config = BandguardsConfig::default();
1923
1924        stats.circ_event(
1925            "40",
1926            "LAUNCHED",
1927            "HS_SERVICE_INTRO",
1928            Some("HSSI_ESTABLISHED"),
1929            &[],
1930            None,
1931            1000.0,
1932        );
1933        stats.circ_event(
1934            "40",
1935            "BUILT",
1936            "HS_SERVICE_INTRO",
1937            Some("HSSI_ESTABLISHED"),
1938            &[],
1939            None,
1940            1001.0,
1941        );
1942
1943        stats.circbw_event("40", CELL_PAYLOAD_SIZE, 0, 0, 0, 0, 0, 1002.0);
1944
1945        let result = stats.check_circuit_limits("40", &config);
1946        assert!(matches!(
1947            result,
1948            CircuitLimitResult::TorBug {
1949                bug_id: "#29699",
1950                ..
1951            }
1952        ));
1953    }
1954
1955    #[test]
1956    fn test_tor_bug_29700_workaround() {
1957        let mut stats = BandwidthStats::new();
1958        let config = BandguardsConfig::default();
1959
1960        stats.circ_event(
1961            "41",
1962            "LAUNCHED",
1963            "HS_SERVICE_REND",
1964            Some("HSSR_CONNECTING"),
1965            &[],
1966            None,
1967            1000.0,
1968        );
1969        stats.circ_event(
1970            "41",
1971            "BUILT",
1972            "HS_SERVICE_REND",
1973            Some("HSSR_CONNECTING"),
1974            &[],
1975            None,
1976            1001.0,
1977        );
1978
1979        stats.circbw_event("41", CELL_PAYLOAD_SIZE, 0, 0, 0, 0, 0, 1002.0);
1980
1981        let result = stats.check_circuit_limits("41", &config);
1982        assert!(matches!(
1983            result,
1984            CircuitLimitResult::TorBug {
1985                bug_id: "#29700",
1986                ..
1987            }
1988        ));
1989    }
1990
1991    #[test]
1992    fn test_tor_bug_29786_workaround() {
1993        let mut stats = BandwidthStats::new();
1994        let config = BandguardsConfig::default();
1995
1996        stats.circ_event(
1997            "42",
1998            "LAUNCHED",
1999            "PATH_BIAS_TESTING",
2000            None,
2001            &[],
2002            None,
2003            1000.0,
2004        );
2005        stats.circ_event("42", "BUILT", "PATH_BIAS_TESTING", None, &[], None, 1001.0);
2006
2007        stats.circbw_event("42", CELL_PAYLOAD_SIZE, 0, 0, 0, 0, 0, 1002.0);
2008
2009        let result = stats.check_circuit_limits("42", &config);
2010        assert!(matches!(
2011            result,
2012            CircuitLimitResult::TorBug {
2013                bug_id: "#29786",
2014                ..
2015            }
2016        ));
2017    }
2018
2019    #[test]
2020    fn test_tor_bug_29927_workaround() {
2021        let mut stats = BandwidthStats::new();
2022        let config = BandguardsConfig::default();
2023
2024        stats.circ_event(
2025            "43",
2026            "LAUNCHED",
2027            "HS_CLIENT_INTRO",
2028            Some("HSCI_DONE"),
2029            &[],
2030            None,
2031            1000.0,
2032        );
2033        stats.circ_event(
2034            "43",
2035            "BUILT",
2036            "HS_CLIENT_INTRO",
2037            Some("HSCI_DONE"),
2038            &[],
2039            None,
2040            1001.0,
2041        );
2042
2043        stats.circbw_event("43", CELL_PAYLOAD_SIZE, 0, 0, 0, 0, 0, 1002.0);
2044
2045        let result = stats.check_circuit_limits("43", &config);
2046        assert!(matches!(
2047            result,
2048            CircuitLimitResult::TorBug {
2049                bug_id: "#29927",
2050                ..
2051            }
2052        ));
2053    }
2054
2055    #[test]
2056    fn test_stray_circ_minor_event() {
2057        let mut stats = BandwidthStats::new();
2058
2059        stats.circ_minor_event(
2060            "999",
2061            "CANNIBALIZED",
2062            "HS_SERVICE_REND",
2063            Some("HSSR_CONNECTING"),
2064            Some("HS_VANGUARDS"),
2065            None,
2066            &[],
2067        );
2068
2069        assert!(!stats.circs.contains_key("999"));
2070    }
2071}
2072
2073#[cfg(test)]
2074mod proptests {
2075    use super::*;
2076    use proptest::prelude::*;
2077
2078    proptest! {
2079        #![proptest_config(ProptestConfig::with_cases(100))]
2080
2081        #[test]
2082        fn bandwidth_tracking_accuracy(
2083            events in prop::collection::vec(
2084                (100u64..10000, 100u64..10000, 50u64..5000, 50u64..5000, 10u64..500, 10u64..500),
2085                1..20
2086            ),
2087        ) {
2088            let mut stats = BandwidthStats::new();
2089
2090            stats.circ_event("123", "LAUNCHED", "GENERAL", None, &[], None, 1000.0);
2091
2092            let mut expected_read = 0u64;
2093            let mut expected_sent = 0u64;
2094            let mut expected_delivered_read = 0u64;
2095            let mut expected_delivered_sent = 0u64;
2096            let mut expected_overhead_read = 0u64;
2097            let mut expected_overhead_sent = 0u64;
2098
2099            for (i, (read, written, del_read, del_written, oh_read, oh_written)) in events.iter().enumerate() {
2100                stats.circbw_event(
2101                    "123",
2102                    *read,
2103                    *written,
2104                    *del_read,
2105                    *del_written,
2106                    *oh_read,
2107                    *oh_written,
2108                    1001.0 + i as f64,
2109                );
2110
2111                expected_read += read;
2112                expected_sent += written;
2113                expected_delivered_read += del_read;
2114                expected_delivered_sent += del_written;
2115                expected_overhead_read += oh_read;
2116                expected_overhead_sent += oh_written;
2117            }
2118
2119            let circ = stats.circs.get("123").unwrap();
2120            prop_assert_eq!(circ.read_bytes, expected_read);
2121            prop_assert_eq!(circ.sent_bytes, expected_sent);
2122            prop_assert_eq!(circ.delivered_read_bytes, expected_delivered_read);
2123            prop_assert_eq!(circ.delivered_sent_bytes, expected_delivered_sent);
2124            prop_assert_eq!(circ.overhead_read_bytes, expected_overhead_read);
2125            prop_assert_eq!(circ.overhead_sent_bytes, expected_overhead_sent);
2126        }
2127
2128        #[test]
2129        fn circuit_limit_enforcement(
2130            limit_mb in 1u64..100,
2131            bytes_mb in 0u64..200,
2132        ) {
2133            let mut stats = BandwidthStats::new();
2134            let config = BandguardsConfig {
2135                circ_max_megabytes: limit_mb,
2136                ..Default::default()
2137            };
2138
2139            stats.circ_event("123", "BUILT", "GENERAL", None, &[], None, 1000.0);
2140
2141            let bytes = bytes_mb * 1024 * 1024;
2142            let delivered = (bytes / CELL_PAYLOAD_SIZE) * RELAY_PAYLOAD_SIZE;
2143            stats.circbw_event("123", bytes, 0, delivered, 0, 0, 0, 1001.0);
2144
2145            let result = stats.check_circuit_limits("123", &config);
2146
2147            if bytes > limit_mb * 1024 * 1024 {
2148                match result {
2149                    CircuitLimitResult::MaxBytesExceeded { .. } => {}
2150                    _ => prop_assert!(false, "Expected MaxBytesExceeded for {} bytes > {} MB limit", bytes, limit_mb),
2151                }
2152            } else {
2153                prop_assert_eq!(result, CircuitLimitResult::Ok,
2154                    "Expected Ok for {} bytes <= {} MB limit", bytes, limit_mb);
2155            }
2156        }
2157
2158        #[test]
2159        fn dropped_cell_detection(
2160            cells_received in 10u64..1000,
2161            cells_delivered in 0u64..1000,
2162            cells_overhead in 0u64..100,
2163        ) {
2164            let mut circ = BwCircuitStat::new("123".to_string(), false);
2165
2166            circ.read_bytes = cells_received * CELL_PAYLOAD_SIZE;
2167            circ.delivered_read_bytes = cells_delivered * RELAY_PAYLOAD_SIZE;
2168            circ.overhead_read_bytes = cells_overhead * RELAY_PAYLOAD_SIZE;
2169
2170            let dropped = circ.dropped_read_cells();
2171            let expected_dropped = cells_received as i64 - (cells_delivered + cells_overhead) as i64;
2172
2173            prop_assert_eq!(dropped, expected_dropped,
2174                "Expected {} dropped cells, got {}", expected_dropped, dropped);
2175        }
2176    }
2177}