diff --git a/src/lox_analysis.rs b/src/lox_analysis.rs index c9163b5..86dc2d6 100644 --- a/src/lox_analysis.rs +++ b/src/lox_analysis.rs @@ -1,12 +1,6 @@ use crate::analysis::Analyzer; -use lox_library::{ - bridge_table::MAX_BRIDGES_PER_BUCKET, - proto::{ - level_up::{LEVEL_INTERVAL, LEVEL_INVITATIONS}, - trust_promotion::UNTRUSTED_INTERVAL, - }, - OPENINV_K, -}; +use lox_library::proto::{level_up::LEVEL_INTERVAL, trust_promotion::UNTRUSTED_INTERVAL}; +use std::cmp::min; // Get max value from slice, or 0 if empty fn max(nums: &[u32]) -> u32 { @@ -19,73 +13,20 @@ fn max(nums: &[u32]) -> u32 { max_num } -// Max users expected on a Lox bridge based on how long it's been around -fn max_users(days: u32) -> u32 { - if days <= UNTRUSTED_INTERVAL { - // k users for open-entry bucket - OPENINV_K - } else if days <= UNTRUSTED_INTERVAL + LEVEL_INTERVAL[1] { - // k users per bridge x 3 bridges in invite-only bucket - OPENINV_K * MAX_BRIDGES_PER_BUCKET as u32 - } else if days <= UNTRUSTED_INTERVAL + LEVEL_INTERVAL[2] { - // suppose users have used all their invitations - OPENINV_K * MAX_BRIDGES_PER_BUCKET as u32 * (1 + LEVEL_INVITATIONS[1]) - } else { - // stop counting here, just say it's a lot - 100 - } -} - -// Maximum number of negative reports without considering the bridge blocked -fn max_negative_reports(days: u32) -> u32 { - // How many users do we expect to use this bridge? - let max_users = max_users(days); - - // Based on that, allow this many negative reports - if max_users <= 10 { - 1 - } else if max_users <= 30 { - 5 - } else { - 10 - } -} - -fn too_few_bridge_ips( - days: u32, - _max_bridge_ips: u32, - bridge_ips_today: u32, - positive_reports_today: u32, -) -> bool { - // How many users do we expect to use this bridge? - let max_users = max_users(days); - - // Based on that, expect this many bridge ips - let min_bip = if max_users <= 16 { - // expect 10 +// Safe u32 subtraction: x - y or 0 if y > x +fn ssub(x: u32, y: u32) -> u32 { + if y > x { 0 - } else if max_users <= 40 { - // expect 30, possibly as few as 21 - 0 - } else if max_users <= 96 { - // expect 90 - 8 } else { - 16 - }; - - // If we see positive reports from trusted users, halve the minimum - let min_bip = if positive_reports_today > 0 { - min_bip / 2 - } else { - min_bip - }; - - bridge_ips_today < min_bip + x - y + } } /// Analyzer for Lox bridges based on how long they've been active -pub struct LoxAnalyzer {} +pub struct LoxAnalyzer { + // simple variable threshold from 0-4 or something + harshness: u32, +} impl Analyzer for LoxAnalyzer { // At this stage, we expect 0-10 users, so 0, 8, or 16 bridge-ips @@ -101,13 +42,13 @@ impl Analyzer for LoxAnalyzer { // Get max bridge_ips we've seen let max_bridge_ips = max(bridge_ips); - if too_few_bridge_ips(age, max_bridge_ips, bridge_ips_today, 0) { + if self.too_few_bridge_ips(age, max_bridge_ips, bridge_ips_today, 0) { return true; } - // If we have more negative reports than expected, consider the - // bridge blocked - negative_reports_today > max_negative_reports(age) + // If we have too many negative reports, consider the bridge + // blocked + self.too_many_negative_reports(negative_reports_today) } fn stage_two( @@ -122,13 +63,13 @@ impl Analyzer for LoxAnalyzer { // Get max bridge_ips we've seen let max_bridge_ips = max(bridge_ips); - if too_few_bridge_ips(age, max_bridge_ips, bridge_ips_today, 0) { + if self.too_few_bridge_ips(age, max_bridge_ips, bridge_ips_today, 0) { return true; } - // If we have more negative reports than expected, consider the - // bridge blocked - negative_reports_today > max_negative_reports(age) + // If we have too many negative reports, consider the bridge + // blocked + self.too_many_negative_reports(negative_reports_today) } fn stage_three( @@ -145,7 +86,7 @@ impl Analyzer for LoxAnalyzer { // Get max bridge_ips we've seen let max_bridge_ips = max(bridge_ips); - if too_few_bridge_ips( + if self.too_few_bridge_ips( age, max_bridge_ips, bridge_ips_today, @@ -154,8 +95,102 @@ impl Analyzer for LoxAnalyzer { return true; } - // If we have more negative reports than expected, consider the - // bridge blocked - negative_reports_today > max_negative_reports(age) + // If we have too many negative reports, consider the bridge + // blocked + self.too_many_negative_reports(negative_reports_today) + } +} + +impl LoxAnalyzer { + pub fn new(harshness: u32) -> Self { + Self { harshness } + } + + // Maximum number of negative reports without considering the bridge blocked + fn too_many_negative_reports(&self, negative_reports_today: u32) -> bool { + // If we have more negative reports than 4 - harshness + negative_reports_today > 4 - self.harshness + + // harshness: 4, blocked if 1 negative report + // harshness: 3, blocked if 2 negative reports + // harshness: 2, blocked if 3 negative reports + // harshness: 1, blocked if 4 negative reports + // harshness: 0, blocked if 5 negative reports + } + + // Based on the age of the bridge and the max bridge-ips observed, do we + // have too few today? + fn too_few_bridge_ips( + &self, + days: u32, + max_bridge_ips: u32, + bridge_ips_today: u32, + positive_reports_today: u32, + ) -> bool { + if days <= UNTRUSTED_INTERVAL { + // We expect 0-10 users + if self.harshness == 4 { + // In the most extreme case, mark any bridge with 0 connections + bridge_ips_today == 0 + } else if self.harshness >= 2 { + // With medium harshness, mark any bridge that has had + // 9+ connections and now sees 0 + max_bridge_ips > 8 && bridge_ips_today == 0 + } else { + // With low harshness, we cannot make a judgement from + // bridge stats + false + } + } else if days <= UNTRUSTED_INTERVAL + LEVEL_INTERVAL[1] { + // We expect 1-30 users + + let threshold = min(max_bridge_ips, 32); + + // If we're at least 4 - self.harshness bins below the + // maximum value we've seen or 32 if we've seen a lot of + // connections, consider the bridge blocked. + bridge_ips_today < ssub(threshold, (4 - self.harshness) * 8) + + // Examples: + // max 64 connections, harshness = 4 + // Return true if today's count is less than 32 + // max 32 connections, harshness = 4 + // Return true if today's count is less than 32 + // max 32 connections, harshness = 3 + // Return true if today's count is less than 24 + // max 32 connections, harshness = 2 + // Return true if today's count is less than 16 + // max 8 connections, harshness = 4 + // Return true if today's count is less than 8 + // max 8 connections, harshness = 3 + // Return false + } else { + // Similar, but consider positive reports as well + let threshold = min(max_bridge_ips, 32); + + // We allow positive reports to increase the threshold for + // considering the bridge blocked. 8 positive reports + // (rounded up) remove 1 level reduction from the threshold + let threshold = ssub( + threshold, + (4 - self.harshness + (positive_reports_today + 7) / 8) * 8, + ); + + // For example, suppose we've seen 32+ connections. + // + // If we have harshness 4, we mark the bridge blocked if it + // has fewer than 32 connections. + // + // If we have harshness 4 but have seen 1-7 positive + // reports, we mark the bridge blocked only if it has fewer + // than 24 connections. + // + // 25 positive reports reduce all thresholds to 0, meaning + // bridges will never be marked blocked from bridge stats + // if the adversary can submit 25 positive reports. + + // Consider the bridge blocked if we have too few connections + bridge_ips_today < threshold + } } } diff --git a/src/main.rs b/src/main.rs index 2188d4a..937c4d6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -103,7 +103,8 @@ async fn update_daily_info( update_positive_reports(db, distributors).await; let new_blockages = guess_blockages( db, - &lox_analysis::LoxAnalyzer {}, + // Using max_threshold for convenience + &lox_analysis::LoxAnalyzer::new(max_threshold), confidence, min_historical_days, max_historical_days,