use std::collections::HashMap;
use crate::config::BandguardsConfig;
pub const CELL_PAYLOAD_SIZE: u64 = 509;
pub const RELAY_HEADER_SIZE: u64 = 11;
pub const RELAY_PAYLOAD_SIZE: u64 = CELL_PAYLOAD_SIZE - RELAY_HEADER_SIZE;
const SECS_PER_HOUR: u64 = 3600;
const BYTES_PER_KB: u64 = 1024;
const BYTES_PER_MB: u64 = 1024 * BYTES_PER_KB;
pub const MAX_CIRC_DESTROY_LAG_SECS: u64 = 2;
#[derive(Debug, Clone)]
pub struct BwCircuitStat {
pub circ_id: String,
pub is_hs: bool,
pub is_service: bool,
pub is_hsdir: bool,
pub is_serv_intro: bool,
pub dropped_cells_allowed: u64,
pub purpose: Option<String>,
pub hs_state: Option<String>,
pub old_purpose: Option<String>,
pub old_hs_state: Option<String>,
pub in_use: bool,
pub built: bool,
pub created_at: f64,
pub read_bytes: u64,
pub sent_bytes: u64,
pub delivered_read_bytes: u64,
pub delivered_sent_bytes: u64,
pub overhead_read_bytes: u64,
pub overhead_sent_bytes: u64,
pub guard_fp: Option<String>,
pub possibly_destroyed_at: Option<f64>,
}
impl BwCircuitStat {
pub fn new(circ_id: String, is_hs: bool) -> Self {
Self {
circ_id,
is_hs,
is_service: true,
is_hsdir: false,
is_serv_intro: false,
dropped_cells_allowed: 0,
purpose: None,
hs_state: None,
old_purpose: None,
old_hs_state: None,
in_use: false,
built: false,
created_at: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs_f64(),
read_bytes: 0,
sent_bytes: 0,
delivered_read_bytes: 0,
delivered_sent_bytes: 0,
overhead_read_bytes: 0,
overhead_sent_bytes: 0,
guard_fp: None,
possibly_destroyed_at: None,
}
}
pub fn total_bytes(&self) -> u64 {
self.read_bytes + self.sent_bytes
}
pub fn dropped_read_cells(&self) -> i64 {
let cells_received = self.read_bytes / CELL_PAYLOAD_SIZE;
let cells_delivered =
(self.delivered_read_bytes + self.overhead_read_bytes) / RELAY_PAYLOAD_SIZE;
cells_received as i64 - cells_delivered as i64
}
pub fn age_secs(&self) -> f64 {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs_f64();
now - self.created_at
}
pub fn age_hours(&self) -> f64 {
self.age_secs() / SECS_PER_HOUR as f64
}
}
#[derive(Debug, Clone)]
pub struct BwGuardStat {
pub to_guard: String,
pub killed_conns: u32,
pub killed_conn_at: f64,
pub killed_conn_pending: bool,
pub conns_made: u32,
pub close_reasons: HashMap<String, u32>,
}
impl BwGuardStat {
pub fn new(guard_fp: String) -> Self {
Self {
to_guard: guard_fp,
killed_conns: 0,
killed_conn_at: 0.0,
killed_conn_pending: false,
conns_made: 0,
close_reasons: HashMap::new(),
}
}
pub fn record_close_reason(&mut self, reason: &str) {
*self.close_reasons.entry(reason.to_string()).or_insert(0) += 1;
}
}
#[derive(Debug, Clone)]
pub struct BandwidthStats {
pub circs: HashMap<String, BwCircuitStat>,
pub live_guard_conns: HashMap<String, BwGuardStat>,
pub guards: HashMap<String, BwGuardStat>,
pub circs_destroyed_total: u64,
pub no_conns_since: Option<f64>,
pub no_circs_since: Option<f64>,
pub network_down_since: Option<f64>,
pub max_fake_id: i32,
pub disconnected_circs: bool,
pub disconnected_conns: bool,
}
impl Default for BandwidthStats {
fn default() -> Self {
Self::new()
}
}
impl BandwidthStats {
pub fn new() -> Self {
Self {
circs: HashMap::new(),
live_guard_conns: HashMap::new(),
guards: HashMap::new(),
circs_destroyed_total: 0,
no_conns_since: Some(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs_f64(),
),
no_circs_since: None,
network_down_since: None,
max_fake_id: -1,
disconnected_circs: false,
disconnected_conns: false,
}
}
pub fn orconn_event(
&mut self,
conn_id: &str,
guard_fp: &str,
status: &str,
reason: Option<&str>,
arrived_at: f64,
) {
if !self.guards.contains_key(guard_fp) {
self.guards
.insert(guard_fp.to_string(), BwGuardStat::new(guard_fp.to_string()));
}
match status {
"CONNECTED" => {
if self.disconnected_conns {
self.disconnected_conns = false;
}
self.live_guard_conns
.insert(conn_id.to_string(), BwGuardStat::new(guard_fp.to_string()));
if let Some(guard) = self.guards.get_mut(guard_fp) {
guard.conns_made += 1;
}
self.no_conns_since = None;
}
"CLOSED" | "FAILED" => {
let actual_conn_id = self.fixup_orconn_id(conn_id, guard_fp);
if self.live_guard_conns.contains_key(&actual_conn_id) {
for circ in self.circs.values_mut() {
if circ.in_use && circ.guard_fp.as_deref() == Some(guard_fp) {
circ.possibly_destroyed_at = Some(arrived_at);
if let Some(guard) = self.guards.get_mut(guard_fp) {
guard.killed_conn_at = arrived_at;
}
}
}
self.live_guard_conns.remove(&actual_conn_id);
if self.live_guard_conns.is_empty() && self.no_conns_since.is_none() {
self.no_conns_since = Some(arrived_at);
}
}
if status == "CLOSED" {
if let Some(r) = reason {
if let Some(guard) = self.guards.get_mut(guard_fp) {
guard.record_close_reason(r);
}
}
}
}
_ => {}
}
}
fn fixup_orconn_id(&self, conn_id: &str, guard_fp: &str) -> String {
if let Ok(id) = conn_id.parse::<i32>() {
if id <= self.max_fake_id {
for (fake_id, stat) in &self.live_guard_conns {
if stat.to_guard == guard_fp {
if let Ok(fid) = fake_id.parse::<i32>() {
if fid <= self.max_fake_id {
return fake_id.clone();
}
}
}
}
}
}
conn_id.to_string()
}
#[allow(clippy::too_many_arguments)]
pub fn circ_event(
&mut self,
circ_id: &str,
status: &str,
purpose: &str,
hs_state: Option<&str>,
path: &[String],
remote_reason: Option<&str>,
arrived_at: f64,
) -> Option<bool> {
if status == "FAILED"
&& self.no_circs_since.is_none()
&& self.any_circuits_pending(Some(circ_id))
{
self.no_circs_since = Some(arrived_at);
}
if status == "FAILED" || status == "CLOSED" {
if let Some(circ) = self.circs.remove(circ_id) {
if circ.in_use && circ.possibly_destroyed_at.is_some() {
if let Some(destroyed_at) = circ.possibly_destroyed_at {
if arrived_at - destroyed_at <= MAX_CIRC_DESTROY_LAG_SECS as f64
&& remote_reason == Some("CHANNEL_CLOSED")
{
if let Some(guard_fp) = &circ.guard_fp {
if let Some(guard) = self.guards.get_mut(guard_fp) {
guard.killed_conn_at = 0.0;
guard.killed_conns += 1;
}
}
self.circs_destroyed_total += 1;
return Some(true);
}
}
}
return Some(false);
}
return None;
}
let is_hs = hs_state.is_some() || purpose.starts_with("HS");
if !self.circs.contains_key(circ_id) {
let mut circ = BwCircuitStat::new(circ_id.to_string(), is_hs);
if purpose.starts_with("HS_CLIENT") {
circ.is_service = false;
} else if purpose.starts_with("HS_SERVICE") {
circ.is_service = true;
}
if purpose == "HS_CLIENT_HSDIR" || purpose == "HS_SERVICE_HSDIR" {
circ.is_hsdir = true;
} else if purpose == "HS_SERVICE_INTRO" {
circ.is_serv_intro = true;
}
self.circs.insert(circ_id.to_string(), circ);
}
if let Some(circ) = self.circs.get_mut(circ_id) {
circ.purpose = Some(purpose.to_string());
circ.hs_state = hs_state.map(|s| s.to_string());
if status == "BUILT" || status == "GUARD_WAIT" {
circ.built = true;
if self.disconnected_circs {
self.disconnected_circs = false;
}
self.no_circs_since = None;
if purpose.starts_with("HS_CLIENT") || purpose.starts_with("HS_SERVICE") {
circ.in_use = true;
if !path.is_empty() {
circ.guard_fp = Some(path[0].clone());
}
}
} else if status == "EXTENDED" {
if self.disconnected_circs {
self.disconnected_circs = false;
}
self.no_circs_since = None;
}
}
None
}
#[allow(clippy::too_many_arguments)]
pub fn circ_minor_event(
&mut self,
circ_id: &str,
event_type: &str,
purpose: &str,
hs_state: Option<&str>,
old_purpose: Option<&str>,
old_hs_state: Option<&str>,
path: &[String],
) {
if let Some(circ) = self.circs.get_mut(circ_id) {
circ.purpose = Some(purpose.to_string());
circ.hs_state = hs_state.map(|s| s.to_string());
circ.old_purpose = old_purpose.map(|s| s.to_string());
circ.old_hs_state = old_hs_state.map(|s| s.to_string());
if purpose.starts_with("HS_CLIENT") {
circ.is_service = false;
} else if purpose.starts_with("HS_SERVICE") {
circ.is_service = true;
}
if purpose == "HS_CLIENT_HSDIR" || purpose == "HS_SERVICE_HSDIR" {
circ.is_hsdir = true;
} else if purpose == "HS_SERVICE_INTRO" {
circ.is_serv_intro = true;
}
if event_type == "PURPOSE_CHANGED" && old_purpose == Some("HS_VANGUARDS") {
circ.in_use = true;
if !path.is_empty() {
circ.guard_fp = Some(path[0].clone());
}
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn circbw_event(
&mut self,
circ_id: &str,
read: u64,
written: u64,
delivered_read: u64,
delivered_written: u64,
overhead_read: u64,
overhead_written: u64,
_arrived_at: f64,
) {
if self.disconnected_circs {
self.disconnected_circs = false;
}
self.no_circs_since = None;
if let Some(circ) = self.circs.get_mut(circ_id) {
circ.read_bytes += read;
circ.sent_bytes += written;
circ.delivered_read_bytes += delivered_read;
circ.delivered_sent_bytes += delivered_written;
circ.overhead_read_bytes += overhead_read;
circ.overhead_sent_bytes += overhead_written;
}
}
pub fn check_circuit_limits(
&self,
circ_id: &str,
config: &BandguardsConfig,
) -> CircuitLimitResult {
let circ = match self.circs.get(circ_id) {
Some(c) => c,
None => return CircuitLimitResult::Ok,
};
let dropped = circ.dropped_read_cells();
if dropped > circ.dropped_cells_allowed as i64 {
let tor_bug = self.check_tor_bug_workaround(circ, dropped);
if let Some(bug_id) = tor_bug {
return CircuitLimitResult::TorBug {
bug_id,
dropped_cells: dropped,
};
}
if circ.built {
return CircuitLimitResult::DroppedCells {
dropped_cells: dropped,
};
}
}
if config.circ_max_megabytes > 0
&& circ.total_bytes() > config.circ_max_megabytes * BYTES_PER_MB
{
return CircuitLimitResult::MaxBytesExceeded {
bytes: circ.total_bytes(),
limit: config.circ_max_megabytes * BYTES_PER_MB,
};
}
if config.circ_max_hsdesc_kilobytes > 0
&& circ.is_hsdir
&& circ.total_bytes() > config.circ_max_hsdesc_kilobytes as u64 * BYTES_PER_KB
{
return CircuitLimitResult::HsdirBytesExceeded {
bytes: circ.total_bytes(),
limit: config.circ_max_hsdesc_kilobytes as u64 * BYTES_PER_KB,
};
}
if config.circ_max_serv_intro_kilobytes > 0
&& circ.is_serv_intro
&& circ.total_bytes() > config.circ_max_serv_intro_kilobytes as u64 * BYTES_PER_KB
{
return CircuitLimitResult::ServIntroBytesExceeded {
bytes: circ.total_bytes(),
limit: config.circ_max_serv_intro_kilobytes as u64 * BYTES_PER_KB,
};
}
CircuitLimitResult::Ok
}
fn check_tor_bug_workaround(
&self,
circ: &BwCircuitStat,
_dropped: i64,
) -> Option<&'static str> {
let purpose = circ.purpose.as_deref().unwrap_or("");
let hs_state = circ.hs_state.as_deref().unwrap_or("");
let old_purpose = circ.old_purpose.as_deref().unwrap_or("");
let old_hs_state = circ.old_hs_state.as_deref().unwrap_or("");
if purpose == "HS_SERVICE_INTRO" && hs_state == "HSSI_ESTABLISHED" {
return Some("#29699");
}
if purpose == "CIRCUIT_PADDING"
&& old_purpose == "HS_CLIENT_INTRO"
&& old_hs_state == "HSCI_INTRO_SENT"
{
return Some("#40359");
}
if purpose == "HS_CLIENT_REND" || (purpose == "HS_CLIENT_INTRO" && hs_state == "HSCI_DONE")
{
return Some("#29927");
}
if purpose == "HS_SERVICE_REND" && hs_state == "HSSR_CONNECTING" {
return Some("#29700");
}
if purpose == "PATH_BIAS_TESTING" {
return Some("#29786");
}
None
}
pub fn get_aged_circuits(&self, config: &BandguardsConfig) -> Vec<String> {
if config.circ_max_age_hours == 0 {
return Vec::new();
}
let max_age_secs = config.circ_max_age_hours as f64 * SECS_PER_HOUR as f64;
self.circs
.iter()
.filter(|(_, circ)| circ.age_secs() > max_age_secs)
.map(|(id, _)| id.clone())
.collect()
}
pub fn check_connectivity(
&mut self,
now: f64,
config: &BandguardsConfig,
) -> ConnectivityStatus {
if let Some(no_conns_since) = self.no_conns_since {
let disconnected_secs = (now - no_conns_since) as u32;
if config.conn_max_disconnected_secs > 0
&& disconnected_secs >= config.conn_max_disconnected_secs
&& (!self.disconnected_conns
|| disconnected_secs.is_multiple_of(config.conn_max_disconnected_secs))
{
self.disconnected_conns = true;
return ConnectivityStatus::NoConnections {
secs: disconnected_secs,
};
}
} else if let Some(no_circs_since) = self.no_circs_since {
let disconnected_secs = (now - no_circs_since) as u32;
if config.circ_max_disconnected_secs > 0
&& disconnected_secs >= config.circ_max_disconnected_secs
&& self.any_circuits_pending(None)
&& (!self.disconnected_circs
|| disconnected_secs.is_multiple_of(config.circ_max_disconnected_secs))
{
self.disconnected_circs = true;
return ConnectivityStatus::CircuitsFailing {
secs: disconnected_secs,
network_down_secs: self.network_down_since.map(|t| (now - t) as u32),
};
}
}
ConnectivityStatus::Connected
}
pub fn network_liveness_event(&mut self, status: &str, arrived_at: f64) {
match status {
"UP" => {
self.network_down_since = None;
}
"DOWN" => {
self.network_down_since = Some(arrived_at);
}
_ => {}
}
}
fn any_circuits_pending(&self, except_id: Option<&str>) -> bool {
self.circs
.iter()
.any(|(id, circ)| !circ.built && except_id.is_none_or(|e| id != e))
}
pub fn circuit_count(&self) -> usize {
self.circs.len()
}
pub fn live_connection_count(&self) -> usize {
self.live_guard_conns.len()
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum CircuitLimitResult {
Ok,
DroppedCells {
dropped_cells: i64,
},
TorBug {
bug_id: &'static str,
dropped_cells: i64,
},
MaxBytesExceeded {
bytes: u64,
limit: u64,
},
HsdirBytesExceeded {
bytes: u64,
limit: u64,
},
ServIntroBytesExceeded {
bytes: u64,
limit: u64,
},
}
#[derive(Debug, Clone, PartialEq)]
pub enum ConnectivityStatus {
Connected,
NoConnections {
secs: u32,
},
CircuitsFailing {
secs: u32,
network_down_secs: Option<u32>,
},
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bw_circuit_stat_new() {
let circ = BwCircuitStat::new("123".to_string(), true);
assert_eq!(circ.circ_id, "123");
assert!(circ.is_hs);
assert!(circ.is_service);
assert!(!circ.is_hsdir);
assert!(!circ.is_serv_intro);
assert_eq!(circ.read_bytes, 0);
assert_eq!(circ.sent_bytes, 0);
}
#[test]
fn test_total_bytes() {
let mut circ = BwCircuitStat::new("123".to_string(), true);
circ.read_bytes = 1000;
circ.sent_bytes = 500;
assert_eq!(circ.total_bytes(), 1500);
}
#[test]
fn test_dropped_read_cells() {
let mut circ = BwCircuitStat::new("123".to_string(), true);
circ.read_bytes = 5090;
circ.delivered_read_bytes = 3984;
circ.overhead_read_bytes = 0;
assert_eq!(circ.dropped_read_cells(), 2);
}
#[test]
fn test_dropped_read_cells_with_overhead() {
let mut circ = BwCircuitStat::new("123".to_string(), true);
circ.read_bytes = 5090;
circ.delivered_read_bytes = 3486; circ.overhead_read_bytes = 498;
assert_eq!(circ.dropped_read_cells(), 2);
}
#[test]
fn test_bw_guard_stat_new() {
let guard = BwGuardStat::new("A".repeat(40));
assert_eq!(guard.to_guard, "A".repeat(40));
assert_eq!(guard.killed_conns, 0);
assert_eq!(guard.conns_made, 0);
assert!(guard.close_reasons.is_empty());
}
#[test]
fn test_record_close_reason() {
let mut guard = BwGuardStat::new("A".repeat(40));
guard.record_close_reason("DONE");
guard.record_close_reason("DONE");
guard.record_close_reason("ERROR");
assert_eq!(guard.close_reasons.get("DONE"), Some(&2));
assert_eq!(guard.close_reasons.get("ERROR"), Some(&1));
}
#[test]
fn test_bandwidth_stats_new() {
let stats = BandwidthStats::new();
assert!(stats.circs.is_empty());
assert!(stats.live_guard_conns.is_empty());
assert!(stats.guards.is_empty());
assert_eq!(stats.circs_destroyed_total, 0);
assert!(stats.no_conns_since.is_some());
}
#[test]
fn test_orconn_event_connected() {
let mut stats = BandwidthStats::new();
let fp = "A".repeat(40);
stats.orconn_event("1", &fp, "CONNECTED", None, 1000.0);
assert!(stats.live_guard_conns.contains_key("1"));
assert!(stats.guards.contains_key(&fp));
assert_eq!(stats.guards.get(&fp).unwrap().conns_made, 1);
assert!(stats.no_conns_since.is_none());
}
#[test]
fn test_orconn_event_closed() {
let mut stats = BandwidthStats::new();
let fp = "A".repeat(40);
stats.orconn_event("1", &fp, "CONNECTED", None, 1000.0);
stats.orconn_event("1", &fp, "CLOSED", Some("DONE"), 1001.0);
assert!(!stats.live_guard_conns.contains_key("1"));
assert!(stats.no_conns_since.is_some());
assert_eq!(
stats.guards.get(&fp).unwrap().close_reasons.get("DONE"),
Some(&1)
);
}
#[test]
fn test_circ_event_creates_circuit() {
let mut stats = BandwidthStats::new();
stats.circ_event(
"123",
"LAUNCHED",
"HS_SERVICE_REND",
Some("HSSR_CONNECTING"),
&[],
None,
1000.0,
);
assert!(stats.circs.contains_key("123"));
let circ = stats.circs.get("123").unwrap();
assert!(circ.is_hs);
assert!(circ.is_service);
}
#[test]
fn test_circ_event_built() {
let mut stats = BandwidthStats::new();
let path = vec!["A".repeat(40)];
stats.circ_event(
"123",
"LAUNCHED",
"HS_SERVICE_REND",
Some("HSSR_CONNECTING"),
&[],
None,
1000.0,
);
stats.circ_event(
"123",
"BUILT",
"HS_SERVICE_REND",
Some("HSSR_CONNECTING"),
&path,
None,
1001.0,
);
let circ = stats.circs.get("123").unwrap();
assert!(circ.built);
assert!(circ.in_use);
assert_eq!(circ.guard_fp, Some("A".repeat(40)));
}
#[test]
fn test_circbw_event() {
let mut stats = BandwidthStats::new();
stats.circ_event("123", "LAUNCHED", "GENERAL", None, &[], None, 1000.0);
stats.circbw_event("123", 1000, 500, 800, 400, 100, 50, 1001.0);
let circ = stats.circs.get("123").unwrap();
assert_eq!(circ.read_bytes, 1000);
assert_eq!(circ.sent_bytes, 500);
assert_eq!(circ.delivered_read_bytes, 800);
assert_eq!(circ.delivered_sent_bytes, 400);
assert_eq!(circ.overhead_read_bytes, 100);
assert_eq!(circ.overhead_sent_bytes, 50);
}
#[test]
fn test_check_circuit_limits_ok() {
let mut stats = BandwidthStats::new();
let config = BandguardsConfig::default();
stats.circ_event("123", "LAUNCHED", "GENERAL", None, &[], None, 1000.0);
let result = stats.check_circuit_limits("123", &config);
assert_eq!(result, CircuitLimitResult::Ok);
}
#[test]
fn test_check_circuit_limits_max_bytes() {
let mut stats = BandwidthStats::new();
let config = BandguardsConfig {
circ_max_megabytes: 1, ..Default::default()
};
stats.circ_event("123", "BUILT", "GENERAL", None, &[], None, 1000.0);
let bytes = 2 * BYTES_PER_MB;
let delivered = (bytes / CELL_PAYLOAD_SIZE) * RELAY_PAYLOAD_SIZE;
stats.circbw_event("123", bytes, 0, delivered, 0, 0, 0, 1001.0);
let result = stats.check_circuit_limits("123", &config);
match result {
CircuitLimitResult::MaxBytesExceeded { bytes: b, limit } => {
assert_eq!(b, bytes);
assert_eq!(limit, BYTES_PER_MB);
}
_ => panic!("Expected MaxBytesExceeded, got {:?}", result),
}
}
#[test]
fn test_network_liveness_event() {
let mut stats = BandwidthStats::new();
stats.network_liveness_event("DOWN", 1000.0);
assert_eq!(stats.network_down_since, Some(1000.0));
stats.network_liveness_event("UP", 1001.0);
assert_eq!(stats.network_down_since, None);
}
#[test]
fn test_connectivity_status_connected() {
let mut stats = BandwidthStats::new();
let config = BandguardsConfig::default();
stats.no_conns_since = None;
stats.no_circs_since = None;
let status = stats.check_connectivity(1000.0, &config);
assert_eq!(status, ConnectivityStatus::Connected);
}
const BYTES_PER_KB_TEST: u64 = 1024;
const CELL_DATA_RATE: f64 = RELAY_PAYLOAD_SIZE as f64 / CELL_PAYLOAD_SIZE as f64;
fn check_hsdir(stats: &mut BandwidthStats, config: &BandguardsConfig, circ_id: &str) -> bool {
let limit = config.circ_max_hsdesc_kilobytes as u64 * BYTES_PER_KB_TEST;
let mut read: u64 = CELL_PAYLOAD_SIZE;
while read < limit {
let delivered = (CELL_DATA_RATE * CELL_PAYLOAD_SIZE as f64) as u64;
stats.circbw_event(circ_id, CELL_PAYLOAD_SIZE, 0, delivered, 0, 0, 0, 1000.0);
read += CELL_PAYLOAD_SIZE;
if let CircuitLimitResult::HsdirBytesExceeded { .. } =
stats.check_circuit_limits(circ_id, config)
{
return true;
}
}
let delivered = (CELL_DATA_RATE * CELL_PAYLOAD_SIZE as f64) as u64;
stats.circbw_event(circ_id, CELL_PAYLOAD_SIZE, 0, delivered, 0, 0, 0, 1000.0);
matches!(
stats.check_circuit_limits(circ_id, config),
CircuitLimitResult::HsdirBytesExceeded { .. }
)
}
fn check_serv_intro(
stats: &mut BandwidthStats,
config: &BandguardsConfig,
circ_id: &str,
) -> bool {
let limit = config.circ_max_serv_intro_kilobytes as u64 * BYTES_PER_KB_TEST;
let mut read: u64 = CELL_PAYLOAD_SIZE;
while read < limit {
let delivered = (CELL_DATA_RATE * CELL_PAYLOAD_SIZE as f64) as u64;
stats.circbw_event(circ_id, CELL_PAYLOAD_SIZE, 0, delivered, 0, 0, 0, 1000.0);
read += CELL_PAYLOAD_SIZE;
if let CircuitLimitResult::ServIntroBytesExceeded { .. } =
stats.check_circuit_limits(circ_id, config)
{
return true;
}
}
let delivered = (CELL_DATA_RATE * CELL_PAYLOAD_SIZE as f64) as u64;
stats.circbw_event(circ_id, CELL_PAYLOAD_SIZE, 0, delivered, 0, 0, 0, 1000.0);
matches!(
stats.check_circuit_limits(circ_id, config),
CircuitLimitResult::ServIntroBytesExceeded { .. }
)
}
fn check_maxbytes(
stats: &mut BandwidthStats,
config: &BandguardsConfig,
circ_id: &str,
) -> bool {
let limit = config.circ_max_megabytes * BYTES_PER_MB;
let chunk = 1000 * CELL_PAYLOAD_SIZE;
let mut read: u64 = 0;
while read + 2 * chunk < limit {
let delivered = (CELL_DATA_RATE * chunk as f64) as u64;
stats.circbw_event(circ_id, chunk, chunk, delivered, 0, 0, 0, 1000.0);
read += 2 * chunk;
if let CircuitLimitResult::MaxBytesExceeded { .. } =
stats.check_circuit_limits(circ_id, config)
{
return true;
}
}
let delivered = (CELL_DATA_RATE * (2 * chunk) as f64) as u64;
stats.circbw_event(circ_id, 2 * chunk, 0, delivered, 0, 0, 0, 1000.0);
matches!(
stats.check_circuit_limits(circ_id, config),
CircuitLimitResult::MaxBytesExceeded { .. }
)
}
fn check_dropped_bytes(
stats: &mut BandwidthStats,
config: &BandguardsConfig,
circ_id: &str,
delivered_cells: u64,
dropped_cells: u64,
) -> Option<CircuitLimitResult> {
let valid_bytes = (CELL_DATA_RATE * CELL_PAYLOAD_SIZE as f64 / 2.0) as u64;
for _ in 0..delivered_cells {
stats.circbw_event(
circ_id,
CELL_PAYLOAD_SIZE,
CELL_PAYLOAD_SIZE,
valid_bytes,
0,
valid_bytes,
0,
1000.0,
);
let result = stats.check_circuit_limits(circ_id, config);
if !matches!(result, CircuitLimitResult::Ok) {
return Some(result);
}
}
for _ in 0..dropped_cells {
stats.circbw_event(
circ_id,
CELL_PAYLOAD_SIZE,
CELL_PAYLOAD_SIZE,
0,
0,
0,
0,
1000.0,
);
let result = stats.check_circuit_limits(circ_id, config);
if !matches!(result, CircuitLimitResult::Ok) {
return Some(result);
}
}
None
}
#[test]
fn test_circuit_built_failed_closed_removed_from_map() {
let mut stats = BandwidthStats::new();
stats.circ_event("1", "LAUNCHED", "HS_VANGUARDS", None, &[], None, 1000.0);
stats.circ_event("1", "BUILT", "HS_VANGUARDS", None, &[], None, 1001.0);
assert!(stats.circs.contains_key("1"));
stats.circ_event("1", "FAILED", "HS_VANGUARDS", None, &[], None, 1002.0);
assert!(!stats.circs.contains_key("1"));
stats.circ_event("1", "CLOSED", "HS_VANGUARDS", None, &[], None, 1003.0);
assert!(!stats.circs.contains_key("1"));
}
#[test]
fn test_circuit_built_closed_removed_from_map() {
let mut stats = BandwidthStats::new();
stats.circ_event("2", "LAUNCHED", "HS_VANGUARDS", None, &[], None, 1000.0);
stats.circ_event("2", "BUILT", "HS_VANGUARDS", None, &[], None, 1001.0);
assert!(stats.circs.contains_key("2"));
stats.circ_event("2", "CLOSED", "HS_VANGUARDS", None, &[], None, 1002.0);
assert!(!stats.circs.contains_key("2"));
}
#[test]
fn test_hsdir_size_cap_exceeded_direct_service_circ() {
let mut stats = BandwidthStats::new();
let config = BandguardsConfig {
circ_max_hsdesc_kilobytes: 30,
..Default::default()
};
stats.circ_event(
"3",
"LAUNCHED",
"HS_SERVICE_HSDIR",
Some("HSSI_CONNECTING"),
&[],
None,
1000.0,
);
stats.circ_event(
"3",
"BUILT",
"HS_SERVICE_HSDIR",
Some("HSSI_CONNECTING"),
&[],
None,
1001.0,
);
let circ = stats.circs.get("3").unwrap();
assert!(circ.is_hsdir);
assert!(circ.is_service);
assert!(check_hsdir(&mut stats, &config, "3"));
}
#[test]
fn test_hsdir_size_cap_disabled() {
let mut stats = BandwidthStats::new();
let config = BandguardsConfig {
circ_max_hsdesc_kilobytes: 0,
..Default::default()
};
stats.circ_event(
"5",
"LAUNCHED",
"HS_SERVICE_HSDIR",
Some("HSSI_CONNECTING"),
&[],
None,
1000.0,
);
stats.circ_event(
"5",
"BUILT",
"HS_SERVICE_HSDIR",
Some("HSSI_CONNECTING"),
&[],
None,
1001.0,
);
assert!(!check_hsdir(&mut stats, &config, "5"));
}
#[test]
fn test_intro_size_cap_disabled_by_default() {
let mut stats = BandwidthStats::new();
let config = BandguardsConfig::default();
assert_eq!(config.circ_max_serv_intro_kilobytes, 0);
stats.circ_event(
"6",
"LAUNCHED",
"HS_SERVICE_INTRO",
Some("HSSI_CONNECTING"),
&[],
None,
1000.0,
);
stats.circ_event(
"6",
"BUILT",
"HS_SERVICE_INTRO",
Some("HSSI_CONNECTING"),
&[],
None,
1001.0,
);
let circ = stats.circs.get("6").unwrap();
assert!(circ.is_serv_intro);
assert!(circ.is_service);
assert!(!check_serv_intro(&mut stats, &config, "6"));
}
#[test]
fn test_intro_size_cap_exceeded() {
let mut stats = BandwidthStats::new();
let config = BandguardsConfig {
circ_max_serv_intro_kilobytes: 1024,
..Default::default()
};
stats.circ_event(
"7",
"LAUNCHED",
"HS_SERVICE_INTRO",
Some("HSSI_CONNECTING"),
&[],
None,
1000.0,
);
stats.circ_event(
"7",
"BUILT",
"HS_SERVICE_INTRO",
Some("HSSI_CONNECTING"),
&[],
None,
1001.0,
);
assert!(check_serv_intro(&mut stats, &config, "7"));
}
#[test]
fn test_max_bytes_exceeded() {
let mut stats = BandwidthStats::new();
let config = BandguardsConfig {
circ_max_megabytes: 100,
..Default::default()
};
stats.circ_event("10", "LAUNCHED", "HS_VANGUARDS", None, &[], None, 1000.0);
stats.circ_event("10", "BUILT", "HS_VANGUARDS", None, &[], None, 1001.0);
assert!(check_maxbytes(&mut stats, &config, "10"));
}
#[test]
fn test_max_bytes_disabled() {
let mut stats = BandwidthStats::new();
let config = BandguardsConfig {
circ_max_megabytes: 0,
..Default::default()
};
stats.circ_event(
"11",
"LAUNCHED",
"HS_SERVICE_REND",
Some("HSSR_CONNECTING"),
&[],
None,
1000.0,
);
stats.circ_event(
"11",
"BUILT",
"HS_SERVICE_REND",
Some("HSSR_CONNECTING"),
&[],
None,
1001.0,
);
assert!(!check_maxbytes(&mut stats, &config, "11"));
}
#[test]
fn test_regular_reading_ok() {
let mut stats = BandwidthStats::new();
let config = BandguardsConfig::default();
stats.circ_event("20", "LAUNCHED", "HS_VANGUARDS", None, &[], None, 1000.0);
stats.circ_event("20", "BUILT", "HS_VANGUARDS", None, &[], None, 1001.0);
let result = check_dropped_bytes(&mut stats, &config, "20", 100, 0);
assert!(result.is_none());
}
#[test]
fn test_dropped_cells_before_app_data() {
let mut stats = BandwidthStats::new();
let config = BandguardsConfig::default();
stats.circ_event("21", "LAUNCHED", "HS_VANGUARDS", None, &[], None, 1000.0);
stats.circ_event("21", "BUILT", "HS_VANGUARDS", None, &[], None, 1001.0);
let result = check_dropped_bytes(&mut stats, &config, "21", 0, 1);
assert!(matches!(
result,
Some(CircuitLimitResult::DroppedCells { .. })
));
}
#[test]
fn test_dropped_cells_after_app_data() {
let mut stats = BandwidthStats::new();
let config = BandguardsConfig::default();
stats.circ_event("22", "LAUNCHED", "HS_VANGUARDS", None, &[], None, 1000.0);
stats.circ_event("22", "BUILT", "HS_VANGUARDS", None, &[], None, 1001.0);
let result = check_dropped_bytes(&mut stats, &config, "22", 1000, 1);
assert!(matches!(
result,
Some(CircuitLimitResult::DroppedCells { .. })
));
}
#[test]
fn test_dropped_cells_allowed_on_not_built_circ() {
let mut stats = BandwidthStats::new();
let config = BandguardsConfig::default();
stats.circ_event("23", "LAUNCHED", "HS_VANGUARDS", None, &[], None, 1000.0);
stats.circ_event("23", "EXTENDED", "HS_VANGUARDS", None, &[], None, 1001.0);
let result = check_dropped_bytes(&mut stats, &config, "23", 0, 1);
assert!(result.is_none());
}
#[test]
fn test_general_circ_dropped_cells() {
let mut stats = BandwidthStats::new();
let config = BandguardsConfig::default();
stats.circ_event("24", "LAUNCHED", "GENERAL", None, &[], None, 1000.0);
stats.circ_event("24", "BUILT", "GENERAL", None, &[], None, 1001.0);
let result = check_dropped_bytes(&mut stats, &config, "24", 1000, 1);
assert!(matches!(
result,
Some(CircuitLimitResult::DroppedCells { .. })
));
}
#[test]
fn test_orconn_connected() {
let mut stats = BandwidthStats::new();
let guard_fp = "5416F3E8F80101A133B1970495B04FDBD1C7446B";
stats.orconn_event("11", guard_fp, "CONNECTED", None, 1000.0);
assert!(stats.live_guard_conns.contains_key("11"));
assert!(stats.guards.contains_key(guard_fp));
assert_eq!(stats.guards.get(guard_fp).unwrap().conns_made, 1);
}
#[test]
fn test_orconn_closed() {
let mut stats = BandwidthStats::new();
let guard_fp = "5416F3E8F80101A133B1970495B04FDBD1C7446B";
stats.orconn_event("11", guard_fp, "CONNECTED", None, 1000.0);
assert!(stats.live_guard_conns.contains_key("11"));
stats.orconn_event("11", guard_fp, "CLOSED", Some("DONE"), 1001.0);
assert!(!stats.live_guard_conns.contains_key("11"));
}
#[test]
fn test_no_conns_since_tracking() {
let mut stats = BandwidthStats::new();
let guard_fp = "5416F3E8F80101A133B1970495B04FDBD1C7446B";
assert!(stats.no_conns_since.is_some());
stats.orconn_event("1", guard_fp, "CONNECTED", None, 1000.0);
assert!(stats.no_conns_since.is_none());
stats.orconn_event("1", guard_fp, "CLOSED", None, 1001.0);
assert!(stats.no_conns_since.is_some());
}
#[test]
fn test_connectivity_check_no_connections() {
let mut stats = BandwidthStats::new();
let config = BandguardsConfig {
conn_max_disconnected_secs: 15,
..Default::default()
};
stats.no_conns_since = Some(1000.0);
let status = stats.check_connectivity(1020.0, &config);
assert!(matches!(
status,
ConnectivityStatus::NoConnections { secs: 20 }
));
}
#[test]
fn test_connectivity_disabled() {
let mut stats = BandwidthStats::new();
let config = BandguardsConfig {
conn_max_disconnected_secs: 0,
..Default::default()
};
stats.no_conns_since = Some(1000.0);
let status = stats.check_connectivity(2000.0, &config);
assert_eq!(status, ConnectivityStatus::Connected);
}
#[test]
fn test_circ_minor_purpose_changed() {
let mut stats = BandwidthStats::new();
let path = vec!["5416F3E8F80101A133B1970495B04FDBD1C7446B".to_string()];
stats.circ_event("30", "LAUNCHED", "HS_VANGUARDS", None, &[], None, 1000.0);
stats.circ_event("30", "BUILT", "HS_VANGUARDS", None, &path, None, 1001.0);
stats.circ_minor_event(
"30",
"PURPOSE_CHANGED",
"HS_SERVICE_REND",
Some("HSSR_CONNECTING"),
Some("HS_VANGUARDS"),
None,
&path,
);
let circ = stats.circs.get("30").unwrap();
assert_eq!(circ.purpose, Some("HS_SERVICE_REND".to_string()));
assert!(circ.in_use);
assert_eq!(circ.guard_fp, Some(path[0].clone()));
}
#[test]
fn test_circ_minor_cannibalized_to_hsdir() {
let mut stats = BandwidthStats::new();
stats.circ_event("31", "LAUNCHED", "HS_VANGUARDS", None, &[], None, 1000.0);
stats.circ_event("31", "BUILT", "HS_VANGUARDS", None, &[], None, 1001.0);
let circ = stats.circs.get("31").unwrap();
assert!(!circ.is_hsdir);
stats.circ_minor_event(
"31",
"CANNIBALIZED",
"HS_CLIENT_HSDIR",
Some("HSCI_CONNECTING"),
Some("HS_VANGUARDS"),
None,
&[],
);
let circ = stats.circs.get("31").unwrap();
assert!(circ.is_hsdir);
assert!(!circ.is_service);
}
#[test]
fn test_circ_minor_cannibalized_to_serv_intro() {
let mut stats = BandwidthStats::new();
stats.circ_event("32", "LAUNCHED", "HS_VANGUARDS", None, &[], None, 1000.0);
stats.circ_event("32", "BUILT", "HS_VANGUARDS", None, &[], None, 1001.0);
let circ = stats.circs.get("32").unwrap();
assert!(!circ.is_serv_intro);
stats.circ_minor_event(
"32",
"CANNIBALIZED",
"HS_SERVICE_INTRO",
Some("HSSI_CONNECTING"),
Some("HS_VANGUARDS"),
None,
&[],
);
let circ = stats.circs.get("32").unwrap();
assert!(circ.is_serv_intro);
assert!(circ.is_service);
}
#[test]
fn test_tor_bug_29699_workaround() {
let mut stats = BandwidthStats::new();
let config = BandguardsConfig::default();
stats.circ_event(
"40",
"LAUNCHED",
"HS_SERVICE_INTRO",
Some("HSSI_ESTABLISHED"),
&[],
None,
1000.0,
);
stats.circ_event(
"40",
"BUILT",
"HS_SERVICE_INTRO",
Some("HSSI_ESTABLISHED"),
&[],
None,
1001.0,
);
stats.circbw_event("40", CELL_PAYLOAD_SIZE, 0, 0, 0, 0, 0, 1002.0);
let result = stats.check_circuit_limits("40", &config);
assert!(matches!(
result,
CircuitLimitResult::TorBug {
bug_id: "#29699",
..
}
));
}
#[test]
fn test_tor_bug_29700_workaround() {
let mut stats = BandwidthStats::new();
let config = BandguardsConfig::default();
stats.circ_event(
"41",
"LAUNCHED",
"HS_SERVICE_REND",
Some("HSSR_CONNECTING"),
&[],
None,
1000.0,
);
stats.circ_event(
"41",
"BUILT",
"HS_SERVICE_REND",
Some("HSSR_CONNECTING"),
&[],
None,
1001.0,
);
stats.circbw_event("41", CELL_PAYLOAD_SIZE, 0, 0, 0, 0, 0, 1002.0);
let result = stats.check_circuit_limits("41", &config);
assert!(matches!(
result,
CircuitLimitResult::TorBug {
bug_id: "#29700",
..
}
));
}
#[test]
fn test_tor_bug_29786_workaround() {
let mut stats = BandwidthStats::new();
let config = BandguardsConfig::default();
stats.circ_event(
"42",
"LAUNCHED",
"PATH_BIAS_TESTING",
None,
&[],
None,
1000.0,
);
stats.circ_event("42", "BUILT", "PATH_BIAS_TESTING", None, &[], None, 1001.0);
stats.circbw_event("42", CELL_PAYLOAD_SIZE, 0, 0, 0, 0, 0, 1002.0);
let result = stats.check_circuit_limits("42", &config);
assert!(matches!(
result,
CircuitLimitResult::TorBug {
bug_id: "#29786",
..
}
));
}
#[test]
fn test_tor_bug_29927_workaround() {
let mut stats = BandwidthStats::new();
let config = BandguardsConfig::default();
stats.circ_event(
"43",
"LAUNCHED",
"HS_CLIENT_INTRO",
Some("HSCI_DONE"),
&[],
None,
1000.0,
);
stats.circ_event(
"43",
"BUILT",
"HS_CLIENT_INTRO",
Some("HSCI_DONE"),
&[],
None,
1001.0,
);
stats.circbw_event("43", CELL_PAYLOAD_SIZE, 0, 0, 0, 0, 0, 1002.0);
let result = stats.check_circuit_limits("43", &config);
assert!(matches!(
result,
CircuitLimitResult::TorBug {
bug_id: "#29927",
..
}
));
}
#[test]
fn test_stray_circ_minor_event() {
let mut stats = BandwidthStats::new();
stats.circ_minor_event(
"999",
"CANNIBALIZED",
"HS_SERVICE_REND",
Some("HSSR_CONNECTING"),
Some("HS_VANGUARDS"),
None,
&[],
);
assert!(!stats.circs.contains_key("999"));
}
}
#[cfg(test)]
mod proptests {
use super::*;
use proptest::prelude::*;
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn bandwidth_tracking_accuracy(
events in prop::collection::vec(
(100u64..10000, 100u64..10000, 50u64..5000, 50u64..5000, 10u64..500, 10u64..500),
1..20
),
) {
let mut stats = BandwidthStats::new();
stats.circ_event("123", "LAUNCHED", "GENERAL", None, &[], None, 1000.0);
let mut expected_read = 0u64;
let mut expected_sent = 0u64;
let mut expected_delivered_read = 0u64;
let mut expected_delivered_sent = 0u64;
let mut expected_overhead_read = 0u64;
let mut expected_overhead_sent = 0u64;
for (i, (read, written, del_read, del_written, oh_read, oh_written)) in events.iter().enumerate() {
stats.circbw_event(
"123",
*read,
*written,
*del_read,
*del_written,
*oh_read,
*oh_written,
1001.0 + i as f64,
);
expected_read += read;
expected_sent += written;
expected_delivered_read += del_read;
expected_delivered_sent += del_written;
expected_overhead_read += oh_read;
expected_overhead_sent += oh_written;
}
let circ = stats.circs.get("123").unwrap();
prop_assert_eq!(circ.read_bytes, expected_read);
prop_assert_eq!(circ.sent_bytes, expected_sent);
prop_assert_eq!(circ.delivered_read_bytes, expected_delivered_read);
prop_assert_eq!(circ.delivered_sent_bytes, expected_delivered_sent);
prop_assert_eq!(circ.overhead_read_bytes, expected_overhead_read);
prop_assert_eq!(circ.overhead_sent_bytes, expected_overhead_sent);
}
#[test]
fn circuit_limit_enforcement(
limit_mb in 1u64..100,
bytes_mb in 0u64..200,
) {
let mut stats = BandwidthStats::new();
let config = BandguardsConfig {
circ_max_megabytes: limit_mb,
..Default::default()
};
stats.circ_event("123", "BUILT", "GENERAL", None, &[], None, 1000.0);
let bytes = bytes_mb * 1024 * 1024;
let delivered = (bytes / CELL_PAYLOAD_SIZE) * RELAY_PAYLOAD_SIZE;
stats.circbw_event("123", bytes, 0, delivered, 0, 0, 0, 1001.0);
let result = stats.check_circuit_limits("123", &config);
if bytes > limit_mb * 1024 * 1024 {
match result {
CircuitLimitResult::MaxBytesExceeded { .. } => {}
_ => prop_assert!(false, "Expected MaxBytesExceeded for {} bytes > {} MB limit", bytes, limit_mb),
}
} else {
prop_assert_eq!(result, CircuitLimitResult::Ok,
"Expected Ok for {} bytes <= {} MB limit", bytes, limit_mb);
}
}
#[test]
fn dropped_cell_detection(
cells_received in 10u64..1000,
cells_delivered in 0u64..1000,
cells_overhead in 0u64..100,
) {
let mut circ = BwCircuitStat::new("123".to_string(), false);
circ.read_bytes = cells_received * CELL_PAYLOAD_SIZE;
circ.delivered_read_bytes = cells_delivered * RELAY_PAYLOAD_SIZE;
circ.overhead_read_bytes = cells_overhead * RELAY_PAYLOAD_SIZE;
let dropped = circ.dropped_read_cells();
let expected_dropped = cells_received as i64 - (cells_delivered + cells_overhead) as i64;
prop_assert_eq!(dropped, expected_dropped,
"Expected {} dropped cells, got {}", expected_dropped, dropped);
}
}
}