diff --git a/Cargo.toml b/Cargo.toml index 1e5894e..81575fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ anyhow = "1.0" array-bytes = "6.2.0" bincode = "1" clap = { version = "4.4.14", features = ["derive"] } +curve25519-dalek = { version = "4", default-features = false } hyper = { version = "0.14.28", features = ["full"] } lox_cli = { git = "https://git-crysp.uwaterloo.ca/vvecna/lox_cli.git", version = "0.1" } lox-library = { git = "https://gitlab.torproject.org/vecna/lox.git", version = "0.1.0" } diff --git a/src/bridge.rs b/src/bridge.rs index 01269db..4083750 100644 --- a/src/bridge.rs +++ b/src/bridge.rs @@ -86,4 +86,14 @@ impl Bridge { self.real_connections = 0; self.total_connections = 0; } + + // Has this bridge been distributed to a non-censor user? + pub fn has_been_distributed(&self) -> bool { + self.first_real_user > 0 + } + + // Does Troll Patrol think this bridge is blocked? + pub fn troll_patrol_blocked(&self) -> bool { + self.first_detected_blocked > 0 + } } diff --git a/src/censor.rs b/src/censor.rs index 95b5843..13a3f81 100644 --- a/src/censor.rs +++ b/src/censor.rs @@ -1,5 +1,6 @@ use crate::{bridge::Bridge, config::Config}; +use curve25519_dalek::scalar::Scalar; use lox_cli::{get_lox_pub, networking::Networking}; use lox_library::{cred::Lox, scalar_u32}; use rand::Rng; @@ -24,6 +25,12 @@ pub struct Censor { // credentials we have for this bridge). pub lox_credentials: HashMap<[u8; 20], (Lox, u32)>, + // Map of buckets to count of censor users with that bucket. Note + // that this is the count of users that have *ever* had that bucket, + // so this variable should NOT be used to count the overall number + // of censor agents. + pub agents: HashMap, + // If censor implements random blocking, this is the date when it // will start blocking all the bridges it knows. pub delay_date: u32, @@ -52,6 +59,7 @@ impl Censor { start_date, known_bridges: HashSet::<[u8; 20]>::new(), lox_credentials: HashMap::<[u8; 20], (Lox, u32)>::new(), + agents: HashMap::::new(), delay_date: delay_date, partial_blocking_percent: partial_blocking_percent, } @@ -116,6 +124,25 @@ impl Censor { } } + // Get the number of agents the censor has with a given bucket + pub fn num_agents(&self, bucket: &Scalar) -> u32 { + match self.agents.get(bucket) { + Some(v) => *v, + None => 0, + } + } + + // Add to the number of agents a censor has for a given bucket + pub fn add_agent(&mut self, bucket: &Scalar) { + self.agents.insert( + *bucket, + match self.agents.get(bucket) { + Some(v) => *v + 1, + None => 1, + }, + ); + } + // Censor sends a positive report for the given bridge. Returns true // if successful, false otherwise. pub async fn send_positive_report(&self, config: &Config, fingerprint: &[u8; 20]) -> bool { @@ -158,11 +185,8 @@ impl Censor { { let bridge = bridges.get_mut(fingerprint).unwrap(); - // A large number - let num_connections = 30000; - // Make a bunch of connections to the bridge - bridge.censor_flood(num_connections); + bridge.censor_flood(config.censor_max_connections); // If we have a lv3+ credential, submit a bunch of // positive reports @@ -176,11 +200,9 @@ impl Censor { // detection algorithm. In practice, the censor // should submit as many reports as possible. let num_prs = if config.one_positive_report_per_cred { - // *cred_count - min(*cred_count, 25) + min(*cred_count, config.censor_max_pr) } else { - //30000 - 25 + config.censor_max_pr }; for _ in 0..num_prs { self.send_positive_report(config, &bridge.fingerprint).await; diff --git a/src/config.rs b/src/config.rs index fde3224..21c6ca2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -10,6 +10,11 @@ pub struct Config { pub bootstrapping_period_duration: u32, // Define censor behavior pub censor_secrecy: censor::Secrecy, + // The maximum number of connections for the censor to make to each + // bridge + pub censor_max_connections: u32, + // The maximum number of positive reports for the censor to submit + pub censor_max_pr: u32, pub censor_speed: censor::Speed, pub censor_event_duration: u32, pub censor_totality: censor::Totality, diff --git a/src/main.rs b/src/main.rs index 9df89a0..c461e64 100644 --- a/src/main.rs +++ b/src/main.rs @@ -43,6 +43,8 @@ pub struct Config { pub tp_test_port: u16, pub bootstrapping_period_duration: u32, pub censor_secrecy: censor::Secrecy, + pub censor_max_connections: u32, + pub censor_max_pr: u32, pub censor_speed: censor::Speed, pub censor_event_duration: u32, pub censor_totality: censor::Totality, @@ -95,6 +97,8 @@ pub async fn main() { tp_net, bootstrapping_period_duration: config.bootstrapping_period_duration, censor_secrecy: config.censor_secrecy, + censor_max_connections: config.censor_max_connections, + censor_max_pr: config.censor_max_pr, censor_speed: config.censor_speed, censor_event_duration: config.censor_event_duration, censor_totality: config.censor_totality, @@ -207,13 +211,23 @@ pub async fn main() { users.shuffle(&mut rng); // Users do daily actions - for user in &mut users { - let invited_friends = user + let mut i = 0; + while i < users.len() { + let user = users.get_mut(i).unwrap(); + if let Ok((mut invited_friends, remove_user)) = user .daily_tasks(&sconfig, &mut bridges, &mut censor, &mut invites) - .await; + .await + { + // We remove censor users once they stop serving a purpose + if remove_user { + // This removes the user and replaces them with the + // last element of the vector, for efficiency. This + // is fine to do because the users act in a random + // order anyway. + users.swap_remove(i); + continue; + } - if invited_friends.is_ok() { - let mut invited_friends = invited_friends.unwrap(); if invited_friends.len() > 0 { new_users.append(&mut invited_friends); } @@ -228,6 +242,11 @@ pub async fn main() { count_users_cannot_connect += 1; } } + + // Iterate loop (note that we do not reach this if we remove + // a user, so we'll get the replacement user at that same + // index) + i += 1; } // Also count number of new users with/without connections diff --git a/src/user.rs b/src/user.rs index 667973a..7dcf2d4 100644 --- a/src/user.rs +++ b/src/user.rs @@ -46,6 +46,64 @@ pub fn give_bucket_to_censor( } } +// Check if bucket is blocked, according to the LA (regardless of +// whether the censor actually blocks the bridges) +pub fn bucket_blocked_lox(bucket: &[BridgeLine], bridges: &HashMap<[u8; 20], Bridge>) -> bool { + // Count number of non-default bridges (either 1 or 3) + let mut num_real_bridges = 0; + let mut num_blocked_bridges = 0; + + for bridge in bucket { + if *bridge != BridgeLine::default() { + match bridges.get(&bridge.get_hashed_fingerprint()) { + Some(b) => { + num_real_bridges += 1; + if b.troll_patrol_blocked() { + num_blocked_bridges += 1; + } + } + None => { + // Something went wrong, I guess + println!( + "Tried to check if bridge was blocked before it was added to simulation" + ); + } + } + } + } + + // Return true if open-entry bucket with a blocked bridge or + // invite-only bucket with 2+ blocked bridges + num_real_bridges == 1 && num_blocked_bridges == 1 + || num_real_bridges == 3 && num_blocked_bridges >= 2 +} + +// Check if bucket contains a bridge that has ever been distributed to a +// real user +pub fn bucket_has_been_distributed( + bucket: &[BridgeLine], + bridges: &HashMap<[u8; 20], Bridge>, +) -> bool { + for bridge in bucket { + if *bridge != BridgeLine::default() { + match bridges.get(&bridge.get_hashed_fingerprint()) { + Some(b) => { + if b.has_been_distributed() { + return true; + } + } + None => { + // Something went wrong, I guess + println!( + "Tried to check if bridge had been distributed before it was added to simulation" + ); + } + } + } + } + false +} + pub struct User { // Does this user cooperate with a censor? pub is_censor: bool, @@ -123,6 +181,30 @@ impl User { if is_censor { // Give bridges to censor give_bucket_to_censor(&bucket, bridges, censor); + + // Various conditions to avoid creating censor users if we don't need to + + // If the bucket is already marked blocked, don't create this user + if bucket_blocked_lox(&bucket, bridges) { + return Err(anyhow!("Bucket blocked, don't create new censor user")); + } + + // If the bucket has never been distributed to a real user, don't create this user + if !bucket_has_been_distributed(&bucket, bridges) { + return Err(anyhow!( + "Bucket never distributed to a real user, don't create new censor user" + )); + } + + // If we already have enough agents with this bucket, don't create this user + if censor.num_agents(&result.primary_cred.bucket) > config.censor_max_pr { + return Err(anyhow!( + "We already have enough censor users with this bucket" + )); + } + + // If we haven't returned yet, add this user to censor's list + censor.add_agent(&result.primary_cred.bucket); } else { // Test bridges to see if they work let (s, f) = result.test_bridges(&bucket, config, bridges, censor); @@ -229,6 +311,23 @@ impl User { if is_censor { give_bucket_to_censor(&bucket, bridges, censor); + + // Various conditions to avoid creating censor users unnecessarily + + // If the bucket is already marked blocked, don't create this user + if bucket_blocked_lox(&bucket, bridges) { + return Err(anyhow!("Bucket blocked, don't create new censor user")); + } + + // If we already have enough agents with this bucket, don't create this user + if censor.num_agents(&result.primary_cred.bucket) > config.censor_max_pr { + return Err(anyhow!( + "We already have enough censor users with this bucket" + )); + } + + // If we haven't returned yet, add this user to censor's list + censor.add_agent(&result.primary_cred.bucket); } else { let (s, f) = result.test_bridges(&bucket, config, bridges, censor); @@ -411,12 +510,17 @@ impl User { bridges: &mut HashMap<[u8; 20], Bridge>, censor: &mut Censor, invites: &mut Vec, - ) -> Result> { + ) -> Result<(Vec, bool)> { if self.is_censor { self.daily_tasks_censor(config, bridges, censor).await } else { - self.daily_tasks_non_censor(config, bridges, censor, invites) + match self + .daily_tasks_non_censor(config, bridges, censor, invites) .await + { + Ok(users) => Ok((users, false)), + Err(e) => Err(e), + } } } @@ -756,13 +860,15 @@ impl User { } // User cooperates with censor and performs daily tasks to try to - // learn more bridges. + // learn more bridges. Returns a vector of newly invited users + // and a boolean indicating whether this censor user should be + // removed from the global user set (for efficiency). pub async fn daily_tasks_censor( &mut self, config: &Config, bridges: &mut HashMap<[u8; 20], Bridge>, censor: &mut Censor, - ) -> Result> { + ) -> Result<(Vec, bool)> { // Download bucket to see if bridge is still reachable and if we // have any new bridges let (bucket, reachcred) = get_bucket(&config.la_net, &self.primary_cred).await?; @@ -770,14 +876,52 @@ impl User { // Make sure each bridge is in global bridges set and known by // censor - for bridgeline in bucket { - if bridgeline != BridgeLine::default() { - let fingerprint = bridgeline.get_hashed_fingerprint(); - if !bridges.contains_key(&fingerprint) { - let bridge = Bridge::from_bridge_line(&bridgeline); - bridges.insert(fingerprint, bridge); + give_bucket_to_censor(&bucket, bridges, censor); + + // If Lox has marked the bridge blocked, migrate if possible + if bucket_blocked_lox(&bucket, bridges) { + // If we can migrate, migrate + if level >= MIN_TRUST_LEVEL { + if let Ok(migcred) = check_blockage( + &config.la_net, + &self.primary_cred, + get_lox_pub(&config.la_pubkeys), + ) + .await + { + if let Ok(cred) = blockage_migration( + &config.la_net, + &self.primary_cred, + &migcred, + get_lox_pub(&config.la_pubkeys), + get_migration_pub(&config.la_pubkeys), + ) + .await + { + // Successfully migrated! + + // Update credential + self.primary_cred = cred; + + // You can't migrate to level 3 or 4, so the + // censor doesn't want this new credential + + // Download bucket to see if bridge is still + // reachable and if we have any new bridges + let (bucket, _reachcred) = + get_bucket(&config.la_net, &self.primary_cred).await?; + + // Give new bucket to censor + give_bucket_to_censor(&bucket, bridges, censor); + + // Add this user to censor's list + censor.add_agent(&self.primary_cred.bucket); + } } - censor.learn_bridge(&fingerprint); + } else { + // We can't migrate, and we can't level up or anything. + // Mark this user for deletion. + return Ok((Vec::::new(), true)); } } @@ -788,7 +932,7 @@ impl User { && eligible_for_level_up(&config.la_net, &self.primary_cred).await { let new_cred = if level == 0 { - trust_migration( + let nc = trust_migration( &config.la_net, &self.primary_cred, &trust_promotion( @@ -800,7 +944,13 @@ impl User { get_lox_pub(&config.la_pubkeys), get_migration_pub(&config.la_pubkeys), ) - .await? + .await?; + + // We now have a new bucket value, so add the user to censor's list + censor.add_agent(&nc.bucket); + + // New credential as new_cred + nc } else { level_up( &config.la_net, @@ -834,97 +984,8 @@ impl User { censor.give_lox_cred(&fingerprint, &self.primary_cred, false); } } - } else { - // LA has identified this bucket as blocked. This change - // will not be reverted, so either migrate or replace the - // primary credential with a new level 0 credential and work - // on gaining trust for that one. - - // Migrate if able - if level >= MIN_TRUST_LEVEL { - if let Ok(migcred) = check_blockage( - &config.la_net, - &self.primary_cred, - get_lox_pub(&config.la_pubkeys), - ) - .await - { - if let Ok(cred) = blockage_migration( - &config.la_net, - &self.primary_cred, - &migcred, - get_lox_pub(&config.la_pubkeys), - get_migration_pub(&config.la_pubkeys), - ) - .await - { - self.primary_cred = cred; - - // You can't migrate to level 3 or 4, so the - // censor doesn't want this new credential - - // Download bucket to see if bridge is still - // reachable and if we have any new bridges - let (bucket, _reachcred) = - get_bucket(&config.la_net, &self.primary_cred).await?; - - // Make sure each bridge is in global bridges - // set and known by censor - for bridgeline in bucket { - if bridgeline != BridgeLine::default() { - let fingerprint = bridgeline.get_hashed_fingerprint(); - if !bridges.contains_key(&fingerprint) { - let bridge = Bridge::from_bridge_line(&bridgeline); - bridges.insert(fingerprint, bridge); - } - censor.learn_bridge(&fingerprint); - } - } - } - } - - // Removing this case for efficiency. If the censor is in - // play, we just assume it wins the open-entry game and stop - // distributing open-entry invites altogether. - /* - } else { - // If unable to migrate, try to get a new open-entry - // credential and start over - let res = Self::get_new_credential(&config).await; - if res.is_ok() { - let (new_cred, bl) = res.unwrap(); - let fingerprint = bl.get_hashed_fingerprint(); - if !bridges.contains_key(&fingerprint) { - let bridge = Bridge::from_bridge_line(&bl); - bridges.insert(fingerprint, bridge); - } - censor.learn_bridge(&fingerprint); - // Censor doesn't want new_cred yet - self.primary_cred = new_cred; - } - */ - } } - // Also removing this case for efficiency. - /* - // Separately from primary credential, censor user requests a - // new secondary credential each day just to block the - // open-entry bridges. This is stored but not reused. - let res = Self::get_new_credential(&config).await; - if res.is_ok() { - let (_new_cred, bl) = res.unwrap(); - let fingerprint = bl.get_hashed_fingerprint(); - if !bridges.contains_key(&fingerprint) { - let bridge = Bridge::from_bridge_line(&bl); - bridges.insert(fingerprint, bridge); - } - censor.learn_bridge(&fingerprint); - // Censor doesn't want new_cred. User doesn't actually use - // secondary_cred, so don't store it. - } - */ - // Censor user invites as many censor friends as possible let invitations = scalar_u32(&self.primary_cred.invites_remaining).unwrap(); let mut new_friends = Vec::::new(); @@ -945,6 +1006,6 @@ impl User { } } } - Ok(new_friends) + Ok((new_friends, false)) } }