// User behavior in simulation use crate::{ bridge::Bridge, censor::{Censor, Secrecy::*, Totality::*}, config::Config, }; use anyhow::{anyhow, Result}; use lox_cli::{networking::*, *}; use lox_library::{ bridge_table::BridgeLine, cred::{Invitation, Lox}, proto::check_blockage::MIN_TRUST_LEVEL, scalar_u32, }; use rand::Rng; use std::collections::{HashMap, HashSet}; use troll_patrol::{ get_date, negative_report::NegativeReport, positive_report::PositiveReport, BridgeDistributor, }; use x25519_dalek::PublicKey; // Helper function to probabilistically return true or false pub fn event_happens(probability: f64) -> bool { let mut rng = rand::thread_rng(); let num: f64 = rng.gen_range(0.0..1.0); num < probability } // Make sure each bridge is in global bridges set and known by censor pub fn give_bucket_to_censor( bucket: &[BridgeLine], bridges: &mut HashMap<[u8; 20], Bridge>, censor: &mut 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); } } } // 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, // The user always has a primary credential. If this credential's bucket is // blocked, the user may replace it or temporarily hold two credentials // while waiting to migrate from the primary credential. pub primary_cred: Lox, secondary_cred: Option, // Does the user submit reports to Troll Patrol? submits_reports: bool, // How likely is this user to use bridges on a given day? This has // been converted to a global parameter (prob_user_connects), but we // still leave the user implementation for now in case we want to // switch back to it. prob_use_bridges: f64, // If the censor implements partial blocking, is the user blocked? in_censorship_range: bool, // Track date the user joined and whether they're able to connect pub join_date: u32, pub able_to_connect: bool, // Attempted to connect today pub attempted_connection: bool, } impl User { pub async fn new( config: &Config, is_censor: bool, bridges: &mut HashMap<[u8; 20], Bridge>, censor: &mut Censor, ) -> Result { if censor.is_active() && !is_censor { return Err(anyhow!("Censor is active; open invites disabled")); } let cred = Self::get_new_credential(&config).await?.0; // Decide how likely this user is to use bridges on a given day // and whether they submit reports let (prob_use_bridges, submits_reports) = if is_censor { (0.0, false) } else { // Use global value let prob_use_bridges = config.prob_user_connects; let submits_reports = event_happens(config.prob_user_submits_reports); (prob_use_bridges, submits_reports) }; let in_censorship_range = if config.censor_totality == Partial { event_happens(config.censor_partial_blocking_percent) } else { true }; let mut result = Self { is_censor, primary_cred: cred, secondary_cred: None, submits_reports: submits_reports, prob_use_bridges: prob_use_bridges, in_censorship_range, join_date: get_date(), able_to_connect: false, attempted_connection: true, }; // Immediately download and try to use our bridges, or report them to the censor. let (bucket, _reachcred) = get_bucket(&config.la_net, &result.primary_cred).await?; 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); // Our bridge worked! Yay! if !s.is_empty() { result.able_to_connect = true; } // Report any failures if we submit reports if submits_reports { for bl in f { let mut negative_reports = Vec::::new(); negative_reports.push(NegativeReport::from_bridgeline( bl, config.country.to_string(), BridgeDistributor::Lox, )); Self::send_negative_reports(&config, negative_reports).await?; } } } // Return our new user Ok(result) } // Get an invite if able pub async fn get_invite( &mut self, config: &Config, bridges: &mut HashMap<[u8; 20], Bridge>, censor: &mut Censor, ) -> Result { let etable = get_reachability_credential(&config.la_net).await?; let (new_cred, invite) = issue_invite( &config.la_net, &self.primary_cred, &etable, get_lox_pub(&config.la_pubkeys), get_reachability_pub(&config.la_pubkeys), get_invitation_pub(&config.la_pubkeys), ) .await?; self.primary_cred = new_cred; // If user cooperates with censor, give bridges and credential // now. Otherwise, wait until the user actually wants to use // Tor. if self.is_censor { let (bucket, _reachcred) = get_bucket(&config.la_net, &self.primary_cred).await?; give_bucket_to_censor(&bucket, bridges, censor); } Ok(invite) } pub async fn from_invite( invite: Invitation, config: &Config, is_censor: bool, bridges: &mut HashMap<[u8; 20], Bridge>, censor: &mut Censor, ) -> Result { let cred = redeem_invite( &config.la_net, &invite, get_lox_pub(&config.la_pubkeys), get_invitation_pub(&config.la_pubkeys), ) .await? .0; // Decide how likely this user is to use bridges on a given day // and whether they submit reports let (prob_use_bridges, submits_reports) = if is_censor { (0.0, false) } else { // Use global value let prob_use_bridges = config.prob_user_connects; let submits_reports = event_happens(config.prob_user_submits_reports); (prob_use_bridges, submits_reports) }; let in_censorship_range = if config.censor_totality == Partial { event_happens(config.censor_partial_blocking_percent) } else { true }; let mut result = Self { is_censor, primary_cred: cred, secondary_cred: None, submits_reports: submits_reports, prob_use_bridges: prob_use_bridges, in_censorship_range, join_date: get_date(), able_to_connect: false, attempted_connection: true, }; let (bucket, _reachcred) = get_bucket(&config.la_net, &result.primary_cred).await?; 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); // Our bridge worked! Yay! if !s.is_empty() { result.able_to_connect = true; } // Report any failures if we submit reports if submits_reports { for bl in f { let mut negative_reports = Vec::::new(); negative_reports.push(NegativeReport::from_bridgeline( bl, config.country.to_string(), BridgeDistributor::Lox, )); Self::send_negative_reports(&config, negative_reports).await?; } } } Ok(result) } // Attempt to "connect" to the bridge, returns true if successful. // Note that this does not involve making a real connection to a // real bridge. pub fn connect( in_censorship_range: bool, config: &Config, bridge: &mut Bridge, censor: &Censor, ) -> bool { if censor.blocks_bridge(config, &bridge.fingerprint) { if config.censor_totality == Full || config.censor_totality == Partial && in_censorship_range { // If censor tries to hide its censorship, record a // false connection if config.censor_secrecy == Hiding { bridge.connect_total(); } // Return false because the connection failed return false; } else if config.censor_totality == Throttling { // With some probability, the user connects but gives up // because there is too much interference. In this case, // a real connection occurs, but we treat it like a // false connection from the censor. if event_happens(config.prob_user_treats_throttling_as_blocking) { bridge.connect_total(); // Return false because there was interference // detected in the connection return false; } } } // Connection may randomly fail, without censor intervention let mut connection_fails = true; // The user retries some number of times for _ in 0..=config.num_connection_retries { if !event_happens(config.prob_connection_fails) { connection_fails = false; break; } } if connection_fails { return false; } // If we haven't returned yet, the connection succeeded bridge.connect_real(); true } // Given a Lox credential, download its bucket, test its bridges, and // return sets of working and non-working bridges pub fn test_bridges( &mut self, bucket: &[BridgeLine], config: &Config, bridges: &mut HashMap<[u8; 20], Bridge>, censor: &Censor, ) -> (HashSet, HashSet) { let mut failed = HashSet::::new(); let mut succeeded = HashSet::::new(); for bridgeline in bucket { if *bridgeline != BridgeLine::default() { let fingerprint = bridgeline.get_hashed_fingerprint(); // Make sure bridge is in the global set if !bridges.contains_key(&fingerprint) { let bridge = Bridge::from_bridge_line(&bridgeline); bridges.insert(fingerprint, bridge); } // If this is the first time the bridge has been // distributed to a real user, store that info let bridge = bridges.get_mut(&fingerprint).unwrap(); if bridge.first_real_user == 0 { bridge.first_real_user = get_date(); } // Attempt to connect to the bridge if Self::connect( self.in_censorship_range, config, bridges .get_mut(&bridgeline.get_hashed_fingerprint()) .unwrap(), censor, ) { self.able_to_connect = true; succeeded.insert(*bridgeline); } else { failed.insert(*bridgeline); } } } (succeeded, failed) } pub async fn get_new_credential(config: &Config) -> Result<(Lox, BridgeLine)> { get_lox_credential( &config.la_net, &get_open_invitation(&config.la_net).await?, get_lox_pub(&config.la_pubkeys), ) .await } pub async fn send_negative_reports( config: &Config, reports: Vec, ) -> Result<()> { let date = get_date(); let pubkey = match serde_json::from_slice::>( &config .tp_net .request("/nrkey".to_string(), serde_json::to_string(&date)?.into()) .await?, )? { Some(v) => v, None => return Err(anyhow!("No available negative report encryption key")), }; for report in reports { config .tp_net .request( "/negativereport".to_string(), bincode::serialize(&report.encrypt(&pubkey))?, ) .await?; } Ok(()) } pub async fn send_positive_reports( config: &Config, reports: Vec, ) -> Result<()> { for report in reports { config .tp_net .request("/positivereport".to_string(), report.to_json().into_bytes()) .await?; } Ok(()) } pub async fn daily_tasks( &mut self, config: &Config, bridges: &mut HashMap<[u8; 20], Bridge>, censor: &mut Censor, invites: &mut Vec, ) -> Result<(Vec, bool)> { if self.is_censor { self.daily_tasks_censor(config, bridges, censor).await } else { match self .daily_tasks_non_censor(config, bridges, censor, invites) .await { Ok(users) => Ok((users, false)), Err(e) => Err(e), } } } // User performs daily connection attempts, etc. and returns a // vector of newly invited friends. // TODO: The map of bridges and the censor should be Arc> // or something so we can parallelize this. pub async fn daily_tasks_non_censor( &mut self, config: &Config, bridges: &mut HashMap<[u8; 20], Bridge>, censor: &mut Censor, invites: &mut Vec, ) -> Result> { // Probabilistically decide if the user should use bridges today if event_happens(self.prob_use_bridges) { self.attempted_connection = true; // Start with the assumption that we can't connect, change // only if we can self.able_to_connect = false; // Download bucket to see if bridge is still reachable. (We // assume that this step can be done even if the user can't // actually talk to the LA.) let (bucket, reachcred) = get_bucket(&config.la_net, &self.primary_cred).await?; let level = match scalar_u32(&self.primary_cred.trust_level) { Some(v) => v, None => return Err(anyhow!("Failed to get trust level from credential")), }; // Can we level up the main credential? let can_level_up = level == 0 && eligible_for_trust_promotion(&config.la_net, &self.primary_cred).await || reachcred.is_some() && level > 0 && eligible_for_level_up(&config.la_net, &self.primary_cred).await; // Can we migrate the main credential? let can_migrate = reachcred.is_none() && level >= MIN_TRUST_LEVEL; // Can we level up the secondary credential? let mut second_level_up = false; // Track which connections succeeded and which failed. We use sets // instead of vectors for deduplication in case we test a bridge // multiple times. let mut succeeded = HashSet::::new(); let mut failed = HashSet::::new(); let (s, f) = self.test_bridges(&bucket, config, bridges, censor); // Add working bridges to succeeded set for b in s { succeeded.insert(b); } // Add non-working bridges to failed set for b in f { failed.insert(b); } // If we were not able to connect to any bridges but also // cannot migrate, ask for a new invitation if !self.able_to_connect && !can_migrate { while let Some(invite) = invites.pop() { match redeem_invite( &config.la_net, &invite, get_lox_pub(&config.la_pubkeys), get_invitation_pub(&config.la_pubkeys), ) .await { Ok((cred, bucket)) => { self.primary_cred = cred; // Test our bridges let (s, f) = self.test_bridges(&bucket, config, bridges, censor); for b in s { succeeded.insert(b); } for b in f { failed.insert(b); } // We got a credential. Stop now. break; } Err(e) => { println!("Failed to redeem invite. Error: {}", e); } } } } // If we were still not able to connect to any bridges, get // a second credential let second_cred = if !self.able_to_connect { if self.secondary_cred.is_some() { std::mem::replace(&mut self.secondary_cred, None) } else { // If the censor is in play, we cannot get open-entry invites if censor.is_active() { None } else { // Get new credential match Self::get_new_credential(&config).await { Ok((cred, _bl)) => Some(cred), Err(e) => { eprintln!("Failed to get new Lox credential. Error: {}", e); None } } } } } else { // If we're able to connect with the primary credential, don't // keep a secondary one. None }; if second_cred.is_some() { let second_cred = second_cred.as_ref().unwrap(); // Test our bridges let (second_bucket, second_reachcred) = get_bucket(&config.la_net, &second_cred).await?; let (s, f) = self.test_bridges(&second_bucket, config, bridges, censor); // Add each working bridge to succeeded set for b in s { succeeded.insert(b); } // Add each non-working bridge to failed set for b in f { failed.insert(b); } // If we're able to connect, see if we can level up if self.able_to_connect { if second_reachcred.is_some() && eligible_for_trust_promotion(&config.la_net, &second_cred).await { second_level_up = true; } } } let mut negative_reports = Vec::::new(); let mut positive_reports = Vec::::new(); if self.submits_reports { for bridgeline in &failed { negative_reports.push(NegativeReport::from_bridgeline( *bridgeline, config.country.to_string(), BridgeDistributor::Lox, )); } if level >= 3 { for bridgeline in &succeeded { // If we haven't received a positive report yet, // add a record about it with today's date let fingerprint = bridgeline.get_hashed_fingerprint(); let bridge = bridges.get_mut(&fingerprint).unwrap(); if bridge.first_positive_report == 0 { bridge.first_positive_report = get_date(); } if let Ok(pr) = PositiveReport::from_lox_credential( fingerprint, None, &self.primary_cred, get_lox_pub(&config.la_pubkeys), config.country.to_string(), ) { positive_reports.push(pr) } } } } // We might restrict these steps to succeeded.len() > 0, but // we do assume the user can contact the LA somehow, so // let's just allow it. if can_level_up { // Trust migration from level 0 to level 1 let cred = if level == 0 { trust_migration( &config.la_net, &self.primary_cred, &trust_promotion( &config.la_net, &self.primary_cred, get_lox_pub(&config.la_pubkeys), ) .await?, get_lox_pub(&config.la_pubkeys), get_migration_pub(&config.la_pubkeys), ) .await? } else { // Level up from level 1+ level_up( &config.la_net, &self.primary_cred, &reachcred.unwrap(), // must be Some get_lox_pub(&config.la_pubkeys), get_reachability_pub(&config.la_pubkeys), ) .await? }; self.primary_cred = cred; self.secondary_cred = None; } else if can_migrate { // If we can't level up, try to migrate let cred = blockage_migration( &config.la_net, &self.primary_cred, &check_blockage( &config.la_net, &self.primary_cred, get_lox_pub(&config.la_pubkeys), ) .await?, get_lox_pub(&config.la_pubkeys), get_migration_pub(&config.la_pubkeys), ) .await?; self.primary_cred = cred; self.secondary_cred = None; } else if second_level_up { // If we can't migrate, try to level up our secondary // credential let second_cred = second_cred.as_ref().unwrap(); let cred = trust_migration( &config.la_net, &second_cred, &trust_promotion( &config.la_net, &second_cred, get_lox_pub(&config.la_pubkeys), ) .await?, get_lox_pub(&config.la_pubkeys), get_migration_pub(&config.la_pubkeys), ) .await?; self.primary_cred = cred; self.secondary_cred = None; } else if second_cred.is_some() { // Couldn't connect with primary credential if self.able_to_connect { // Keep the second credential only if it's useful self.secondary_cred = second_cred; } } if !negative_reports.is_empty() { Self::send_negative_reports(&config, negative_reports).await?; } if !positive_reports.is_empty() { Self::send_positive_reports(&config, positive_reports).await?; } // Invite friends if applicable let invitations = match scalar_u32(&self.primary_cred.invites_remaining) { Some(v) => v, None => 0, // This is probably an error case that should not happen }; // It's just more convenient in the code to do this this way. // Invite censors directly as new users. If the invited user is // not a censor, instead add the invitation to a global list. let mut new_censors = Vec::::new(); // Scale the probability of inviting a censor, based on the // user's own trust level. We assume that users with // more-trusted credentials are less likely to invite // censors because they have more to lose. let level_scale = match scalar_u32(&self.primary_cred.trust_level) { // These numbers are fairly arbitrary. Some(4) => 0.25, Some(3) => 0.5, Some(2) => 1.0, _ => 1.0, // should not have invitations }; for _ in 0..invitations { if event_happens(config.prob_user_invites_friend) { match self.get_invite(config, bridges, censor).await { Ok(invite) => { // With some probability, the user is convinced to // invite a censor. We assume users with higher // trust levels will be more cautious with // invitations because they have more to lose. if censor.is_active() && event_happens(config.prob_censor_gets_invite * level_scale) { // Invite friend (who might be a censor) match Self::from_invite(invite, config, true, bridges, censor).await { Ok(friend) => { // You really shouldn't push your friends, // especially new ones whose boundaries you // might not know well. new_censors.push(friend); } Err(e) => { println!("{}", e); } } } else { invites.push(invite); } } Err(e) => { println!("{}", e); } } } } Ok(new_censors) } else { // If we didn't try to connect, indicate so. This is to // prevent users from being counted as able/unable to // connect when they just haven't tried. self.attempted_connection = false; Ok(Vec::::new()) } } // User cooperates with censor and performs daily tasks to try to // 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<(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?; let level = scalar_u32(&self.primary_cred.trust_level).unwrap(); // Make sure each bridge is in global bridges set and known by // censor 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); } } } else { // We can't migrate, and we can't level up or anything. // Mark this user for deletion. return Ok((Vec::::new(), true)); } } // Censor user tries to level up their primary credential if level == 0 && eligible_for_trust_promotion(&config.la_net, &self.primary_cred).await || reachcred.is_some() && level > 0 && eligible_for_level_up(&config.la_net, &self.primary_cred).await { let new_cred = if level == 0 { let nc = trust_migration( &config.la_net, &self.primary_cred, &trust_promotion( &config.la_net, &self.primary_cred, get_lox_pub(&config.la_pubkeys), ) .await?, get_lox_pub(&config.la_pubkeys), get_migration_pub(&config.la_pubkeys), ) .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, &self.primary_cred, &reachcred.unwrap(), // must be Some get_lox_pub(&config.la_pubkeys), get_reachability_pub(&config.la_pubkeys), ) .await? }; self.primary_cred = new_cred; 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 bl in bucket { 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); if level == 2 { // level up to 3 // Give censor an additional credential censor.give_lox_cred(&fingerprint, &self.primary_cred, true); } else if level > 2 { // level up to 4 // Replace censor's credential with newer one, // but don't add to count of censor's // credentials censor.give_lox_cred(&fingerprint, &self.primary_cred, false); } } } // 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(); for _ in 0..invitations { match self.get_invite(config, bridges, censor).await { Ok(invite) => { match Self::from_invite(invite, &config, true, bridges, censor).await { Ok(friend) => { new_friends.push(friend); } Err(e) => { println!("{}", e); } } } Err(e) => { println!("{}", e); } } } Ok((new_friends, false)) } }