diff --git a/Cargo.lock b/Cargo.lock index 017a05bb4..a6ae2b3f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2652,7 +2652,7 @@ dependencies = [ [[package]] name = "rita_bin" -version = "0.21.0" +version = "0.21.1" dependencies = [ "actix", "actix-rt", @@ -2688,7 +2688,7 @@ dependencies = [ [[package]] name = "rita_client" -version = "0.21.0" +version = "0.21.1" dependencies = [ "actix", "actix-web", @@ -2727,7 +2727,7 @@ dependencies = [ [[package]] name = "rita_common" -version = "0.21.0" +version = "0.21.1" dependencies = [ "actix", "actix-service", @@ -2769,7 +2769,7 @@ dependencies = [ [[package]] name = "rita_exit" -version = "0.21.0" +version = "0.21.1" dependencies = [ "actix", "actix-web", @@ -2801,7 +2801,7 @@ dependencies = [ [[package]] name = "rita_extender" -version = "0.21.0" +version = "0.21.1" dependencies = [ "actix", "actix-web", diff --git a/althea_types/src/interop.rs b/althea_types/src/interop.rs index 00961d91e..5d9e2575a 100644 --- a/althea_types/src/interop.rs +++ b/althea_types/src/interop.rs @@ -770,8 +770,6 @@ pub struct OperatorExitCheckinMessage { pub pass: String, /// This is to keep track of the rita exit uptime for debugging purposes pub exit_uptime: Duration, - /// A list of registered wg keys that ops can use to display routers to be registered - pub registered_keys: Option>, /// Number of users online pub users_online: Option, } diff --git a/rita_exit/src/database/mod.rs b/rita_exit/src/database/mod.rs index 4fa15acd8..b286664a8 100644 --- a/rita_exit/src/database/mod.rs +++ b/rita_exit/src/database/mod.rs @@ -1,10 +1,10 @@ //! This module contains all the tools and functions that integrate with the clients database //! for the exit, which is most exit logic in general. Keep in mind database connections are remote //! and therefore synchronous database requests are quite expensive (on the order of tens of milliseconds) -use crate::database::geoip::get_country; use crate::database::geoip::get_gateway_ip_bulk; use crate::database::geoip::get_gateway_ip_single; use crate::database::geoip::verify_ip; +use crate::database::struct_tools::display_hashset; use crate::database::struct_tools::get_client_internal_ip; use crate::database::struct_tools::get_client_ipv6; use crate::database::struct_tools::to_exit_client; @@ -25,6 +25,7 @@ use settings::get_rita_exit; use std::collections::HashMap; use std::collections::HashSet; use std::net::IpAddr; +use std::time::Duration; use std::time::Instant; use std::time::SystemTime; @@ -35,6 +36,20 @@ pub mod struct_tools; /// one day in seconds pub const ONE_DAY: i64 = 86400; +/// Default url exits use to request a client registration +pub const DEFAULT_REGISTER_URL: &str = "https://operator.althea.net:8080/register_router"; + +/// Timeout when requesting client registration +pub const CLIENT_REGISTER_TIMEOUT: Duration = Duration::from_secs(5); + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum ExitSignupReturn { + RegistrationOk, + PendingRegistration, + BadPhoneNumber, + InternalServerError { e: String }, +} + pub fn get_exit_info() -> ExitDetails { let exit_settings = get_rita_exit(); ExitDetails { @@ -45,13 +60,19 @@ pub fn get_exit_info() -> ExitDetails { netmask: exit_settings.exit_network.netmask, description: exit_settings.description, verif_mode: match exit_settings.verif_settings { - Some(ExitVerifSettings::Email(_mailer_settings)) => ExitVerifMode::Email, Some(ExitVerifSettings::Phone(_phone_settings)) => ExitVerifMode::Phone, None => ExitVerifMode::Off, }, } } +fn get_client_register_url() -> String { + match get_rita_exit().client_register_url { + Some(url) => url, + _ => DEFAULT_REGISTER_URL.to_string(), + } +} + /// Handles a new client registration api call. Performs a geoip lookup /// on their registration ip to make sure that they are coming from a valid gateway /// ip and then sends out an email of phone message @@ -67,15 +88,93 @@ pub async fn signup_client( let verify_status = verify_ip(gateway_ip)?; info!("verified the ip country {:?}", client); - let user_country = get_country(gateway_ip)?; - info!("got the country {:?}", client); + // Is client requesting from a valid country? If so send registration request to ops + if !verify_status { + return Ok(ExitState::Denied { + message: format!( + "This exit only accepts connections from {}", + display_hashset(&exit_settings.allowed_countries), + ), + }); + } + + // Forward request to ops and send result to client accordingly + let exit_client = to_exit_client(client.global); + if let Ok(exit_client) = exit_client { + match forward_client_signup_request(client).await { + registered => { + return Ok(ExitState::Registered { + our_details: ExitClientDetails { + client_internal_ip: get_client_internal_ip(), + internet_ipv6_subnet: get_client_ipv6(client.global), + }, + general_details: get_exit_info(), + message: "Registration OK".to_string(), + }) + } + + pending => { + return Ok(ExitState::Pending { + general_details: get_exit_info(), + message: "awaiting email verification".to_string(), + email_code: None, + phone_code: None, + }) + } + } + } else { + return Ok(ExitState::Denied { + message: format!("Error parsing client details with {:?}", exit_client,), + }); + } +} + +pub async fn forward_client_signup_request(exit_client: ExitClientIdentity) -> ExitSignupReturn { + let url: &str; + if cfg!(feature = "dev_env") { + url = "http://0.0.0.0:8080/register_router"; + } else if cfg!(feature = "operator_debug") { + url = "http://192.168.10.2:8080/register_router"; + } else { + url = &get_client_register_url(); + } info!( - "Doing database work for {:?} in country {} with verify_status {}", - client, user_country, verify_status + "About to request client {} registration with {}", + exit_client.global, url ); - forward_client_signup_request(client) + let client = awc::Client::default(); + let response = client + .post(url) + .timeout(CLIENT_REGISTER_TIMEOUT) + .send_json(&exit_client) + .await; + + let response = match response { + Ok(mut response) => { + trace!("Response is {:?}", response.status()); + trace!("Response is {:?}", response.headers()); + response.json().await + } + Err(e) => { + error!("Failed to perform client registration with {:?}", e); + return ExitSignupReturn::InternalServerError { + e: format!("Unable to contact registration server: {}", e), + }; + } + }; + + let response: ExitSignupReturn = match response { + Ok(a) => a, + Err(e) => { + error!("Failed to decode registration request {:?}", e); + return ExitSignupReturn::InternalServerError { + e: format!("Failed to decode registration request {:?}", e), + }; + } + }; + response } /// Gets the status of a client and updates it in the database diff --git a/rita_exit/src/database/struct_tools.rs b/rita_exit/src/database/struct_tools.rs index 4cba72806..073819676 100644 --- a/rita_exit/src/database/struct_tools.rs +++ b/rita_exit/src/database/struct_tools.rs @@ -2,9 +2,12 @@ use althea_kernel_interface::ExitClient; use althea_types::{Identity, WgKey}; use ipnetwork::{IpNetwork, Ipv4Network}; use std::collections::hash_map::DefaultHasher; +use std::collections::{HashMap, HashSet}; use std::convert::TryInto; +use std::fmt::Write; use std::hash::{Hash, Hasher}; use std::net::{IpAddr, Ipv4Addr}; +use std::sync::{Arc, RwLock}; use crate::{generate_iterative_client_subnet, RitaExitError, DEFAULT_CLIENT_SUBNET_SIZE}; @@ -14,6 +17,48 @@ pub const CLIENT_WG_PORT: u16 = 59999; /// Max number of time we try to generate a valid ip addr before returning an eror pub const MAX_IP_RETRIES: u8 = 10; +lazy_static! { + /// Keep track of ip addrs assigned to clients and ensure collisions dont happen. In worst case + /// the exit restarts and loses all this data in which case those client they had collision may get new + /// ip addrs and would need to setup wg exit tunnel again + static ref IP_ASSIGNMENT_MAP: Arc> = Arc::new(RwLock::new(IpAssignmentMap::default())); +} + +#[derive(Clone, Debug, Default)] +pub struct IpAssignmentMap { + pub ipv6_assignments: HashMap, + pub internal_ip_assignments: HashMap, +} + +// Lazy static setters/getters +pub fn get_ipv6_assignments() -> HashMap { + IP_ASSIGNMENT_MAP.read().unwrap().ipv6_assignments.clone() +} + +pub fn get_internal_ip_assignments() -> HashMap { + IP_ASSIGNMENT_MAP + .read() + .unwrap() + .internal_ip_assignments + .clone() +} + +pub fn add_new_ipv6_assignment(addr: IpAddr, key: WgKey) { + IP_ASSIGNMENT_MAP + .write() + .unwrap() + .ipv6_assignments + .insert(addr, key); +} + +pub fn add_new_internal_ip_assignement(addr: IpAddr, key: WgKey) { + IP_ASSIGNMENT_MAP + .write() + .unwrap() + .internal_ip_assignments + .insert(addr, key); +} + /// Given a client identity, get the clients ipv6 addr using the wgkey as a generative seed pub fn get_client_ipv6(their_record: Identity) -> Result, Box> { let rita_exit = settings::get_rita_exit(); @@ -30,11 +75,31 @@ pub fn get_client_ipv6(their_record: Identity) -> Result, Box< // is lower than this number. For example, exit subnet: fd00:1000/120, client subnet /124, number of subnets will be // 2^(124 - 120) => 2^4 => 16 let total_subnets = 1 << (client_subnet_size - exit_sub.prefix()); - let generative_index = wg_hash % total_subnets; - let client_subnet = - generate_iterative_client_subnet(exit_sub, generative_index, client_subnet_size)?; + let mut generative_index = wg_hash % total_subnets; + + // Loop to try to generate a valid address + let mut retries = 0; + loop { + // Return an error if we retry too many times + if retries > MAX_IP_RETRIES { + return Err(Box::new(RitaExitError::MiscStringError(format!( + "Unable to get internet ipv6 using network {} and index {}", + exit_sub, generative_index + )))); + } - Ok(Some(client_subnet)) + let client_subnet = + generate_iterative_client_subnet(exit_sub, generative_index, client_subnet_size)?; + + if validate_internet_ipv6(client_subnet, their_record.wg_public_key) { + add_new_ipv6_assignment(client_subnet.ip(), their_record.wg_public_key); + return Ok(Some(client_subnet)); + } else { + retries += 1; + generative_index = (generative_index + 1) % total_subnets; + continue; + } + } } else { // This exit doesnt support ipv6 Ok(None) @@ -91,7 +156,8 @@ pub fn get_client_internal_ip(their_record: Identity) -> Result Result bool { + let assigned_ips = get_ipv6_assignments(); + let assignment = assigned_ips.get(&client_subnet.ip()); + match assignment { + Some(a) => { + // There is an entry, verify if its our entry else false + if *a == our_wgkey { + return true; + } else { + return false; + } + } + // There is no assigned ip here, ip is valid + None => return true, + } +} + /// Check that this ip can be assigned, make sure it isnt our ip, network ip, broadcast ip, etc -pub fn validate_internal_ip(network: Ipv4Network, assigned_ip: Ipv4Addr, our_ip: Ipv4Addr) -> bool { +pub fn validate_internal_ip( + network: Ipv4Network, + assigned_ip: Ipv4Addr, + our_ip: Ipv4Addr, + our_wgkey: WgKey, +) -> bool { let broadcast = network.broadcast(); let network_ip = network.network(); - match assigned_ip { - our_ip => return false, - network_ip => return false, - broadcast => return false, - _ => return true, + + // Collision with our ip + if assigned_ip == our_ip { + return false; + } + // collision with the network ip + if assigned_ip == network_ip { + return false; + } + // collision with broadcast address + if assigned_ip == broadcast { + return false; + } + + let assignments = get_internal_ip_assignments(); + let assignment = assignments.get(&IpAddr::V4(assigned_ip)); + match assignment { + Some(a) => { + // check if this existing ip is ours + if *a == our_wgkey { + return true; + } else { + return false; + } + } + // No assignment, we can use this address + None => return true, } } @@ -131,3 +242,12 @@ pub fn hash_wgkey(key: WgKey) -> u64 { key.to_string().hash(&mut hasher); hasher.finish() } + +/// quick display function for a neat error +pub fn display_hashset(input: &HashSet) -> String { + let mut out = String::new(); + for item in input.iter() { + write!(out, "{item}, ").unwrap(); + } + out +} diff --git a/rita_exit/src/network_endpoints/mod.rs b/rita_exit/src/network_endpoints/mod.rs index b2a219827..8daed878c 100644 --- a/rita_exit/src/network_endpoints/mod.rs +++ b/rita_exit/src/network_endpoints/mod.rs @@ -246,7 +246,7 @@ pub async fn get_exit_list(request: Json) -> HttpRe let their_nacl_pubkey = request.pubkey.into(); let ret: ExitList = ExitList { - exit_list: settings::get_rita_exit().exit_network.cluster_exits, + exit_list: get_smart_contract_cluster_exits(request.pubkey), wg_exit_listen_port: settings::get_rita_exit().exit_network.wg_v2_tunnel_port, }; diff --git a/rita_exit/src/operator_update/mod.rs b/rita_exit/src/operator_update/mod.rs index 1bf3de028..b8306b833 100644 --- a/rita_exit/src/operator_update/mod.rs +++ b/rita_exit/src/operator_update/mod.rs @@ -1,14 +1,9 @@ //! This module is responsible for checking in with the operator server and getting updated local settings pub mod update_loop; - -use althea_types::ExitClientIdentity; use althea_types::OperatorExitCheckinMessage; -use althea_types::OperatorExitUpdateMessage; -use althea_types::WgKey; use rita_common::KI; use std::time::{Duration, Instant}; -use crate::database::signup_client; use crate::rita_loop::EXIT_INTERFACE; pub struct UptimeStruct { @@ -46,68 +41,17 @@ pub async fn operator_update(rita_started: Instant) { info!("About to perform operator update with {}", url); let client = awc::Client::default(); - let response = client + let _response = client .post(url) .timeout(OPERATOR_UPDATE_TIMEOUT) .send_json(&OperatorExitCheckinMessage { id, pass, exit_uptime: rita_started.elapsed(), - registered_keys: get_registered_list(), // Since this checkin works only from b20, we only need to look on wg_exit_v2 users_online: KI.get_wg_exit_clients_online(EXIT_INTERFACE).ok(), }) .await; - let response = match response { - Ok(mut response) => { - trace!("Response is {:?}", response.status()); - trace!("Response is {:?}", response.headers()); - response.json().await - } - Err(e) => { - error!("Failed to perform exit operator checkin with {:?}", e); - return; - } - }; - - let new_settings: OperatorExitUpdateMessage = match response { - Ok(a) => a, - Err(e) => { - error!("Failed to perform exit operator checkin with {:?}", e); - return; - } - }; - - // Perform operator updates - register_op_clients(new_settings.to_register).await; } } - -async fn register_op_clients(clients: Vec) { - info!("Signing up ops clients {:?}", clients); - for c in clients { - // Though this is asnyc, it wont block since the only async part (sms handling) - // is skiped in this function - let c_key = c.global.wg_public_key; - if let Err(e) = signup_client(c, true).await { - error!("Unable to signup client {} with {:?}", c_key, e); - }; - } -} - -pub fn get_registered_list() -> Option> { - let registered_routers = get_registered_client_list_from_smart_contract(); - Some( - registered_routers - .iter() - .filter_map(|r| match r.parse() { - Ok(a) => Some(a), - Err(_) => { - error!("Invalid wg key in database! {}", r); - None - } - }) - .collect::>(), - ) -} diff --git a/settings/src/exit.rs b/settings/src/exit.rs index 63087fe32..960ae9783 100644 --- a/settings/src/exit.rs +++ b/settings/src/exit.rs @@ -191,7 +191,6 @@ pub struct PhoneVerifSettings { #[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] #[serde(tag = "type", content = "contents")] pub enum ExitVerifSettings { - Email(EmailVerifSettings), Phone(PhoneVerifSettings), }