Move simulation code from Troll Patrol to its own repo

This commit is contained in:
Vecna 2024-06-18 07:23:05 -04:00
commit 19fc667b36
10 changed files with 1655 additions and 0 deletions

22
Cargo.toml Normal file
View File

@ -0,0 +1,22 @@
[package]
name = "lox-simulation"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0"
array-bytes = "6.2.0"
bincode = "1"
clap = { version = "4.4.14", features = ["derive"] }
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" }
memory-stats = "1.0.0"
rand = "0.8"
serde = "1.0.197"
serde_json = "1.0"
tokio = { version = "1", features = ["full"] }
troll-patrol = { git = "https://git-crysp.uwaterloo.ca/vvecna/troll-patrol.git", version = "0.1.0", features = ["simulation"] }
x25519-dalek = { version = "2", features = ["serde"] }

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Ivy Vecna
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# Lox Simulation
This is a simulation for evaluating Lox and Troll Patrol.

84
src/bridge.rs Normal file
View File

@ -0,0 +1,84 @@
use lox_library::bridge_table::BridgeLine;
use std::collections::BTreeMap;
use troll_patrol::{extra_info::ExtraInfo, get_date};
// The Bridge struct only tracks data for today
pub struct Bridge {
pub fingerprint: [u8; 20],
// The following four values are Julian dates used to track
// accuracy of the Troll Patrol system. A value of 0 is used to
// indicate this event *has not happened yet*. Note that 0 is not a
// date we will ever encounter.
// Date the bridge was first distributed to a user, i.e., the date
// we created this Bridge object
pub first_distributed: u32,
// First date a censor blocked this bridge
pub first_blocked: u32,
// First date Troll Patrol detected that this bridge was blocked
// (whether or not it was correct)
pub first_detected_blocked: u32,
// First date Troll Patrol received a positive report for this
// bridge (for identifying stage three)
pub first_positive_report: u32,
pub real_connections: u32,
pub total_connections: u32,
}
impl Bridge {
pub fn new(fingerprint: &[u8; 20]) -> Self {
Self {
fingerprint: *fingerprint,
first_distributed: get_date(),
first_blocked: 0,
first_detected_blocked: 0,
first_positive_report: 0,
real_connections: 0,
total_connections: 0,
}
}
pub fn from_bridge_line(bridgeline: &BridgeLine) -> Self {
Self::new(&bridgeline.get_hashed_fingerprint())
}
pub fn connect_real(&mut self) {
self.real_connections += 1;
self.total_connections += 1;
}
pub fn connect_total(&mut self) {
self.total_connections += 1;
}
// Let the censor simulate a bunch of connections at once
pub fn censor_flood(&mut self, num_connections: u32) {
self.total_connections += num_connections;
}
// Generate an extra-info report for today
pub fn gen_extra_info(&self, country: &str) -> ExtraInfo {
let mut bridge_ips = BTreeMap::<String, u32>::new();
// Round up to a multiple of 8
let rounded_connection_count =
self.total_connections + 7 - (self.total_connections + 7) % 8;
//let rounded_connection_count = (self.total_connections + 7) / 8 * 8;
bridge_ips.insert(country.to_string(), rounded_connection_count);
ExtraInfo {
nickname: String::from("simulation-bridge"),
fingerprint: self.fingerprint,
date: get_date(),
bridge_ips,
}
}
pub fn reset_for_tomorrow(&mut self) {
self.real_connections = 0;
self.total_connections = 0;
}
}

256
src/censor.rs Normal file
View File

