#![allow(non_snake_case)] use crate::{ analysis::{blocked_in, Analyzer}, bridge_verification_info::BridgeVerificationInfo, *, }; use lox_library::{ bridge_table::{self, BridgeLine, BridgeTable}, cred::Lox, proto::*, scalar_u32, BridgeAuth, BridgeDb, }; use base64::{engine::general_purpose, Engine as _}; use curve25519_dalek::{ristretto::RistrettoBasepointTable, Scalar}; use rand::RngCore; use sha1::{Digest, Sha1}; use std::{ collections::{BTreeMap, HashMap, HashSet}, sync::{Arc, Mutex}, }; use x25519_dalek::{PublicKey, StaticSecret}; struct TestHarness { bdb: BridgeDb, pub ba: BridgeAuth, } impl TestHarness { fn new() -> Self { TestHarness::new_buckets(5, 5) } fn new_buckets(num_buckets: u16, hot_spare: u16) -> Self { // Create a BridegDb let mut bdb = BridgeDb::new(); // Create a BridgeAuth let mut ba = BridgeAuth::new(bdb.pubkey); // Make 3 x num_buckets open invitation bridges, in sets of 3 for _ in 0..num_buckets { let bucket = [random(), random(), random()]; let _ = ba.add_openinv_bridges(bucket, &mut bdb); } // Add hot_spare more hot spare buckets for _ in 0..hot_spare { let bucket = [random(), random(), random()]; let _ = ba.add_spare_bucket(bucket, &mut bdb); } // Create the encrypted bridge table ba.enc_bridge_table(); Self { bdb, ba } } fn advance_days(&mut self, days: u16) { self.ba.advance_days(days); } fn get_new_credential(&mut self) -> Lox { let inv = self.bdb.invite().unwrap(); let (req, state) = open_invite::request(&inv); let resp = self.ba.handle_open_invite(req).unwrap(); let (cred, _bridgeline) = open_invite::handle_response(state, resp, &self.ba.lox_pub).unwrap(); cred } fn level_up(&mut self, cred: &Lox) -> Lox { let current_level = scalar_u32(&cred.trust_level).unwrap(); if current_level == 0 { self.advance_days(trust_promotion::UNTRUSTED_INTERVAL.try_into().unwrap()); let (promreq, promstate) = trust_promotion::request(cred, &self.ba.lox_pub, self.ba.today()).unwrap(); let promresp = self.ba.handle_trust_promotion(promreq).unwrap(); let migcred = trust_promotion::handle_response(promstate, promresp).unwrap(); let (migreq, migstate) = migration::request(cred, &migcred, &self.ba.lox_pub, &self.ba.migration_pub) .unwrap(); let migresp = self.ba.handle_migration(migreq).unwrap(); let new_cred = migration::handle_response(migstate, migresp, &self.ba.lox_pub).unwrap(); new_cred } else { self.advance_days( level_up::LEVEL_INTERVAL[usize::try_from(current_level).unwrap()] .try_into() .unwrap(), ); let (id, key) = bridge_table::from_scalar(cred.bucket).unwrap(); let encbuckets = self.ba.enc_bridge_table(); let bucket = bridge_table::BridgeTable::decrypt_bucket(id, &key, encbuckets.get(&id).unwrap()) .unwrap(); let reachcred = bucket.1.unwrap(); let (lvreq, lvstate) = level_up::request( cred, &reachcred, &self.ba.lox_pub, &self.ba.reachability_pub, self.ba.today(), ) .unwrap(); let lvresp = self.ba.handle_level_up(lvreq).unwrap(); let new_cred = level_up::handle_response(lvstate, lvresp, &self.ba.lox_pub).unwrap(); new_cred } } fn get_bucket(&mut self, cred: &Lox) -> [BridgeLine; bridge_table::MAX_BRIDGES_PER_BUCKET] { let (id, key) = bridge_table::from_scalar(cred.bucket).unwrap(); let encbuckets = self.ba.enc_bridge_table(); let bucket = bridge_table::BridgeTable::decrypt_bucket(id, &key, encbuckets.get(&id).unwrap()) .unwrap(); bucket.0 } } pub fn random() -> BridgeLine { let mut rng = rand::thread_rng(); let mut res: BridgeLine = BridgeLine::default(); // Pick a random 4-byte address let mut addr: [u8; 4] = [0; 4]; rng.fill_bytes(&mut addr); // If the leading byte is 224 or more, that's not a valid IPv4 // address. Choose an IPv6 address instead (but don't worry too // much about it being well formed). if addr[0] >= 224 { rng.fill_bytes(&mut res.addr); } else { // Store an IPv4 address as a v4-mapped IPv6 address res.addr[10] = 255; res.addr[11] = 255; res.addr[12..16].copy_from_slice(&addr); }; let ports: [u16; 4] = [443, 4433, 8080, 43079]; let portidx = (rng.next_u32() % 4) as usize; res.port = ports[portidx]; res.uid_fingerprint = rng.next_u64(); rng.fill_bytes(&mut res.fingerprint); let mut cert: [u8; 52] = [0; 52]; rng.fill_bytes(&mut cert); let infostr: String = format!( "obfs4 cert={}, iat-mode=0", general_purpose::STANDARD_NO_PAD.encode(cert) ); res.info[..infostr.len()].copy_from_slice(infostr.as_bytes()); res } #[tokio::test] async fn test_download_extra_infos() { let bridge_to_test = array_bytes::hex2array("72E12B89136B45BBC81D1EF0AC7DDDBB91B148DB").unwrap(); // Open test database let db: Db = sled::open("test_db_dei").unwrap(); // Delete all data in test DB db.clear().unwrap(); assert!(!db.contains_key("bridges").unwrap()); assert!(!db.contains_key(bridge_to_test).unwrap()); // Download and process recent extra-infos files update_extra_infos( &db, "https://collector.torproject.org/recent/bridge-descriptors/extra-infos/", ) .await .unwrap(); // Check that DB contains information on a bridge with high uptime assert!(db.contains_key("bridges").unwrap()); let bridges: HashSet<[u8; 20]> = bincode::deserialize(&db.get("bridges").unwrap().unwrap()).unwrap(); assert!(bridges.contains(&bridge_to_test)); assert!(db.contains_key(bridge_to_test).unwrap()); let _bridge_info: BridgeInfo = bincode::deserialize(&db.get(bridge_to_test).unwrap().unwrap()).unwrap(); } #[test] fn test_simulate_extra_infos() { let extra_info_str = r#"@type bridge-extra-info 1.3 extra-info ElephantBridgeDE2 72E12B89136B45BBC81D1EF0AC7DDDBB91B148DB master-key-ed25519 eWxjRwAWW7n8BGG9fNa6rApmBFbe3f0xcD7dqwOICW8 published 2024-04-06 03:51:04 transport obfs4 write-history 2024-04-05 04:55:22 (86400 s) 31665735680,14918491136,15423603712,36168353792,40396827648 read-history 2024-04-05 04:55:22 (86400 s) 31799622656,15229917184,15479115776,36317251584,40444155904 ipv6-write-history 2024-04-05 04:55:22 (86400 s) 5972127744,610078720,516897792,327949312,640708608 ipv6-read-history 2024-04-05 04:55:22 (86400 s) 4156158976,4040448000,2935756800,4263080960,6513532928 dirreq-write-history 2024-04-05 04:55:22 (86400 s) 625217536,646188032,618014720,584386560,600778752 dirreq-read-history 2024-04-05 04:55:22 (86400 s) 18816000,19000320,18484224,17364992,18443264 geoip-db-digest 44073997E1ED63E183B79DE2A1757E9997A834E3 geoip6-db-digest C0BF46880C6C132D746683279CC90DD4B2D31786 dirreq-stats-end 2024-04-05 06:51:23 (86400 s) dirreq-v3-ips ru=16,au=8,by=8,cn=8,gb=8,ir=8,mt=8,nl=8,pl=8,tn=8,tr=8,us=8 dirreq-v3-reqs ru=72,gb=64,pl=32,cn=16,ir=16,us=16,au=8,by=8,mt=8,nl=8,tn=8,tr=8 dirreq-v3-resp ok=216,not-enough-sigs=0,unavailable=0,not-found=0,not-modified=328,busy=0 dirreq-v3-direct-dl complete=0,timeout=0,running=0 dirreq-v3-tunneled-dl complete=212,timeout=4,running=0,min=21595,d1=293347,d2=1624137,q1=1911800,d3=2066929,d4=2415000,md=2888500,d6=3264000,d7=3851333,q3=41> hidserv-stats-end 2024-04-05 06:51:23 (86400 s) hidserv-rend-relayed-cells 7924 delta_f=2048 epsilon=0.30 bin_size=1024 hidserv-dir-onions-seen -12 delta_f=8 epsilon=0.30 bin_size=8 hidserv-v3-stats-end 2024-04-05 12:00:00 (86400 s) hidserv-rend-v3-relayed-cells -4785 delta_f=2048 epsilon=0.30 bin_size=1024 hidserv-dir-v3-onions-seen 5 delta_f=8 epsilon=0.30 bin_size=8 padding-counts 2024-04-05 06:51:42 (86400 s) bin-size=10000 write-drop=0 write-pad=80000 write-total=79980000 read-drop=0 read-pad=1110000 read-total=7989000> bridge-stats-end 2024-04-05 06:51:44 (86400 s) bridge-ips ru=40,us=32,??=8,au=8,br=8,by=8,cn=8,de=8,eg=8,eu=8,gb=8,ge=8,hr=8,ie=8,ir=8,kp=8,lt=8,mt=8,nl=8,pl=8,ro=8,sg=8,tn=8,tr=8,vn=8 bridge-ip-versions v4=104,v6=8 bridge-ip-transports =56,obfs4=56 router-digest-sha256 zK0VMl3i0B2eaeQTR03e2hZ0i8ytkuhK/psgD2J1/lQ router-digest F30B38390C375E1EE74BFED844177804442569E0"#; let extra_info_set = ExtraInfo::parse_file(&extra_info_str); assert_eq!(extra_info_set.len(), 1); let extra_info = extra_info_set.iter().next().unwrap().clone(); let extra_info_str = extra_info.to_string(); let extra_info_2 = ExtraInfo::parse_file(&extra_info_str) .into_iter() .next() .unwrap() .clone(); assert_eq!(extra_info, extra_info_2); let bridge_to_test: [u8; 20] = array_bytes::hex2array("72E12B89136B45BBC81D1EF0AC7DDDBB91B148DB").unwrap(); // Open test database let db: Db = sled::open("test_db_sei").unwrap(); // Delete all data in test DB db.clear().unwrap(); assert!(!db.contains_key("bridges").unwrap()); assert!(!db.contains_key(bridge_to_test).unwrap()); // TODO: Run local web server and change this to update_extra_infos add_extra_info_to_db(&db, extra_info_2); // Check that DB contains information on a bridge with high uptime assert!(db.contains_key("bridges").unwrap()); let bridges: HashSet<[u8; 20]> = bincode::deserialize(&db.get("bridges").unwrap().unwrap()).unwrap(); assert!(bridges.contains(&bridge_to_test)); assert!(db.contains_key(bridge_to_test).unwrap()); let _bridge_info: BridgeInfo = bincode::deserialize(&db.get(bridge_to_test).unwrap().unwrap()).unwrap(); } #[test] fn test_negative_reports() { let mut th = TestHarness::new(); // Get new level 1 credential let cred = th.get_new_credential(); let cred = th.level_up(&cred); let bridges = th.get_bucket(&cred); // Create BridgeVerificationInfo for each bridge let mut buckets = HashSet::::new(); buckets.insert(cred.bucket); let bridge_info_1 = BridgeVerificationInfo { bridge_line: bridges[0], buckets: buckets.clone(), pubkey: None, }; let bridge_info_2 = BridgeVerificationInfo { bridge_line: bridges[1], buckets: buckets.clone(), pubkey: None, }; let bridge_info_3 = BridgeVerificationInfo { bridge_line: bridges[2], buckets: buckets.clone(), pubkey: None, }; // Create reports let report_1 = NegativeReport::from_bridgeline(bridges[0], "ru".to_string(), BridgeDistributor::Lox); let report_2 = NegativeReport::from_lox_bucket(bridges[1].fingerprint, cred.bucket, "ru".to_string()); let report_3 = NegativeReport::from_lox_credential(bridges[2].fingerprint, &cred, "ru".to_string()); // Backdated reports let date = get_date(); let mut rng = rand::thread_rng(); let mut nonce = [0; 32]; rng.fill_bytes(&mut nonce); let report_4 = NegativeReport::new( bridges[0].fingerprint, ProofOfBridgeKnowledge::HashOfBridgeLine(HashOfBridgeLine::new( &bridges[0], date - 1, nonce, )), "ru".to_string(), date - 1, nonce, BridgeDistributor::Lox, ); let mut nonce = [0; 32]; rng.fill_bytes(&mut nonce); let report_5 = NegativeReport::new( bridges[1].fingerprint, ProofOfBridgeKnowledge::HashOfBridgeLine(HashOfBridgeLine::new( &bridges[1], date - 2, nonce, )), "ru".to_string(), date - 2, nonce, BridgeDistributor::Lox, ); let mut nonce = [0; 32]; rng.fill_bytes(&mut nonce); let report_6 = NegativeReport::new( bridges[2].fingerprint, ProofOfBridgeKnowledge::HashOfBridgeLine(HashOfBridgeLine::new( &bridges[2], date - 3, nonce, )), "ru".to_string(), date - 3, nonce, BridgeDistributor::Lox, ); // Verify reports assert!(report_1.verify(&bridge_info_1)); assert!(report_2.verify(&bridge_info_2)); assert!(report_3.verify(&bridge_info_3)); assert!(report_4.verify(&bridge_info_1)); assert!(report_5.verify(&bridge_info_2)); assert!(report_6.verify(&bridge_info_3)); // Check that deserialization fails under invalid conditions // Date in the future let mut invalid_report_1 = NegativeReport::from_bridgeline(bridges[0], "ru".to_string(), BridgeDistributor::Lox) .to_serializable_report(); invalid_report_1.date = invalid_report_1.date + 2; // Date too far in past let mut invalid_report_2 = NegativeReport::from_bridgeline(bridges[1], "ru".to_string(), BridgeDistributor::Lox) .to_serializable_report(); invalid_report_2.date = invalid_report_2.date - MAX_BACKDATE - 1; // Invalid country code let invalid_report_3 = NegativeReport::from_bridgeline(bridges[2], "xx".to_string(), BridgeDistributor::Lox) .to_serializable_report(); assert!(invalid_report_1.to_report().is_err()); assert!(invalid_report_2.to_report().is_err()); assert!(invalid_report_3.to_report().is_err()); // Check that verification fails with incorrect data let date = get_date(); let mut rng = rand::thread_rng(); // Incorrect BridgeLine hash let mut nonce = [0; 32]; rng.fill_bytes(&mut nonce); let invalid_report_4 = NegativeReport::new( bridges[0].fingerprint, ProofOfBridgeKnowledge::HashOfBridgeLine(HashOfBridgeLine::new( &BridgeLine::default(), date, nonce, )), "ru".to_string(), date, nonce, BridgeDistributor::Lox, ); // Incorrect bucket hash let mut nonce = [0; 32]; rng.fill_bytes(&mut nonce); let invalid_report_5 = NegativeReport::new( bridges[1].fingerprint, ProofOfBridgeKnowledge::HashOfBucket(HashOfBucket::new(&Scalar::ZERO, date, nonce)), "ru".to_string(), date, nonce, BridgeDistributor::Lox, ); assert!(!invalid_report_4.verify(&bridge_info_1)); assert!(!invalid_report_5.verify(&bridge_info_2)); // Test that reports with duplicate nonces are rejected // (Also test encryption and decryption.) // Open test database let db: Db = sled::open("test_db_nr").unwrap(); // Delete all data in test DB db.clear().unwrap(); assert!(!db.contains_key("nrs-to-process").unwrap()); let mut nonce = [0; 32]; rng.fill_bytes(&mut nonce); // A valid report let valid_report_1 = NegativeReport::new( bridges[0].fingerprint, ProofOfBridgeKnowledge::HashOfBridgeLine(HashOfBridgeLine::new(&bridges[0], date, nonce)), "ru".to_string(), date, nonce, BridgeDistributor::Lox, ); let valid_report_1_copy_1 = NegativeReport::new( bridges[0].fingerprint, ProofOfBridgeKnowledge::HashOfBridgeLine(HashOfBridgeLine::new(&bridges[0], date, nonce)), "ru".to_string(), date, nonce, BridgeDistributor::Lox, ); let valid_report_1_copy_2 = NegativeReport::new( bridges[0].fingerprint, ProofOfBridgeKnowledge::HashOfBridgeLine(HashOfBridgeLine::new(&bridges[0], date, nonce)), "ru".to_string(), date, nonce, BridgeDistributor::Lox, ); // Report which reuses this nonce let invalid_report_1 = NegativeReport::new( bridges[0].fingerprint, ProofOfBridgeKnowledge::HashOfBridgeLine(HashOfBridgeLine::new(&bridges[0], date, nonce)), "ru".to_string(), date, nonce, BridgeDistributor::Lox, ); // This is the same report assert_eq!(valid_report_1, invalid_report_1); // Report which reuses this nonce for a different bridge let invalid_report_2 = NegativeReport::new( bridges[1].fingerprint, ProofOfBridgeKnowledge::HashOfBridgeLine(HashOfBridgeLine::new(&bridges[1], date, nonce)), "ru".to_string(), date, nonce, BridgeDistributor::Lox, ); // Report which uses this nonce but on a different day let valid_report_2 = NegativeReport::new( bridges[0].fingerprint, ProofOfBridgeKnowledge::HashOfBridgeLine(HashOfBridgeLine::new( &bridges[0], date - 1, nonce, )), "ru".to_string(), date - 1, nonce, BridgeDistributor::Lox, ); // Report with different nonce let mut nonce = [0; 32]; rng.fill_bytes(&mut nonce); let valid_report_3 = NegativeReport::new( bridges[0].fingerprint, ProofOfBridgeKnowledge::HashOfBridgeLine(HashOfBridgeLine::new(&bridges[0], date, nonce)), "ru".to_string(), date, nonce, BridgeDistributor::Lox, ); let map_key_1 = format!( "{}_{}_{}", array_bytes::bytes2hex("", valid_report_1.fingerprint), "ru".to_string(), date ); // Generate key for today let secret = StaticSecret::random_from_rng(&mut rng); let public = PublicKey::from(&secret); let secret_yesterday = StaticSecret::random_from_rng(&mut rng); let public_yesterday = PublicKey::from(&secret_yesterday); assert!(!db.contains_key("nr-keys").unwrap()); // Fail to add to database because we can't decrypt handle_encrypted_negative_report(&db, valid_report_1_copy_1.encrypt(&public)); assert!(!db.contains_key("nrs-to-process").unwrap()); // Store yesterday's key but not today's let mut nr_keys = BTreeMap::::new(); nr_keys.insert(date - 1, secret_yesterday); db.insert("nr-keys", bincode::serialize(&nr_keys).unwrap()) .unwrap(); // Fail to add to database because we still can't decrypt handle_encrypted_negative_report(&db, valid_report_1_copy_2.encrypt(&public)); assert!(!db.contains_key("nrs-to-process").unwrap()); // Store today's key nr_keys.insert(date, secret); db.insert("nr-keys", bincode::serialize(&nr_keys).unwrap()) .unwrap(); handle_encrypted_negative_report(&db, valid_report_1.encrypt(&public)); let nrs_to_process: BTreeMap> = bincode::deserialize(&db.get("nrs-to-process").unwrap().unwrap()).unwrap(); let negative_reports = nrs_to_process.get(&map_key_1).unwrap(); assert_eq!(negative_reports.len(), 1); handle_encrypted_negative_report(&db, invalid_report_1.encrypt(&public)); // no change let nrs_to_process: BTreeMap> = bincode::deserialize(&db.get("nrs-to-process").unwrap().unwrap()).unwrap(); let negative_reports = nrs_to_process.get(&map_key_1).unwrap(); assert_eq!(negative_reports.len(), 1); let map_key_2 = format!( "{}_{}_{}", array_bytes::bytes2hex("", invalid_report_2.fingerprint), "ru".to_string(), date ); handle_encrypted_negative_report(&db, invalid_report_2.encrypt(&public)); // no change let nrs_to_process: BTreeMap> = bincode::deserialize(&db.get("nrs-to-process").unwrap().unwrap()).unwrap(); assert!(!nrs_to_process.contains_key(&map_key_2)); let map_key_3 = format!( "{}_{}_{}", array_bytes::bytes2hex("", valid_report_2.fingerprint), "ru".to_string(), date - 1 ); handle_encrypted_negative_report(&db, valid_report_2.encrypt(&public_yesterday)); let nrs_to_process: BTreeMap> = bincode::deserialize(&db.get("nrs-to-process").unwrap().unwrap()).unwrap(); let negative_reports = nrs_to_process.get(&map_key_3).unwrap(); assert_eq!(negative_reports.len(), 1); handle_encrypted_negative_report(&db, valid_report_3.encrypt(&public)); let nrs_to_process: BTreeMap> = bincode::deserialize(&db.get("nrs-to-process").unwrap().unwrap()).unwrap(); let negative_reports = nrs_to_process.get(&map_key_1).unwrap(); assert_eq!(negative_reports.len(), 2); // Same tests, but use hash of bucket // Delete all data in test DB db.clear().unwrap(); assert!(!db.contains_key("nrs-to-process").unwrap()); // Re-generate keys and save in database let public = new_negative_report_key(&db, date).unwrap(); let public_yesterday = new_negative_report_key(&db, date - 1).unwrap(); let mut nonce = [0; 32]; rng.fill_bytes(&mut nonce); // A valid report let valid_report_1 = NegativeReport::new( bridges[0].fingerprint, ProofOfBridgeKnowledge::HashOfBucket(HashOfBucket::new(&cred.bucket, date, nonce)), "ru".to_string(), date, nonce, BridgeDistributor::Lox, ); // Report which reuses this nonce let invalid_report_1 = NegativeReport::new( bridges[0].fingerprint, ProofOfBridgeKnowledge::HashOfBucket(HashOfBucket::new(&cred.bucket, date, nonce)), "ru".to_string(), date, nonce, BridgeDistributor::Lox, ); // This is the same report assert_eq!(valid_report_1, invalid_report_1); // Report which reuses this nonce for a different bridge let invalid_report_2 = NegativeReport::new( bridges[1].fingerprint, ProofOfBridgeKnowledge::HashOfBucket(HashOfBucket::new(&cred.bucket, date, nonce)), "ru".to_string(), date, nonce, BridgeDistributor::Lox, ); // Report which uses this nonce but on a different day let valid_report_2 = NegativeReport::new( bridges[0].fingerprint, ProofOfBridgeKnowledge::HashOfBucket(HashOfBucket::new(&cred.bucket, date - 1, nonce)), "ru".to_string(), date - 1, nonce, BridgeDistributor::Lox, ); // Report with different nonce let mut nonce = [0; 32]; rng.fill_bytes(&mut nonce); let valid_report_3 = NegativeReport::new( bridges[0].fingerprint, ProofOfBridgeKnowledge::HashOfBucket(HashOfBucket::new(&cred.bucket, date, nonce)), "ru".to_string(), date, nonce, BridgeDistributor::Lox, ); let map_key_1 = format!( "{}_{}_{}", array_bytes::bytes2hex("", valid_report_1.fingerprint), "ru".to_string(), date ); handle_encrypted_negative_report(&db, valid_report_1.encrypt(&public)); let nrs_to_process: BTreeMap> = bincode::deserialize(&db.get("nrs-to-process").unwrap().unwrap()).unwrap(); let negative_reports = nrs_to_process.get(&map_key_1).unwrap(); assert_eq!(negative_reports.len(), 1); handle_encrypted_negative_report(&db, invalid_report_1.encrypt(&public)); // no change let nrs_to_process: BTreeMap> = bincode::deserialize(&db.get("nrs-to-process").unwrap().unwrap()).unwrap(); let negative_reports = nrs_to_process.get(&map_key_1).unwrap(); assert_eq!(negative_reports.len(), 1); let map_key_2 = format!( "{}_{}_{}", array_bytes::bytes2hex("", invalid_report_2.fingerprint), "ru".to_string(), date ); handle_encrypted_negative_report(&db, invalid_report_2.encrypt(&public)); // no change let nrs_to_process: BTreeMap> = bincode::deserialize(&db.get("nrs-to-process").unwrap().unwrap()).unwrap(); assert!(!nrs_to_process.contains_key(&map_key_2)); let map_key_3 = format!( "{}_{}_{}", array_bytes::bytes2hex("", valid_report_2.fingerprint), "ru".to_string(), date - 1 ); handle_encrypted_negative_report(&db, valid_report_2.encrypt(&public_yesterday)); let nrs_to_process: BTreeMap> = bincode::deserialize(&db.get("nrs-to-process").unwrap().unwrap()).unwrap(); let negative_reports = nrs_to_process.get(&map_key_3).unwrap(); assert_eq!(negative_reports.len(), 1); handle_encrypted_negative_report(&db, valid_report_3.encrypt(&public)); let nrs_to_process: BTreeMap> = bincode::deserialize(&db.get("nrs-to-process").unwrap().unwrap()).unwrap(); let negative_reports = nrs_to_process.get(&map_key_1).unwrap(); assert_eq!(negative_reports.len(), 2); } #[test] fn test_positive_reports() { let mut th = TestHarness::new(); // Get new level 3 credential let cred = th.get_new_credential(); let cred = th.level_up(&cred); let cred = th.level_up(&cred); let cred = th.level_up(&cred); let bridges = th.get_bucket(&cred); // Create BridgeVerificationInfo for each bridge let mut buckets = HashSet::::new(); buckets.insert(cred.bucket); let bridge_info_1 = BridgeVerificationInfo { bridge_line: bridges[0], buckets: buckets.clone(), pubkey: None, }; let bridge_info_2 = BridgeVerificationInfo { bridge_line: bridges[1], buckets: buckets.clone(), pubkey: None, }; let bridge_info_3 = BridgeVerificationInfo { bridge_line: bridges[2], buckets: buckets.clone(), pubkey: None, }; // Create reports let report_1 = PositiveReport::from_lox_credential( bridges[0].fingerprint, None, &cred, &th.ba.lox_pub, "ru".to_string(), ) .unwrap(); let report_2 = PositiveReport::from_lox_credential( bridges[1].fingerprint, None, &cred, &th.ba.lox_pub, "ru".to_string(), ) .unwrap(); let report_3 = PositiveReport::from_lox_credential( bridges[2].fingerprint, None, &cred, &th.ba.lox_pub, "ru".to_string(), ) .unwrap(); // Compute Htable let H = lox_library::proto::positive_report::compute_H(report_1.date); let Htable = RistrettoBasepointTable::create(&H); assert!(report_1.verify(&mut th.ba, &bridge_info_1, &Htable)); assert!(report_2.verify(&mut th.ba, &bridge_info_2, &Htable)); assert!(report_3.verify(&mut th.ba, &bridge_info_3, &Htable)); // Check that user cannot use credential for other bridge // Get new credential let cred_2 = th.get_new_credential(); let bridges_2 = th.get_bucket(&cred_2); let mut buckets_2 = HashSet::::new(); buckets_2.insert(cred_2.bucket); let bridge_info_4 = BridgeVerificationInfo { bridge_line: bridges_2[0], buckets: buckets_2.clone(), pubkey: None, }; // Use new credential to create positive report even we don't trust it let invalid_report_1 = PositiveReport::from_lox_credential( bridges_2[0].fingerprint, None, &cred_2, &th.ba.lox_pub, "ru".to_string(), ); // Use first credential for bridge from second bucket let invalid_report_2 = PositiveReport::from_lox_credential( bridges_2[0].fingerprint, None, &cred, &th.ba.lox_pub, "ru".to_string(), ); // Use second credential for bridge from first bucket let invalid_report_3 = PositiveReport::from_lox_credential( bridges[0].fingerprint, None, &cred_2, &th.ba.lox_pub, "ru".to_string(), ); // Check that all of these fail assert!(invalid_report_1.is_err()); assert!(!invalid_report_2 .unwrap() .verify(&mut th.ba, &bridge_info_4, &Htable)); assert!(invalid_report_3.is_err()); // Check that deserialization fails under invalid conditions // Date in the future let mut invalid_report_4 = PositiveReport::from_lox_credential( bridges[0].fingerprint, None, &cred, &th.ba.lox_pub, "ru".to_string(), ) .unwrap() .to_serializable_report(); invalid_report_4.date = invalid_report_4.date + 2; // Invalid country code let invalid_report_5 = PositiveReport::from_lox_credential( bridges[0].fingerprint, None, &cred, &th.ba.lox_pub, "xx".to_string(), ) .unwrap() .to_serializable_report(); assert!(invalid_report_4.to_report().is_err()); assert!(invalid_report_5.to_report().is_err()); // Test storing to-be-processed positive reports to database // Create reports let report_1 = PositiveReport::from_lox_credential( bridges[0].fingerprint, None, &cred, &th.ba.lox_pub, "ru".to_string(), ) .unwrap(); let report_2 = PositiveReport::from_lox_credential( bridges[0].fingerprint, None, &cred, &th.ba.lox_pub, "ru".to_string(), ) .unwrap(); let report_3 = PositiveReport::from_lox_credential( bridges[1].fingerprint, None, &cred, &th.ba.lox_pub, "ru".to_string(), ) .unwrap(); // Open test database let db: Db = sled::open("test_db_pr").unwrap(); // Delete all data in test DB db.clear().unwrap(); assert!(!db.contains_key("prs-to-process").unwrap()); let map_key_1 = format!( "{}_{}_{}", array_bytes::bytes2hex("", report_1.fingerprint), &report_1.country, &report_1.date ); let map_key_2 = format!( "{}_{}_{}", array_bytes::bytes2hex("", report_3.fingerprint), &report_3.country, &report_3.date ); save_positive_report_to_process(&db, report_1); let prs_to_process: BTreeMap> = bincode::deserialize(&db.get("prs-to-process").unwrap().unwrap()).unwrap(); let positive_reports = prs_to_process.get(&map_key_1).unwrap(); assert_eq!(positive_reports.len(), 1); assert!(!prs_to_process.contains_key(&map_key_2)); save_positive_report_to_process(&db, report_2); let prs_to_process: BTreeMap> = bincode::deserialize(&db.get("prs-to-process").unwrap().unwrap()).unwrap(); let positive_reports = prs_to_process.get(&map_key_1).unwrap(); assert_eq!(positive_reports.len(), 2); assert!(!prs_to_process.contains_key(&map_key_2)); save_positive_report_to_process(&db, report_3); let prs_to_process: BTreeMap> = bincode::deserialize(&db.get("prs-to-process").unwrap().unwrap()).unwrap(); // Check that this has not changed let positive_reports = prs_to_process.get(&map_key_1).unwrap(); assert_eq!(positive_reports.len(), 2); // New report added to its own collection let positive_reports = prs_to_process.get(&map_key_2).unwrap(); assert_eq!(positive_reports.len(), 1); } #[test] fn test_analysis() { // Test stage 1 analysis { let mut date = get_date(); // New bridge info let mut bridge_info = BridgeInfo::new([0; 20], &String::default()); bridge_info .info_by_country .insert("ru".to_string(), BridgeCountryInfo::new(date)); let analyzer = analysis::NormalAnalyzer::new(5, 0.25); let confidence = 0.95; let mut blocking_countries = HashSet::::new(); // No data today assert_eq!( blocked_in(&analyzer, &bridge_info, confidence, date), blocking_countries ); // 1 connection, 0 negative reports date += 1; bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::BridgeIps, date, 8, ); assert_eq!( blocked_in(&analyzer, &bridge_info, confidence, date), blocking_countries ); // 0 connections, 0 negative reports date += 1; bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::BridgeIps, date, 0, ); assert_eq!( blocked_in(&analyzer, &bridge_info, confidence, date), blocking_countries ); // 0 connections, 1 negative report // (exceeds scaled threshold) date += 1; bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::NegativeReports, date, 1, ); blocking_countries.insert("ru".to_string()); assert_eq!( blocked_in(&analyzer, &bridge_info, confidence, date), blocking_countries ); } { let mut date = get_date(); // New bridge info let mut bridge_info = BridgeInfo::new([0; 20], &String::default()); bridge_info .info_by_country .insert("ru".to_string(), BridgeCountryInfo::new(date)); let analyzer = analysis::NormalAnalyzer::new(5, 0.25); let confidence = 0.95; let mut blocking_countries = HashSet::::new(); // No data today assert_eq!( blocked_in(&analyzer, &bridge_info, confidence, date), blocking_countries ); // 1 connection, 1 negative report date += 1; bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::BridgeIps, date, 8, ); bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::NegativeReports, date, 1, ); assert_eq!( blocked_in(&analyzer, &bridge_info, confidence, date), blocking_countries ); // 8 connections, 2 negative reports date += 1; bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::BridgeIps, date, 8, ); bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::NegativeReports, date, 2, ); assert_eq!( blocked_in(&analyzer, &bridge_info, confidence, date), blocking_countries ); // 8 connections, 3 negative reports // (exceeds scaled threshold) date += 1; bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::BridgeIps, date, 8, ); bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::NegativeReports, date, 3, ); blocking_countries.insert("ru".to_string()); assert_eq!( blocked_in(&analyzer, &bridge_info, confidence, date), blocking_countries ); } { let mut date = get_date(); // New bridge info let mut bridge_info = BridgeInfo::new([0; 20], &String::default()); bridge_info .info_by_country .insert("ru".to_string(), BridgeCountryInfo::new(date)); let analyzer = analysis::NormalAnalyzer::new(5, 0.25); let confidence = 0.95; let mut blocking_countries = HashSet::::new(); // 24 connections, 5 negative reports bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::BridgeIps, date, 24, ); bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::NegativeReports, date, 5, ); assert_eq!( blocked_in(&analyzer, &bridge_info, confidence, date), blocking_countries ); // 24 connections, 6 negative reports // (exceeds max threshold) date += 1; bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::BridgeIps, date, 24, ); bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::NegativeReports, date, 6, ); blocking_countries.insert("ru".to_string()); assert_eq!( blocked_in(&analyzer, &bridge_info, confidence, date), blocking_countries ); } // Test stage 2 analysis { let mut date = get_date(); // New bridge info let mut bridge_info = BridgeInfo::new([0; 20], &String::default()); bridge_info .info_by_country .insert("ru".to_string(), BridgeCountryInfo::new(date)); let analyzer = analysis::NormalAnalyzer::new(5, 0.25); let confidence = 0.95; let mut blocking_countries = HashSet::::new(); // No data today assert_eq!( blocked_in(&analyzer, &bridge_info, confidence, date), blocking_countries ); for i in 1..30 { // 9-32 connections, 0-3 negative reports each day date += 1; bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::BridgeIps, date, 8 * (i % 3 + 2), ); bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::NegativeReports, date, i % 4, ); assert_eq!( blocked_in(&analyzer, &bridge_info, confidence, date), blocking_countries ); } // Data similar to previous days: // 24 connections, 2 negative reports date += 1; bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::BridgeIps, date, 24, ); bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::NegativeReports, date, 2, ); // Should not be blocked because we have similar data. assert_eq!( blocked_in(&analyzer, &bridge_info, confidence, date), blocking_countries ); // Data different from previous days: // 104 connections, 1 negative report date += 1; bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::BridgeIps, date, 104, ); bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::NegativeReports, date, 1, ); // This should not be blocked even though it's very different because // it's different in the good direction. assert_eq!( blocked_in(&analyzer, &bridge_info, confidence, date), blocking_countries ); // Data different from previous days: // 40 connections, 12 negative reports date += 1; bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::BridgeIps, date, 40, ); bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::NegativeReports, date, 12, ); blocking_countries.insert("ru".to_string()); // This should be blocked because it's different in the bad direction. assert_eq!( blocked_in(&analyzer, &bridge_info, confidence, date), blocking_countries ); } { let mut date = get_date(); // New bridge info let mut bridge_info = BridgeInfo::new([0; 20], &String::default()); bridge_info .info_by_country .insert("ru".to_string(), BridgeCountryInfo::new(date)); let analyzer = analysis::NormalAnalyzer::new(5, 0.25); let confidence = 0.95; let mut blocking_countries = HashSet::::new(); // No data today assert_eq!( blocked_in(&analyzer, &bridge_info, confidence, date), blocking_countries ); for i in 1..30 { // 9-32 connections, 0-3 negative reports each day date += 1; bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::BridgeIps, date, 8 * (i % 3 + 2), ); bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::NegativeReports, date, i % 4, ); assert_eq!( blocked_in(&analyzer, &bridge_info, confidence, date), blocking_countries ); } // Data similar to previous days: // 24 connections, 2 negative reports date += 1; bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::BridgeIps, date, 24, ); bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::NegativeReports, date, 2, ); // Should not be blocked because we have similar data. assert_eq!( blocked_in(&analyzer, &bridge_info, confidence, date), blocking_countries ); // Data different from previous days: // 104 connections, 1 negative report date += 1; bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::BridgeIps, date, 104, ); bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::NegativeReports, date, 1, ); // This should not be blocked even though it's very different because // it's different in the good direction. assert_eq!( blocked_in(&analyzer, &bridge_info, confidence, date), blocking_countries ); // Data different from previous days: // 800 connections, 12 negative reports date += 1; bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::BridgeIps, date, 800, ); bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::NegativeReports, date, 12, ); blocking_countries.insert("ru".to_string()); // The censor artificially inflated bridge stats to prevent detection. // Ensure we still detect the censorship from negative reports. assert_eq!( blocked_in(&analyzer, &bridge_info, confidence, date), blocking_countries ); } { let mut date = get_date(); // New bridge info let mut bridge_info = BridgeInfo::new([0; 20], &String::default()); bridge_info .info_by_country .insert("ru".to_string(), BridgeCountryInfo::new(date)); let analyzer = analysis::NormalAnalyzer::new(5, 0.25); let confidence = 0.95; let mut blocking_countries = HashSet::::new(); // No data today assert_eq!( blocked_in(&analyzer, &bridge_info, confidence, date), blocking_countries ); for i in 1..30 { // 9-32 connections, 0-3 negative reports each day date += 1; bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::BridgeIps, date, 8 * (i % 3 + 2), ); bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::NegativeReports, date, i % 4, ); assert_eq!( blocked_in(&analyzer, &bridge_info, confidence, date), blocking_countries ); } // Data similar to previous days: // 24 connections, 2 negative reports date += 1; bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::BridgeIps, date, 24, ); bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::NegativeReports, date, 2, ); // Should not be blocked because we have similar data. assert_eq!( blocked_in(&analyzer, &bridge_info, confidence, date), blocking_countries ); // Data different from previous days: // 104 connections, 1 negative report date += 1; bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::BridgeIps, date, 104, ); bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::NegativeReports, date, 1, ); // This should not be blocked even though it's very different because // it's different in the good direction. assert_eq!( blocked_in(&analyzer, &bridge_info, confidence, date), blocking_countries ); // Data different from previous days: // 0 connections, 0 negative reports date += 1; bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::BridgeIps, date, 0, ); bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::NegativeReports, date, 0, ); blocking_countries.insert("ru".to_string()); // This should be blocked because it's different in the bad direction. assert_eq!( blocked_in(&analyzer, &bridge_info, confidence, date), blocking_countries ); } // Test stage 3 analysis { let mut date = get_date(); // New bridge info let mut bridge_info = BridgeInfo::new([0; 20], &String::default()); bridge_info .info_by_country .insert("ru".to_string(), BridgeCountryInfo::new(date)); let analyzer = analysis::NormalAnalyzer::new(5, 0.25); let confidence = 0.95; let mut blocking_countries = HashSet::::new(); // No data today assert_eq!( blocked_in(&analyzer, &bridge_info, confidence, date), blocking_countries ); for i in 1..30 { // 9-32 connections, 0-3 negative reports, 16-20 positive reports each day date += 1; bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::BridgeIps, date, 8 * (i % 3 + 2), ); bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::NegativeReports, date, i % 4, ); bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::PositiveReports, date, 16 + i % 5, ); assert_eq!( blocked_in(&analyzer, &bridge_info, confidence, date), blocking_countries ); } // Data similar to previous days: // 24 connections, 2 negative reports, 17 positive reports date += 1; bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::BridgeIps, date, 24, ); bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::NegativeReports, date, 2, ); bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::PositiveReports, date, 17, ); // Should not be blocked because we have similar data. assert_eq!( blocked_in(&analyzer, &bridge_info, confidence, date), blocking_countries ); // Data different from previous days: // 104 connections, 1 negative report, 100 positive reports date += 1; bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::BridgeIps, date, 104, ); bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::NegativeReports, date, 1, ); bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::PositiveReports, date, 100, ); // This should not be blocked even though it's very different because // it's different in the good direction. assert_eq!( blocked_in(&analyzer, &bridge_info, confidence, date), blocking_countries ); // Data different from previous days: // 40 connections, 12 negative reports, 40 positive reports date += 1; bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::BridgeIps, date, 40, ); bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::NegativeReports, date, 12, ); bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::PositiveReports, date, 40, ); blocking_countries.insert("ru".to_string()); // This should be blocked because it's different in the bad direction. assert_eq!( blocked_in(&analyzer, &bridge_info, confidence, date), blocking_countries ); } { let mut date = get_date(); // New bridge info let mut bridge_info = BridgeInfo::new([0; 20], &String::default()); bridge_info .info_by_country .insert("ru".to_string(), BridgeCountryInfo::new(date)); let analyzer = analysis::NormalAnalyzer::new(5, 0.25); let confidence = 0.95; let mut blocking_countries = HashSet::::new(); // No data today assert_eq!( blocked_in(&analyzer, &bridge_info, confidence, date), blocking_countries ); for i in 1..30 { // 9-32 connections, 0-3 negative reports, 16-20 positive reports each day date += 1; bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::BridgeIps, date, 8 * (i % 3 + 2), ); bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::NegativeReports, date, i % 4, ); bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::PositiveReports, date, 16 + i % 5, ); assert_eq!( blocked_in(&analyzer, &bridge_info, confidence, date), blocking_countries ); } // Data similar to previous days: // 24 connections, 2 negative reports, 17 positive reports date += 1; bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::BridgeIps, date, 24, ); bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::NegativeReports, date, 2, ); bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::PositiveReports, date, 17, ); // Should not be blocked because we have similar data. assert_eq!( blocked_in(&analyzer, &bridge_info, confidence, date), blocking_countries ); // Data different from previous days: // 104 connections, 1 negative report, 85 positive reports date += 1; bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::BridgeIps, date, 104, ); bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::NegativeReports, date, 1, ); bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::PositiveReports, date, 85, ); // This should not be blocked even though it's very different because // it's different in the good direction. assert_eq!( blocked_in(&analyzer, &bridge_info, confidence, date), blocking_countries ); // Data different from previous days: // 800 connections, 12 negative reports, 750 positive reports date += 1; bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::BridgeIps, date, 800, ); bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::NegativeReports, date, 12, ); bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::PositiveReports, date, 750, ); blocking_countries.insert("ru".to_string()); // The censor artificially inflated bridge stats to prevent detection. // Ensure we still detect the censorship from negative reports. assert_eq!( blocked_in(&analyzer, &bridge_info, confidence, date), blocking_countries ); } { let mut date = get_date(); // New bridge info let mut bridge_info = BridgeInfo::new([0; 20], &String::default()); bridge_info .info_by_country .insert("ru".to_string(), BridgeCountryInfo::new(date)); let analyzer = analysis::NormalAnalyzer::new(5, 0.25); let confidence = 0.95; let mut blocking_countries = HashSet::::new(); // No data today assert_eq!( blocked_in(&analyzer, &bridge_info, confidence, date), blocking_countries ); for i in 1..30 { // 9-32 connections, 0-3 negative reports, 16-20 positive reports each day date += 1; bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::BridgeIps, date, 8 * (i % 3 + 2), ); bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::NegativeReports, date, i % 4, ); bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::PositiveReports, date, 16 + i % 5, ); assert_eq!( blocked_in(&analyzer, &bridge_info, confidence, date), blocking_countries ); } // Data similar to previous days: // 24 connections, 2 negative reports, 17 positive reports date += 1; bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::BridgeIps, date, 24, ); bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::NegativeReports, date, 2, ); bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::PositiveReports, date, 17, ); // Should not be blocked because we have similar data. assert_eq!( blocked_in(&analyzer, &bridge_info, confidence, date), blocking_countries ); // Data different from previous days: // 104 connections, 1 negative report, 100 positive reports date += 1; bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::BridgeIps, date, 104, ); bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::NegativeReports, date, 1, ); bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::PositiveReports, date, 100, ); // This should not be blocked even though it's very different because // it's different in the good direction. assert_eq!( blocked_in(&analyzer, &bridge_info, confidence, date), blocking_countries ); // Data different from previous days: // 24 connections, 1 negative report, 1 positive report date += 1; bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::BridgeIps, date, 24, ); bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::NegativeReports, date, 1, ); bridge_info.info_by_country.get_mut("ru").unwrap().add_info( BridgeInfoType::PositiveReports, date, 1, ); blocking_countries.insert("ru".to_string()); // This should be blocked because it's different in the bad direction. assert_eq!( blocked_in(&analyzer, &bridge_info, confidence, date), blocking_countries ); } }