1use std::collections::HashMap;
171
172use crate::config::BandguardsConfig;
173
174pub const CELL_PAYLOAD_SIZE: u64 = 509;
176
177pub const RELAY_HEADER_SIZE: u64 = 11;
179
180pub const RELAY_PAYLOAD_SIZE: u64 = CELL_PAYLOAD_SIZE - RELAY_HEADER_SIZE;
182
183const SECS_PER_HOUR: u64 = 3600;
185
186const BYTES_PER_KB: u64 = 1024;
188
189const BYTES_PER_MB: u64 = 1024 * BYTES_PER_KB;
191
192pub const MAX_CIRC_DESTROY_LAG_SECS: u64 = 2;
194
195#[derive(Debug, Clone)]
259pub struct BwCircuitStat {
260 pub circ_id: String,
262 pub is_hs: bool,
264 pub is_service: bool,
266 pub is_hsdir: bool,
268 pub is_serv_intro: bool,
270 pub dropped_cells_allowed: u64,
272 pub purpose: Option<String>,
274 pub hs_state: Option<String>,
276 pub old_purpose: Option<String>,
278 pub old_hs_state: Option<String>,
280 pub in_use: bool,
282 pub built: bool,
284 pub created_at: f64,
286 pub read_bytes: u64,
288 pub sent_bytes: u64,
290 pub delivered_read_bytes: u64,
292 pub delivered_sent_bytes: u64,
294 pub overhead_read_bytes: u64,
296 pub overhead_sent_bytes: u64,
298 pub guard_fp: Option<String>,
300 pub possibly_destroyed_at: Option<f64>,
302}
303
304impl BwCircuitStat {
305 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 pub fn total_bytes(&self) -> u64 {
342 self.read_bytes + self.sent_bytes
343 }
344
345 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 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 pub fn age_hours(&self) -> f64 {
377 self.age_secs() / SECS_PER_HOUR as f64
378 }
379}
380
381#[derive(Debug, Clone)]
385pub struct BwGuardStat {
386 pub to_guard: String,
388 pub killed_conns: u32,
390 pub killed_conn_at: f64,
392 pub killed_conn_pending: bool,
394 pub conns_made: u32,
396 pub close_reasons: HashMap<String, u32>,
398}
399
400impl BwGuardStat {
401 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 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#[derive(Debug, Clone)]
497pub struct BandwidthStats {
498 pub circs: HashMap<String, BwCircuitStat>,
500 pub live_guard_conns: HashMap<String, BwGuardStat>,
502 pub guards: HashMap<String, BwGuardStat>,
504 pub circs_destroyed_total: u64,
506 pub no_conns_since: Option<f64>,
508 pub no_circs_since: Option<f64>,
510 pub network_down_since: Option<f64>,
512 pub max_fake_id: i32,
514 pub disconnected_circs: bool,
516 pub disconnected_conns: bool,
518}
519
520impl Default for BandwidthStats {
521 fn default() -> Self {
522 Self::new()
523 }
524}
525
526impl BandwidthStats {
527 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 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 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 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 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 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 fn fixup_orconn_id(&self, conn_id: &str, guard_fp: &str) -> String {
623 if let Ok(id) = conn_id.parse::<i32>() {
625 if id <= self.max_fake_id {
626 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 #[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 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 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 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 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 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 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 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 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 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 #[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 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 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 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 #[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 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 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 let dropped = circ.dropped_read_cells();
883 if dropped > circ.dropped_cells_allowed as i64 {
884 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 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 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 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 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 if purpose == "HS_SERVICE_INTRO" && hs_state == "HSSI_ESTABLISHED" {
948 return Some("#29699");
949 }
950
951 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 if purpose == "HS_CLIENT_REND" || (purpose == "HS_CLIENT_INTRO" && hs_state == "HSCI_DONE")
961 {
962 return Some("#29927");
963 }
964
965 if purpose == "HS_SERVICE_REND" && hs_state == "HSSR_CONNECTING" {
967 return Some("#29700");
968 }
969
970 if purpose == "PATH_BIAS_TESTING" {
972 return Some("#29786");
973 }
974
975 None
976 }
977
978 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 pub fn check_connectivity(
1011 &mut self,
1012 now: f64,
1013 config: &BandguardsConfig,
1014 ) -> ConnectivityStatus {
1015 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 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 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 pub fn circuit_count(&self) -> usize {
1076 self.circs.len()
1077 }
1078
1079 pub fn live_connection_count(&self) -> usize {
1081 self.live_guard_conns.len()
1082 }
1083}
1084
1085#[derive(Debug, Clone, PartialEq)]
1087pub enum CircuitLimitResult {
1088 Ok,
1090 DroppedCells {
1092 dropped_cells: i64,
1094 },
1095 TorBug {
1097 bug_id: &'static str,
1099 dropped_cells: i64,
1101 },
1102 MaxBytesExceeded {
1104 bytes: u64,
1106 limit: u64,
1108 },
1109 HsdirBytesExceeded {
1111 bytes: u64,
1113 limit: u64,
1115 },
1116 ServIntroBytesExceeded {
1118 bytes: u64,
1120 limit: u64,
1122 },
1123}
1124
1125#[derive(Debug, Clone, PartialEq)]
1127pub enum ConnectivityStatus {
1128 Connected,
1130 NoConnections {
1132 secs: u32,
1134 },
1135 CircuitsFailing {
1137 secs: u32,
1139 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 circ.read_bytes = 5090;
1174 circ.delivered_read_bytes = 3984;
1176 circ.overhead_read_bytes = 0;
1177
1178 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 circ.read_bytes = 5090;
1188 circ.delivered_read_bytes = 3486; circ.overhead_read_bytes = 498; 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, ..Default::default()
1338 };
1339
1340 stats.circ_event("123", "BUILT", "GENERAL", None, &[], None, 1000.0);
1341 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 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}