@ -0,0 +1,256 @@
use crate::{bridge::Bridge, config::Config};
use lox_cli::{get_lox_pub, networking::Networking};
use lox_library::{cred::Lox, scalar_u32};
use rand::Rng;
use serde::Deserialize;
use std::{
cmp::min,
collections::{HashMap, HashSet},
};
use troll_patrol::{get_date, positive_report::PositiveReport};
pub struct Censor {
// If we have a bootstrapping period, the censor does not begin
// until this date.
pub start_date: u32,
pub known_bridges: HashSet<[u8; 20]>,
// We don't actually implement the technical restriction to prevent
// one Lox credential from being used to submit many reports, so we
// just implement this as a map of bridge fingerprint to (most
// recent Lox credential for this bridge, count of unique level 3+
// credentials we have for this bridge).
pub lox_credentials: HashMap<[u8; 20], (Lox, u32)>,
// If censor implements random blocking, this is the date when it
// will start blocking all the bridges it knows.
pub delay_date: u32,
// If censor implements partial blocking, what percent of
// connections are blocked?
pub partial_blocking_percent: f64,
}
impl Censor {
pub fn new(config: &Config) -> Self {
let start_date = get_date() + config.bootstrapping_period_duration;
let mut rng = rand::thread_rng();
let delay_date = if config.censor_speed == Speed::Random {
let num: u32 = rng.gen_range(1..365);
start_date + num
} else {
0
};
let partial_blocking_percent = if config.censor_totality == Totality::Partial {
config.censor_partial_blocking_percent
} else {
1.0
};
Censor {
start_date,
known_bridges: HashSet::<[u8; 20]>::new(),
lox_credentials: HashMap::<[u8; 20], (Lox, u32)>::new(),
delay_date: delay_date,
partial_blocking_percent: partial_blocking_percent,
}
}
pub fn knows_bridge(&self, fingerprint: &[u8; 20]) -> bool {
self.known_bridges.contains(fingerprint)
}
pub fn blocks_bridge(&self, config: &Config, fingerprint: &[u8; 20]) -> bool {
self.knows_bridge(fingerprint)
&& (config.censor_speed == Speed::Fast
|| config.censor_speed == Speed::Lox && self.has_lox_cred(fingerprint)
|| config.censor_speed == Speed::Random && self.delay_date <= get_date())
}
pub fn learn_bridge(&mut self, fingerprint: &[u8; 20]) {
self.known_bridges.insert(*fingerprint);
}
pub fn has_lox_cred(&self, fingerprint: &[u8; 20]) -> bool {
self.lox_credentials.contains_key(fingerprint)
&& self.lox_credentials.get(fingerprint).unwrap().1 > 0
}
pub fn give_lox_cred(&mut self, fingerprint: &[u8; 20], cred: &Lox) {
// We only need one level 3+ credential per bridge. (This will
// change if we restrict positive reports to one per bridge per
// credential.)
if scalar_u32(&cred.trust_level).unwrap() >= 3 {
// We want to clone the credential, but that's not allowed,
// so we're going to serialize it and then deserialize it.
let cloned_cred = bincode::deserialize(&bincode::serialize(&cred).unwrap()).unwrap();
// Insert the new credential and add to the count of unique
// credentials we have. We assume that a duplicate
// credential will never be given. If we don't want to make
// this assumption, we could change the count from a u32 to
// a set of credential IDs and get the count as its length.
let count = match self.lox_credentials.get(fingerprint) {
Some((_cred, count)) => *count,
None => 0,
};
self.lox_credentials
.insert(*fingerprint, (cloned_cred, count + 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 {
// If we don't have an appropriate Lox credential, we can't send
// a report. Return false.
if !self.has_lox_cred(fingerprint) {
return false;
}
let (cred, _) = &self.lox_credentials.get(fingerprint).unwrap();
let pr = PositiveReport::from_lox_credential(
*fingerprint,
None,
cred,
get_lox_pub(&config.la_pubkeys),
config.country.clone(),
)
.unwrap();
if config
.tp_net
.request("/positivereport".to_string(), pr.to_json().into_bytes())
.await
.is_err()
{
// failed to send positive report
return false;
}
true
}
// Make a bunch of connections and submit positive reports if possible
async fn flood(&self, config: &Config, bridges: &mut HashMap<[u8; 20], Bridge>) {
// Only do this if Flooding censor
if config.censor_secrecy == Secrecy::Flooding {
for fingerprint in &self.known_bridges {
// Only do this if we're blocking the bridge
if config.censor_speed == Speed::Fast
|| config.censor_speed == Speed::Lox && self.has_lox_cred(fingerprint)
|| config.censor_speed == Speed::Random && self.delay_date <= get_date()
{
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);
// If we have a lv3+ credential, submit a bunch of
// positive reports
if self.has_lox_cred(fingerprint) {
let (_cred, cred_count) =
&self.lox_credentials.get(&bridge.fingerprint).unwrap();
let num_prs = if config.one_positive_report_per_cred {
*cred_count
} else {
30000
};
for _ in 0..num_prs {
self.send_positive_report(config, &bridge.fingerprint).await;
}
}
}
}
}
}
// Send one positive report per connection we blocked
async fn send_positive_reports(
&self,
config: &Config,
bridges: &mut HashMap<[u8; 20], Bridge>,
) {
// Only do this if Hiding censor. Flooding censors should use
// flood() instead.
if config.censor_secrecy == Secrecy::Hiding {
for fingerprint in &self.known_bridges {
// Only do this if we're blocking the bridge
if self.blocks_bridge(config, fingerprint) && self.has_lox_cred(fingerprint) {
let bridge = bridges.get_mut(fingerprint).unwrap();
// We may be restricted to one positive report per
// credential
let num_reports_to_send = if config.one_positive_report_per_cred {
min(
bridge.total_connections - bridge.real_connections,
self.lox_credentials.get(fingerprint).unwrap().1,
)
} else {
bridge.total_connections - bridge.real_connections
};
for _ in 0..num_reports_to_send {
self.send_positive_report(config, fingerprint).await;
}
}
}
}
}
fn recompute_delay(&mut self, config: &Config) {
// Only do this if Random censor
if config.censor_speed == Speed::Random
&& self.delay_date + config.censor_event_duration <= get_date()
{
// Compute new delay date
self.delay_date = {
let mut rng = rand::thread_rng();
let num: u32 = rng.gen_range(1..365);
get_date() + num
}
}
}
pub async fn end_of_day_tasks(
&mut self,
config: &Config,
bridges: &mut HashMap<[u8; 20], Bridge>,
) {
if get_date() >= self.start_date {
if config.censor_secrecy == Secrecy::Flooding
&& !(config.censor_speed == Speed::Random && self.delay_date <= get_date())
{
self.flood(config, bridges).await;
} else if config.censor_secrecy == Secrecy::Hiding
&& !(config.censor_speed == Speed::Random && self.delay_date <= get_date())
{
self.send_positive_reports(config, bridges).await;
}
self.recompute_delay(config);
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
pub enum Speed {
Fast,
Lox,
Random,
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
pub enum Secrecy {
Overt,
Hiding,
Flooding,
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
pub enum Totality {
Full,
Partial,
Throttling,
}

33
src/config.rs Normal file
View File

@ -0,0 +1,33 @@
use crate::censor;
use lox_cli::networking::*;
use lox_library::IssuerPubKey;
pub struct Config {
pub la_pubkeys: Vec<IssuerPubKey>,
pub la_net: HyperNet,
pub tp_net: HyperNet,
pub bootstrapping_period_duration: u32,
// Define censor behavior
pub censor_secrecy: censor::Secrecy,
pub censor_speed: censor::Speed,
pub censor_event_duration: u32,
pub censor_totality: censor::Totality,
pub censor_partial_blocking_percent: f64,
// We model only one country at a time because Lox assumes censors
// share information with each other.
pub country: String,
pub one_positive_report_per_cred: bool,
// Probability that a censor-cooperating user can convince an honest
// user to give them an invite.
pub prob_censor_gets_invite: f64,
// Probability that a connection randomly fails, even though censor
// does not block the bridge
pub prob_connection_fails: f64,
// If the connection fails, retry how many times?
pub num_connection_retries: u32,
pub prob_user_invites_friend: f64,
pub prob_user_is_censor: f64,
pub prob_user_submits_reports: f64,
pub prob_user_treats_throttling_as_blocking: f64,
}

150
src/extra_infos_server.rs Normal file
View File

@ -0,0 +1,150 @@
use hyper::{
body::{self, Bytes},
header::HeaderValue,
server::conn::AddrStream,
service::{make_service_fn, service_fn},
Body, Method, Request, Response, Server,
};
use serde_json::json;
use std::{collections::HashSet, convert::Infallible, net::SocketAddr, time::Duration};
use tokio::{
spawn,
sync::{mpsc, oneshot},
time::sleep,
};
use troll_patrol::extra_info::ExtraInfo;
async fn serve_extra_infos(
extra_infos_pages: &mut Vec<String>,
req: Request<Body>,
) -> Result<Response<Body>, Infallible> {
match req.method() {
&Method::OPTIONS => Ok(Response::builder()
.header("Access-Control-Allow-Origin", HeaderValue::from_static("*"))
.header("Access-Control-Allow-Headers", "accept, content-type")
.header("Access-Control-Allow-Methods", "POST")
.status(200)
.body(Body::from("Allow POST"))
.unwrap()),
_ => match req.uri().path() {
"/" => Ok::<_, Infallible>(serve_index(&extra_infos_pages)),
"/add" => Ok::<_, Infallible>({
let bytes = body::to_bytes(req.into_body()).await.unwrap();
add_extra_infos(extra_infos_pages, bytes)
}),
path => Ok::<_, Infallible>({
// Serve the requested file
serve_extra_infos_file(&extra_infos_pages, path)
}),
},
}
}
pub async fn server() {
let (context_tx, context_rx) = mpsc::channel(32);
let request_tx = context_tx.clone();
spawn(async move { create_context_manager(context_rx).await });
let addr = SocketAddr::from(([127, 0, 0, 1], 8004));
let make_svc = make_service_fn(move |_conn: &AddrStream| {
let request_tx = request_tx.clone();
let service = service_fn(move |req| {
let request_tx = request_tx.clone();
let (response_tx, response_rx) = oneshot::channel();
let cmd = Command::Request {
req,
sender: response_tx,
};
async move {
request_tx.send(cmd).await.unwrap();
response_rx.await.unwrap()
}
});
async move { Ok::<_, Infallible>(service) }
});
let server = Server::bind(&addr).serve(make_svc);
println!("Listening on localhost:8004");
if let Err(e) = server.await {
eprintln!("server error: {}", e);
}
}
async fn create_context_manager(context_rx: mpsc::Receiver<Command>) {
tokio::select! {
create_context = context_manager(context_rx) => create_context,
}
}
async fn context_manager(mut context_rx: mpsc::Receiver<Command>) {
let mut extra_infos_pages = Vec::<String>::new();
while let Some(cmd) = context_rx.recv().await {
use Command::*;
match cmd {
Request { req, sender } => {
let response = serve_extra_infos(&mut extra_infos_pages, req).await;
if let Err(e) = sender.send(response) {
eprintln!("Server Response Error: {:?}", e);
}
sleep(Duration::from_millis(1)).await;
}
}
}
}
#[derive(Debug)]
enum Command {
Request {
req: Request<Body>,
sender: oneshot::Sender<Result<Response<Body>, Infallible>>,
},
}
fn add_extra_infos(extra_infos_pages: &mut Vec<String>, request: Bytes) -> Response<Body> {
let extra_infos: HashSet<ExtraInfo> = match serde_json::from_slice(&request) {
Ok(req) => req,
Err(e) => {
let response = json!({"error": e.to_string()});
let val = serde_json::to_string(&response).unwrap();
return prepare_header(val);
}
};
let mut extra_infos_file = String::new();
for extra_info in extra_infos {
extra_infos_file.push_str(extra_info.to_string().as_str());
}
if extra_infos_file.len() > 0 {
extra_infos_pages.push(extra_infos_file);
}
prepare_header("OK".to_string())
}
fn serve_index(extra_infos_pages: &Vec<String>) -> Response<Body> {
let mut body_str = String::new();
for i in 0..extra_infos_pages.len() {
body_str
.push_str(format!("<a href=\"{}-extra-infos\">{}-extra-infos</a>\n", i, i).as_str());
}
prepare_header(body_str)
}
fn serve_extra_infos_file(extra_infos_pages: &Vec<String>, path: &str) -> Response<Body> {
if path.ends_with("-extra-infos") {
if let Ok(index) = &path[1..(path.len() - "-extra-infos".len())].parse::<usize>() {
if extra_infos_pages.len() > *index {
return prepare_header(extra_infos_pages[*index].clone());
}
}
}
prepare_header("Not a valid file".to_string())
}
// Prepare HTTP Response for successful Server Request
fn prepare_header(response: String) -> Response<Body> {
let mut resp = Response::new(Body::from(response));
resp.headers_mut()
.insert("Access-Control-Allow-Origin", HeaderValue::from_static("*"));
resp
}

5
src/lib.rs Normal file
View File

@ -0,0 +1,5 @@
pub mod bridge;
pub mod censor;
pub mod config;
pub mod extra_infos_server;
pub mod user;

399
src/main.rs Normal file
View File

@ -0,0 +1,399 @@
// Before running this, run:
// 1. rdsys
// 2. lox-distributor
// 3. troll-patrol with the feature "simulation"
use lox_simulation::{
bridge::Bridge,
censor::{self, Censor},
config::Config as SConfig,
extra_infos_server,
user::User,
};
use clap::Parser;
use lox_cli::{networking::*, *};
use memory_stats::memory_stats;
use rand::{prelude::SliceRandom, Rng};
use serde::Deserialize;
use std::{
collections::{HashMap, HashSet},
fs::File,
io::BufReader,
path::PathBuf,
time::Duration,
};
use tokio::{spawn, time::sleep};
use troll_patrol::{extra_info::ExtraInfo, get_date, increment_simulated_date};
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
/// Name/path of the configuration file
#[arg(short, long, default_value = "simulation_config.json")]
config: PathBuf,
}
#[derive(Debug, Deserialize)]
pub struct Config {
pub la_port: u16,
pub la_test_port: u16,
pub tp_port: u16,
pub tp_test_port: u16,
pub bootstrapping_period_duration: u32,
pub censor_secrecy: censor::Secrecy,
pub censor_speed: censor::Speed,
pub censor_event_duration: u32,
pub censor_totality: censor::Totality,
pub censor_partial_blocking_percent: f64,
pub country: String,
pub min_new_users_per_day: u32,
pub max_new_users_per_day: u32,
pub num_connection_retries: u32,
// How many days to simulate
pub num_days: u32,
pub one_positive_report_per_cred: bool,
pub prob_censor_gets_invite: f64,
pub prob_connection_fails: f64,
pub prob_user_invites_friend: f64,
pub prob_user_is_censor: f64,
pub prob_user_submits_reports: f64,
pub prob_user_treats_throttling_as_blocking: f64,
}
#[tokio::main]
pub async fn main() {
let args: Args = Args::parse();
let config: Config = serde_json::from_reader(BufReader::new(
File::open(&args.config).expect("Could not read config file"),
))
.expect("Reading config file from JSON failed");
let la_net = HyperNet {
hostname: format!("http://localhost:{}", config.la_port),
};
let la_net_test = HyperNet {
hostname: format!("http://localhost:{}", config.la_test_port),
};
let tp_net = HyperNet {
hostname: format!("http://localhost:{}", config.tp_port),
};
let tp_net_test = HyperNet {
hostname: format!("http://localhost:{}", config.tp_test_port),
};
let extra_infos_net = HyperNet {
hostname: "http://localhost:8004".to_string(),
};
let la_pubkeys = get_lox_auth_keys(&la_net).await.unwrap();
let sconfig = SConfig {
la_pubkeys,
la_net,
tp_net,
bootstrapping_period_duration: config.bootstrapping_period_duration,
censor_secrecy: config.censor_secrecy,
censor_speed: config.censor_speed,
censor_event_duration: config.censor_event_duration,
censor_totality: config.censor_totality,
censor_partial_blocking_percent: config.censor_partial_blocking_percent,
country: config.country,
num_connection_retries: config.num_connection_retries,
one_positive_report_per_cred: config.one_positive_report_per_cred,
prob_censor_gets_invite: config.prob_censor_gets_invite,
prob_connection_fails: config.prob_connection_fails,
prob_user_invites_friend: config.prob_user_invites_friend,
prob_user_is_censor: config.prob_user_is_censor,
prob_user_submits_reports: config.prob_user_submits_reports,
prob_user_treats_throttling_as_blocking: config.prob_user_treats_throttling_as_blocking,
};
let mut rng = rand::thread_rng();
// Set up censor
let mut censor = Censor::new(&sconfig);
// Set up bridges (no bridges yet)
let mut bridges = HashMap::<[u8; 20], Bridge>::new();
// Set up users
let mut users = Vec::<User>::new();
// Set up extra-infos server
spawn(async move {
extra_infos_server::server().await;
});
sleep(Duration::from_millis(1)).await;
let mut false_neg = 0;
let mut false_pos = 0;
let mut true_neg = 0;
let mut true_pos = 0;
// Track memory use during simulation
let mut max_physical_mem = 0;
let mut max_virtual_mem = 0;
// Main loop
for day in 1..=config.num_days {
println!("Starting day {} of the simulation", day);
println!(
" We have {} users and {} bridges",
users.len(),
bridges.len()
);
println!(
" The censor has learned {} bridges",
censor.known_bridges.len()
);
println!(" Accuracy thus far:");
println!(" True Positives: {}", true_pos);
println!(" True Negatives: {}", true_neg);
println!(" False Positives: {}", false_pos);
println!(" False Negatives: {}", false_neg);
if let Some(usage) = memory_stats() {
if usage.physical_mem > max_physical_mem {
max_physical_mem = usage.physical_mem;
}
if usage.virtual_mem > max_virtual_mem {
max_virtual_mem = usage.virtual_mem;
}
} else {
println!("Failed to get the current memory usage");
}
// USER TASKS
// Number of users who want to join today
let mut num_users_requesting_invites: u32 =
rng.gen_range(config.min_new_users_per_day..=config.max_new_users_per_day);
// How many of the new users are censors?
let mut num_new_censor_users = 0;
for _ in 0..num_users_requesting_invites {
let num: f64 = rng.gen_range(0.0..1.0);
if num < config.prob_user_is_censor {
num_new_censor_users += 1;
num_users_requesting_invites -= 1;
}
}
// Determine whether each new censor user can get an invite from
// an existing trusted user or needs to join via open-entry
// invite. Note: We still favor honest users by giving them
// invites *first*. This means if only a small number of invites
// are available, the censor may still not get invited.
let mut num_censor_invitations = 0;
for _ in 0..num_new_censor_users {
let num: f64 = rng.gen_range(0.0..1.0);
if num < config.prob_censor_gets_invite {
num_censor_invitations += 1;
num_new_censor_users -= 1;
}
}
let mut new_users = Vec::<User>::new();
// Shuffle users so they act in a random order
users.shuffle(&mut rng);
// Users do daily actions
for user in &mut users {
let invited_friends = user
.daily_tasks(
&sconfig,
num_users_requesting_invites,
num_censor_invitations,
&mut bridges,
&mut censor,
)
.await;
if invited_friends.is_ok() {
let mut invited_friends = invited_friends.unwrap();
if invited_friends.len() > 0 {
if !user.is_censor {
// Censors always invite as many censor friends
// as possible. Honest users may invite honest
// friends, or they may accidentally invite
// censor friends.
for inv_friend in &invited_friends {
if inv_friend.is_censor {
num_censor_invitations -= 1;
} else {
num_users_requesting_invites -= 1;
}
}
}
// If this user invited any friends, add them to the
// list of users
new_users.append(&mut invited_friends);
}
}
}
// Add new users
users.append(&mut new_users);
// If any users couldn't get invites, they join with open-entry
// invitations
for _ in 0..num_users_requesting_invites {
let user = User::new(&sconfig, false).await;
if user.is_ok() {
users.push(user.unwrap());
} else {
eprintln!("Failed to create new user.");
}
}
// If any censor users couldn't get invites, they also join with
// open-entry invitations
for _ in 0..(num_new_censor_users + num_censor_invitations) {
let user = User::new(&sconfig, true).await;
if user.is_ok() {
users.push(user.unwrap());
} else {
eprintln!("Failed to create new censor user.");
}
}
// CENSOR TASKS
censor.end_of_day_tasks(&sconfig, &mut bridges).await;
// BRIDGE TASKS
let mut new_extra_infos = HashSet::<ExtraInfo>::new();
for (_, bridge) in bridges.iter_mut() {
// Bridge reports its connections for the day
new_extra_infos.insert(bridge.gen_extra_info(&sconfig.country));
// Bridge resets for tomorrow
bridge.reset_for_tomorrow();
}
// Publish all the bridges' extra-infos for today
let result = extra_infos_net
.request(
"/add".to_string(),
serde_json::to_string(&new_extra_infos).unwrap().into(),
)
.await;
if result.is_ok() {
result.unwrap();
} else {
eprintln!("Failed to publish new extra-infos");
}
// TROLL PATROL TASKS
let new_blockages_resp = tp_net_test.request("/update".to_string(), vec![]).await;
let new_blockages = match new_blockages_resp {
Ok(resp) => match serde_json::from_slice(&resp) {
Ok(new_blockages) => new_blockages,
Err(e) => {
eprintln!("Failed to deserialize new blockages, error: {}", e);
HashMap::<String, HashSet<String>>::new()
}
},
Err(e) => {
eprintln!(
"Failed to get new blockages from Troll Patrol, error: {}",
e
);
HashMap::<String, HashSet<String>>::new()
}
};
// Since we have only one censor, just convert to a set of bridges
let mut blocked_bridges = HashSet::<[u8; 20]>::new();
for (bridge, ccs) in new_blockages {
let fingerprint = array_bytes::hex2array(bridge).unwrap();
if ccs.contains(&sconfig.country) {
blocked_bridges.insert(fingerprint);
}
}
for (fingerprint, bridge) in &mut bridges {
let detected_blocked = blocked_bridges.contains(fingerprint);
// If this is the first day Troll Patrol has determined this
// bridge is blocked, note that for stats
if detected_blocked && bridge.first_detected_blocked == 0 {
bridge.first_detected_blocked = get_date();
}
// Check if censor actually blocks this bridge
let really_blocked = censor.blocks_bridge(&sconfig, fingerprint);
if really_blocked && bridge.first_blocked == 0 {
bridge.first_blocked = get_date();
}
if detected_blocked && really_blocked {
true_pos += 1;
} else if detected_blocked {
false_pos += 1;
} else if really_blocked {
false_neg += 1;
} else {
true_neg += 1;
}
}
// LOX AUTHORITY TASKS
// Advance LA's time to tomorrow
let result = la_net_test
.request(
"/advancedays".to_string(),
serde_json::to_string(&(1 as u16)).unwrap().into(),
)
.await;
if result.is_ok() {
result.unwrap();
} else {
eprintln!("Failed to advance time for LA");
}
// SIMULATION TASKS
// Advance simulated time to tomorrow
increment_simulated_date();
}
// Print various information about the simulation run
println!(
"\nSimulation ended with {} users and {} bridges",
users.len(),
bridges.len()
);
println!("The censor learned {} bridges", censor.known_bridges.len());
println!(
"\nMaximum physical memory usage during simulation: {}",
max_physical_mem
);
println!(
"Maximum virtual memory usage during simulation: {}\n",
max_virtual_mem
);
println!("True Positives: {}", true_pos);
println!("True Negatives: {}", true_neg);
println!("False Positives: {}", false_pos);
println!("False Negatives: {}", false_neg);
println!("\nFull stats per bridge:");
println!(
"Fingerprint,first_distributed,first_blocked,first_detected_blocked,first_positive_report"
);
for (fingerprint, bridge) in bridges {
println!(
"{},{},{},{},{}",
array_bytes::bytes2hex("", fingerprint),
bridge.first_distributed,
bridge.first_blocked,
bridge.first_detected_blocked,
bridge.first_positive_report
);
}
}

682
src/user.rs Normal file
View File

@ -0,0 +1,682 @@
// 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::Lox, proto::check_blockage::MIN_TRUST_LEVEL, scalar_u32,
};
use rand::Rng;
use std::{cmp::min, collections::HashMap};
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
}
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<Lox>,
// Does the user submit reports to Troll Patrol?
submits_reports: bool,
// How likely is this user to use bridges on a given day?
prob_use_bridges: f64,
}
impl User {
pub async fn new(config: &Config, is_censor: bool) -> Result<Self> {
let cred = get_lox_credential(
&config.la_net,
&get_open_invitation(&config.la_net).await?,
get_lox_pub(&config.la_pubkeys),
)
.await?
.0;
// Probabilistically decide whether this user submits reports
let submits_reports = if is_censor {
false
} else {
event_happens(config.prob_user_submits_reports)
};
// Randomly determine how likely this user is to use bridges on
// a given day
let mut rng = rand::thread_rng();
let prob_use_bridges = rng.gen_range(0.0..=1.0);
Ok(Self {
is_censor,
primary_cred: cred,
secondary_cred: None,
submits_reports: submits_reports,
prob_use_bridges: prob_use_bridges,
})
}
pub async fn trusted_user(config: &Config) -> Result<Self> {
let cred = get_lox_credential(
&config.la_net,
&get_open_invitation(&config.la_net).await?,
get_lox_pub(&config.la_pubkeys),
)
.await?
.0;
Ok(Self {
is_censor: false,
primary_cred: cred,
secondary_cred: None,
submits_reports: true,
prob_use_bridges: 1.0,
})
}
// TODO: This should probably return an actual error type
pub async fn invite(
&mut self,
config: &Config,
censor: &mut Censor,
invited_user_is_censor: bool,
) -> Result<Self> {
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 self.is_censor {
// Make sure censor has access to each bridge and each
// credential
let (bucket, _reachcred) = get_bucket(&config.la_net, &self.primary_cred).await?;
for bl in bucket {
let fingerprint = bl.get_hashed_fingerprint();
censor.learn_bridge(&fingerprint);
censor.give_lox_cred(&fingerprint, &self.primary_cred);
}
}
let friend_cred = redeem_invite(
&config.la_net,
&invite,
get_lox_pub(&config.la_pubkeys),
get_invitation_pub(&config.la_pubkeys),
)
.await?
.0;
// Calling function decides if the invited user is a censor
let is_censor = invited_user_is_censor;
// Probabilistically decide whether this user submits reports
let submits_reports = if is_censor {
false
} else {
event_happens(config.prob_user_submits_reports)
};
// Randomly determine how likely this user is to use bridges on
// a given day
let mut rng = rand::thread_rng();
let prob_use_bridges = rng.gen_range(0.0..=1.0);
Ok(Self {
is_censor,
primary_cred: friend_cred,
secondary_cred: None,
submits_reports: submits_reports,
prob_use_bridges: prob_use_bridges,
})
}
// Attempt to "connect" to the bridge, returns true if successful.
// Note that this does not involve making a real connection to a
// real bridge. The function is async because the *censor* might
// submit a positive report during this function.
pub fn connect(&self, config: &Config, bridge: &mut Bridge, censor: &Censor) -> bool {
if censor.blocks_bridge(config, &bridge.fingerprint) {
if config.censor_totality == Full
|| config.censor_totality == Partial
&& event_happens(censor.partial_blocking_percent)
{
// 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
}
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<NegativeReport>,
) -> Result<()> {
let date = get_date();
let pubkey = match serde_json::from_slice::<Option<PublicKey>>(
&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<PositiveReport>,
) -> 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,
num_users_requesting_invites: u32,
num_censor_invites: u32,
bridges: &mut HashMap<[u8; 20], Bridge>,
censor: &mut Censor,
) -> Result<Vec<User>> {
if self.is_censor {
self.daily_tasks_censor(config, bridges, censor).await
} else {
self.daily_tasks_non_censor(
config,
num_users_requesting_invites,
num_censor_invites,
bridges,
censor,
)
.await
}
}
// 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<Mutex<>>
// or something so we can parallelize this.
pub async fn daily_tasks_non_censor(
&mut self,
config: &Config,
num_users_requesting_invites: u32,
num_censor_invites: u32,
bridges: &mut HashMap<[u8; 20], Bridge>,
censor: &mut Censor,
) -> Result<Vec<User>> {
// Probabilistically decide if the user should use bridges today
if event_happens(self.prob_use_bridges) {
// 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")),
};
// Make sure each bridge in bucket is in the global bridges set
for bridgeline in bucket {
if bridgeline != BridgeLine::default() {
if !bridges.contains_key(&bridgeline.get_hashed_fingerprint()) {
let bridge = Bridge::from_bridge_line(&bridgeline);
bridges.insert(bridgeline.get_hashed_fingerprint(), bridge);
}
}
}
// Can we level up the main credential?
let can_level_up = reachcred.is_some()
&& (level == 0
&& eligible_for_trust_promotion(&config.la_net, &self.primary_cred).await
|| 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;
let mut failed = Vec::<BridgeLine>::new();
let mut succeeded = Vec::<BridgeLine>::new();
// Try to connect to each bridge
for i in 0..bucket.len() {
// At level 0, we only have 1 bridge
if bucket[i] != BridgeLine::default() {
if self.connect(
&config,
bridges
.get_mut(&bucket[i].get_hashed_fingerprint())
.unwrap(),
&censor,
) {
succeeded.push(bucket[i]);
} else {
failed.push(bucket[i]);
}
}
}
// If we were not able to connect to any bridges, get a
// second credential
let second_cred = if succeeded.len() < 1 {
if self.secondary_cred.is_some() {
std::mem::replace(&mut self.secondary_cred, 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();
let (second_bucket, second_reachcred) =
get_bucket(&config.la_net, &second_cred).await?;
for bridgeline in second_bucket {
if bridgeline != BridgeLine::default() {
if !bridges.contains_key(&bridgeline.get_hashed_fingerprint()) {
bridges.insert(
bridgeline.get_hashed_fingerprint(),
Bridge::from_bridge_line(&bridgeline),
);
}
// Attempt to connect to second cred's bridge
if self.connect(
&config,
bridges
.get_mut(&bridgeline.get_hashed_fingerprint())
.unwrap(),
censor,
) {
succeeded.push(bridgeline);
if second_reachcred.is_some()
&& eligible_for_trust_promotion(&config.la_net, &second_cred).await
{
second_level_up = true;
}
} else {
failed.push(bridgeline);
}
}
}
}
let mut negative_reports = Vec::<NegativeReport>::new();
let mut positive_reports = Vec::<PositiveReport>::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 bridge = bridges
.get_mut(&bridgeline.get_hashed_fingerprint())
.unwrap();
if bridge.first_positive_report == 0 {
bridge.first_positive_report = get_date();
}
positive_reports.push(
PositiveReport::from_lox_credential(
bridgeline.get_hashed_fingerprint(),
None,
&self.primary_cred,
get_lox_pub(&config.la_pubkeys),
config.country.to_string(),
)
.unwrap(),
);
}
}
}
// 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 {
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(
&config.la_net,
&self.primary_cred,
&reachcred.unwrap(),
get_lox_pub(&config.la_pubkeys),
get_reachability_pub(&config.la_pubkeys),
)
.await?
};
self.primary_cred = cred;
self.secondary_cred = None;
}
// We favor starting over at level 1 to migrating to level
// 1, but if we have a level 4 credential for a bridge that
// hasn't been marked blocked, save the credential so we can
// migrate to a level 2 cred. Note that second_level_up is
// only true if we were unable to connect with bridges from
// our primary credential.
else if second_level_up && (level <= MIN_TRUST_LEVEL || reachcred.is_none()) {
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 can_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_cred.is_some() {
// Couldn't connect with primary credential
if succeeded.len() > 0 {
// Keep the second credential only if it's useful
self.secondary_cred = second_cred;
}
}
if negative_reports.len() > 0 {
Self::send_negative_reports(&config, negative_reports).await?;
}
if positive_reports.len() > 0 {
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
};
let mut new_friends = Vec::<User>::new();
for _i in 0..min(invitations, num_users_requesting_invites) {
if event_happens(config.prob_user_invites_friend) {
// Invite non-censor friend
match self.invite(&config, censor, false).await {
Ok(friend) => {
// You really shouldn't push your friends,
// especially new ones whose boundaries you
// might not know well.
new_friends.push(friend);
}
Err(e) => {
println!("{}", e);
}
}
}
}
// Invite censor users if applicable
let invitations = invitations - new_friends.len() as u32;
for _i in 0..min(invitations, num_censor_invites) {
if event_happens(config.prob_user_invites_friend) {
// Invite non-censor friend
match self.invite(&config, censor, true).await {
Ok(friend) => {
new_friends.push(friend);
}
Err(e) => {
println!("{}", e);
}
}
}
}
Ok(new_friends)
} else {
Ok(Vec::<User>::new())
}
}
// User cooperates with censor and performs daily tasks to try to
// learn more bridges.
pub async fn daily_tasks_censor(
&mut self,
config: &Config,
bridges: &mut HashMap<[u8; 20], Bridge>,
censor: &mut Censor,
) -> Result<Vec<User>> {
// 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
for bridgeline in bucket {
if bridgeline != BridgeLine::default() {
if !bridges.contains_key(&bridgeline.get_hashed_fingerprint()) {
let bridge = Bridge::from_bridge_line(&bridgeline);
bridges.insert(bridgeline.get_hashed_fingerprint(), bridge);
}
censor.learn_bridge(&bridgeline.get_hashed_fingerprint());
}
}
// Censor user tries to level up their primary credential
if reachcred.is_some() {
if level == 0 && eligible_for_trust_promotion(&config.la_net, &self.primary_cred).await
|| level > 0 && eligible_for_level_up(&config.la_net, &self.primary_cred).await
{
let new_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(
&config.la_net,
&self.primary_cred,
&reachcred.unwrap(),
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);
censor.give_lox_cred(&fingerprint, &self.primary_cred);
}
}
} else {
// LA has identified this bucket as blocked. This change
// will not be reverted, so replace the primary credential
// with a new level 0 credential and work on gaining trust
// for that one.
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;
} else {
eprintln!("Censor failed to get new credential");
}
}
// 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.
} else {
eprintln!("Censor failed to get new credential");
}
// Censor user invites as many censor friends as possible
let invitations = scalar_u32(&self.primary_cred.invites_remaining).unwrap();
let mut new_friends = Vec::<User>::new();
for _ in 0..invitations {
match self.invite(&config, censor, true).await {
Ok(friend) => {
new_friends.push(friend);
}
Err(e) => {
println!("{}", e);
}
}
}
Ok(new_friends)
}
}