From bfb8119e1b5425cf6d15b850ad7ee10fc2dbf344 Mon Sep 17 00:00:00 2001 From: Justin Kilpatrick Date: Tue, 12 Nov 2024 15:30:03 -0500 Subject: [PATCH] WIP: IP assignment for CGNAT And SNAT modes [2/?] --- integration_tests/src/setup_utils/rita.rs | 2 + rita_exit/src/database/dualmap.rs | 161 +++++++++++ rita_exit/src/database/ipddr_assignment.rs | 299 +++++++++++++++------ rita_exit/src/database/mod.rs | 11 +- rita_exit/src/error.rs | 2 + rita_exit/src/rita_loop/mod.rs | 6 +- settings/src/error.rs | 8 + settings/src/exit.rs | 87 +++++- 8 files changed, 481 insertions(+), 95 deletions(-) create mode 100644 rita_exit/src/database/dualmap.rs diff --git a/integration_tests/src/setup_utils/rita.rs b/integration_tests/src/setup_utils/rita.rs index 62269fc73..c6af54e0d 100644 --- a/integration_tests/src/setup_utils/rita.rs +++ b/integration_tests/src/setup_utils/rita.rs @@ -290,6 +290,8 @@ pub fn spawn_rita_exit( let client_and_ip_map = Arc::new(RwLock::new(ClientListAnIpAssignmentMap::new( HashSet::new(), + settings::get_rita_exit().exit_network.ipv6_routing, + settings::get_rita_exit().exit_network.ipv4_routing, ))); let workers = 4; diff --git a/rita_exit/src/database/dualmap.rs b/rita_exit/src/database/dualmap.rs new file mode 100644 index 000000000..a7986be1a --- /dev/null +++ b/rita_exit/src/database/dualmap.rs @@ -0,0 +1,161 @@ +/// A very quick implementation of a dual map, which is a map that can be indexed by either key or value. +/// simply by maintaining two maps, one for each direction. This is not a very efficient implementation in terms +/// of memory usage, doubling the memory usage of the map. +use std::collections::HashMap; + +#[derive(Debug, Clone)] +pub struct DualMap { + key_to_value: HashMap, + value_to_key: HashMap, +} + +impl DualMap { + pub fn new() -> DualMap { + DualMap { + key_to_value: HashMap::new(), + value_to_key: HashMap::new(), + } + } + + pub fn into_hashmap(self) -> HashMap { + self.key_to_value + } + + pub fn contains_key(&self, key: &K) -> bool { + self.key_to_value.contains_key(key) + } + + pub fn contains_value(&self, value: &V) -> bool { + self.value_to_key.contains_key(value) + } + + pub fn insert(&mut self, key: K, value: V) { + self.key_to_value.insert(key.clone(), value.clone()); + self.value_to_key.insert(value, key); + } + + pub fn get_by_key(&self, key: &K) -> Option<&V> { + self.key_to_value.get(key) + } + + pub fn get_by_value(&self, value: &V) -> Option<&K> { + self.value_to_key.get(value) + } + + pub fn remove_by_key(&mut self, key: &K) -> Option { + if let Some(value) = self.key_to_value.remove(key) { + self.value_to_key.remove(&value); + Some(value) + } else { + None + } + } + + pub fn remove_by_value(&mut self, value: &V) -> Option { + if let Some(key) = self.value_to_key.remove(value) { + self.key_to_value.remove(&key); + Some(key) + } else { + None + } + } + + pub fn len(&self) -> usize { + self.key_to_value.len() + } + + pub fn is_empty(&self) -> bool { + self.key_to_value.is_empty() + } + + pub fn clear(&mut self) { + self.key_to_value.clear(); + self.value_to_key.clear(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_contains_key() { + let mut map = DualMap::new(); + map.insert(1, "a"); + assert!(map.contains_key(&1)); + assert!(!map.contains_key(&2)); + } + + #[test] + fn test_contains_value() { + let mut map = DualMap::new(); + map.insert(1, "a"); + assert!(map.contains_value(&"a")); + assert!(!map.contains_value(&"b")); + } + + #[test] + fn test_into_hashmap() { + let mut map = DualMap::new(); + map.insert(1, "a"); + map.insert(2, "b"); + let hashmap = map.into_hashmap(); + assert_eq!(hashmap.get(&1), Some(&"a")); + assert_eq!(hashmap.get(&2), Some(&"b")); + } + + #[test] + fn test_insert_and_get() { + let mut map = DualMap::new(); + map.insert(1, "a"); + assert_eq!(map.get_by_key(&1), Some(&"a")); + assert_eq!(map.get_by_value(&"a"), Some(&1)); + } + + #[test] + fn test_remove_by_key() { + let mut map = DualMap::new(); + map.insert(1, "a"); + assert_eq!(map.remove_by_key(&1), Some("a")); + assert_eq!(map.get_by_key(&1), None); + assert_eq!(map.get_by_value(&"a"), None); + } + + #[test] + fn test_remove_by_value() { + let mut map = DualMap::new(); + map.insert(1, "a"); + assert_eq!(map.remove_by_value(&"a"), Some(1)); + assert_eq!(map.get_by_key(&1), None); + assert_eq!(map.get_by_value(&"a"), None); + } + + #[test] + fn test_len_and_is_empty() { + let mut map = DualMap::new(); + assert_eq!(map.len(), 0); + assert!(map.is_empty()); + + map.insert(1, "a"); + assert_eq!(map.len(), 1); + assert!(!map.is_empty()); + + map.remove_by_key(&1); + assert_eq!(map.len(), 0); + assert!(map.is_empty()); + } + + #[test] + fn test_clear() { + let mut map = DualMap::new(); + map.insert(1, "a"); + map.insert(2, "b"); + map.clear(); + assert_eq!(map.len(), 0); + assert!(map.is_empty()); + assert_eq!(map.get_by_key(&1), None); + assert_eq!(map.get_by_key(&2), None); + assert_eq!(map.get_by_value(&"a"), None); + assert_eq!(map.get_by_value(&"b"), None); + } +} diff --git a/rita_exit/src/database/ipddr_assignment.rs b/rita_exit/src/database/ipddr_assignment.rs index 5996ba615..2550faec0 100644 --- a/rita_exit/src/database/ipddr_assignment.rs +++ b/rita_exit/src/database/ipddr_assignment.rs @@ -1,6 +1,10 @@ +use super::dualmap::DualMap; +use crate::database::get_exit_info; +use crate::RitaExitError; use althea_kernel_interface::ExitClient; use althea_types::{ExitClientDetails, ExitClientIdentity, ExitState, Identity, WgKey}; use ipnetwork::{IpNetwork, Ipv4Network, Ipv6Network}; +use settings::exit::{ExitIpv4RoutingSettings, ExitIpv6RoutingSettings}; use settings::get_rita_exit; use std::collections::hash_map::DefaultHasher; use std::collections::{HashMap, HashSet}; @@ -8,40 +12,70 @@ use std::fmt::Write; use std::hash::{Hash, Hasher}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; -use crate::database::get_exit_info; -use crate::RitaExitError; - /// Wg exit port on client side 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; -// Default Subnet size assigned to each client -pub const DEFAULT_CLIENT_SUBNET_SIZE: u8 = 56; - -#[derive(Clone, Debug, Default)] +/// The biggest responsibility of the exit is to map user traffic to the internet. This struct keeps track +/// of the key data around internal and external ipv4 and ipv6 assignments. Across the various modes that +/// exits support. +#[derive(Clone, Debug)] pub struct ClientListAnIpAssignmentMap { - ipv6_assignments: HashMap, - internal_ip_assignments: HashMap, + /// Settings for ipv4 assignment, this includes the internal subnet as well as external nat settings + ipv4_assignment_settings: ExitIpv4RoutingSettings, + /// Settings for ipv6 assignment, fewer options than ipv4 as ipv6 traffic is never natted, can be none + /// if the exit doesn't support ipv6 + ipv6_assignment_settings: Option, + /// A map of ipv6 subnets assigned to clients, these are used both internally and externally since + /// there's no address translation, meaning the traffic maintains the same ip from the client device + /// all the way to the internet + ipv6_assignments: DualMap, + /// A map of ipv4 addresses assigned to clients, these are used internally for the wg_exit tunnel + /// and never external, the external ip is determined by the exit's nat settings + internal_ip_assignments: DualMap, + /// The external ip for a specific client or set of clients depending on the ipv4 nat mode. Under CGNAT + /// each ip will have multiple fixed clients, under SNAT each ip will have one client + external_ip_assignemnts: HashMap>, + /// A set of all clients that have been registered with the exit registered_clients: HashSet, } impl ClientListAnIpAssignmentMap { - pub fn new(clients: HashSet) -> Self { + pub fn new( + clients: HashSet, + ipv6_settings: Option, + ipv4_settings: ExitIpv4RoutingSettings, + ) -> Self { ClientListAnIpAssignmentMap { - ipv6_assignments: HashMap::new(), - internal_ip_assignments: HashMap::new(), + ipv6_assignments: DualMap::new(), + internal_ip_assignments: DualMap::new(), + external_ip_assignemnts: HashMap::new(), registered_clients: clients, + ipv4_assignment_settings: ipv4_settings, + ipv6_assignment_settings: ipv6_settings, } } + pub fn get_external_ip_assignments(&self) -> &HashMap> { + &self.external_ip_assignemnts + } + + pub fn get_ipv4_settings(&self) -> &ExitIpv4RoutingSettings { + &self.ipv4_assignment_settings + } + + pub fn get_ipv6_settings(&self) -> Option { + self.ipv6_assignment_settings.clone() + } + pub fn get_ipv6_assignments(&self) -> HashMap { - self.ipv6_assignments.clone() + self.ipv6_assignments.clone().into_hashmap() } pub fn get_internal_ip_assignments(&self) -> HashMap { - self.internal_ip_assignments.clone() + self.internal_ip_assignments.clone().into_hashmap() } pub fn is_client_registered(&self, client: Identity) -> bool { @@ -106,12 +140,7 @@ impl ClientListAnIpAssignmentMap { internal_netmask, own_internal_ip, )?; - let current_internet_ipv6 = self.get_or_add_client_ipv6( - client.global, - exit_network.get_ipv6_subnet_alt(), - exit.get_client_subnet_size() - .unwrap_or(DEFAULT_CLIENT_SUBNET_SIZE), - )?; + let current_internet_ipv6 = self.get_or_add_client_ipv6(client.global)?; let current_internet_ipv6: Option = current_internet_ipv6.map(|a| a.into()); @@ -129,6 +158,144 @@ impl ClientListAnIpAssignmentMap { } } + /// Gets the clients external ipv4 ip depending on the nat mode of the exit + pub fn get_or_add_client_external_ip( + &mut self, + their_record: Identity, + ) -> Result, Box> { + match &self.ipv4_assignment_settings { + ExitIpv4RoutingSettings::NAT => { + // If we are in NAT mode, we don't assign external ips to clients + // they will use the ip assigned to the exit + Ok(None) + } + ExitIpv4RoutingSettings::CGNAT { + subnet, + static_assignments, + } => { + // check static assignmetns first + for id in static_assignments { + if their_record == id.client_id { + // make sure we have assigned this clients external ip. in CGNAT mode static clients just get + // the same ip every time, they don't get that ip exclusively assigned to them, so adding to this + // list is mostly a way to load balance the clients across the available ips including any static assignments + // in that count. + match self.external_ip_assignemnts.get_mut(&id.client_external_ip) { + Some(clients) => { + clients.insert(their_record); + } + None => { + let mut new_clients = HashSet::new(); + new_clients.insert(their_record); + self.external_ip_assignemnts + .insert(id.client_external_ip, new_clients); + } + } + + return Ok(Some(id.client_external_ip)); + } + } + + // check for already assigned ips + for (ip, clients) in self.external_ip_assignemnts.iter() { + if clients.contains(&their_record) { + return Ok(Some(*ip)); + } + } + + // if we don't have a static assignment, we need to assign an ip, we should pick the ip with the fewest clients + // note this code is designed for relatively small subnets, but since public ipv4 are so valuable it's improbable + // anyone with a /8 is going to show up and use this. + let possible_ips: Vec = subnet.into_iter().collect(); + + let mut target_ip = None; + let mut last_num_assigned = usize::MAX; + for ip in possible_ips { + match self.external_ip_assignemnts.get(&ip) { + Some(clients) => { + if clients.len() < last_num_assigned { + target_ip = Some(ip); + last_num_assigned = clients.len(); + } + } + None => { + target_ip = Some(ip); + // may as well break here, it's impossible to do better than an ip unused + // by any other clients + break; + } + } + } + + // finally we add the newly assigned ip to the list of clients + let target_ip = target_ip.unwrap(); + match self.external_ip_assignemnts.get_mut(&target_ip) { + Some(clients) => { + clients.insert(their_record); + } + None => { + let mut new_clients = HashSet::new(); + new_clients.insert(their_record); + self.external_ip_assignemnts.insert(target_ip, new_clients); + } + } + + Ok(Some(target_ip)) + } + ExitIpv4RoutingSettings::SNAT { + subnet, + static_assignments, + } => { + // unlike in CGNAT mode, in SNAT mode we assign clients an ip and they are exclusively assigned that ip + // so we need to make sure the static ip assignments are handled first by building the full list + for id in static_assignments { + // duplicate static assignments are a configuration error + assert!(!self + .external_ip_assignemnts + .contains_key(&id.client_external_ip)); + let mut new_clients = HashSet::new(); + new_clients.insert(their_record); + self.external_ip_assignemnts + .insert(id.client_external_ip, new_clients); + } + + // check for already assigned ips + for (ip, clients) in self.external_ip_assignemnts.iter() { + if clients.contains(&their_record) { + return Ok(Some(*ip)); + } + } + + // if we don't have a static assignment, we need to find an open ip and assign it + let possible_ips: Vec = subnet.into_iter().collect(); + + let mut target_ip = None; + for ip in possible_ips { + if !self.external_ip_assignemnts.contains_key(&ip) { + target_ip = Some(ip); + // may as well break here, it's impossible to do better than an ip unused + // by any other clients + break; + } + } + + match target_ip { + Some(ip) => { + // since this is SNAT we never have to deal with multiple clients on the same ip + let mut new_clients = HashSet::new(); + new_clients.insert(their_record); + self.external_ip_assignemnts.insert(ip, new_clients); + Ok(Some(ip)) + } + None => { + // we have exhausted all available ips, we can't assign this client an ip + Err(Box::new(RitaExitError::IpExhaustionError)) + } + } + } + } + } + /// Given a client identity, get the clients internal ipv4 addr using the wgkey as a generative seed /// this is the ip used for the wg_exit tunnel for the client. Not the clients public ip visible to the internet /// which is determined by the NAT settings on the exit @@ -152,12 +319,8 @@ impl ClientListAnIpAssignmentMap { } }; - // check if we already have an ip for this client, TODO optimize this datastructure, it's optimized for generating - // new ip, not for lookup, the generation process can be streamlined to avoid that. - for (ip, id) in self.internal_ip_assignments.iter() { - if *id == their_record { - return Ok(*ip); - } + if let Some(val) = self.internal_ip_assignments.get_by_value(&their_record) { + return Ok(*val); } // Keep trying to generate an address till we get a valid one @@ -206,20 +369,18 @@ impl ClientListAnIpAssignmentMap { pub fn get_or_add_client_ipv6( &mut self, their_record: Identity, - exit_sub: Option, - client_subnet_size: u8, ) -> Result, Box> { - // check if we already have an ip for this client, TODO optimize this datastructure, it's optimized for generating - // new ip, not for lookup, the generation process can be streamlined to avoid that. - for (ip, id) in self.ipv6_assignments.iter() { - if *id == their_record { - return Ok(Some(*ip)); - } + if let Some(val) = self.ipv6_assignments.get_by_value(&their_record) { + return Ok(Some(*val)); } - if let Some(exit_sub) = exit_sub { + if let Some(ipv6_settings) = self.get_ipv6_settings() { + let exit_sub = ipv6_settings.subnet; + let client_subnet_size = ipv6_settings.client_subnet_size; let wg_hash = hash_wgkey(their_record.wg_public_key); + // if you hit this check your subnet size is too small to assign a single client, what gives? + assert!(client_subnet_size >= exit_sub.prefix()); // This bitshifting is the total number of client subnets available. We are checking that our iterative index // 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 @@ -265,13 +426,7 @@ impl ClientListAnIpAssignmentMap { &mut self, client: Identity, ) -> Result> { - let internet_ipv6 = self.get_or_add_client_ipv6( - client, - settings::get_rita_exit().exit_network.get_ipv6_subnet_alt(), - settings::get_rita_exit() - .get_client_subnet_size() - .unwrap_or(DEFAULT_CLIENT_SUBNET_SIZE), - )?; + let internet_ipv6 = self.get_or_add_client_ipv6(client)?; let internal_ip = self.get_or_add_client_internal_ip( client, settings::get_rita_exit() @@ -298,26 +453,18 @@ impl ClientListAnIpAssignmentMap { /// Take an index i, a larger subnet and a smaller subnet length and generate the ith smaller subnet in the larger subnet /// For instance, if our larger subnet is fd00::1330/120, smaller sub len is 124, and index is 1, our generated subnet would be fd00::1310/124 pub fn generate_iterative_client_subnet( - exit_sub: IpNetwork, + exit_sub: Ipv6Network, ind: u64, subprefix: u8, ) -> Result> { - let net; - // Covert the subnet's ip address into a u128 integer to allow for easy iterative // addition operations. To this u128, we add (interative_index * client_subnet_size) // and convert this result into an ipv6 addr. This is the starting ip in the client subnet // // For example, if we have exit subnet: fbad::1000/120, client subnet size is 124, index is 1 // we do (fbad::1000).to_int() + (16 * 1) = fbad::1010/124 is the client subnet - let net_as_int: u128 = if let IpAddr::V6(addr) = exit_sub.network() { - net = Ipv6Network::new(addr, subprefix).unwrap(); - addr.into() - } else { - return Err(Box::new(RitaExitError::MiscStringError( - "Exit subnet expected to be ipv6!!".to_string(), - ))); - }; + let net = Ipv6Network::new(exit_sub.network(), subprefix).unwrap(); + let net_as_int: u128 = net.network().into(); if subprefix < exit_sub.prefix() { return Err(Box::new(RitaExitError::MiscStringError( @@ -373,18 +520,21 @@ mod tests { database::ipddr_assignment::generate_iterative_client_subnet, ClientListAnIpAssignmentMap, }; use althea_types::Identity; - use ipnetwork::{IpNetwork, Ipv6Network}; + use ipnetwork::Ipv6Network; + use settings::exit::{ExitIpv4RoutingSettings, ExitIpv6RoutingSettings}; use std::collections::HashSet; - pub fn get_test_data() -> ClientListAnIpAssignmentMap { + pub fn get_test_config_nat() -> ClientListAnIpAssignmentMap { let clients = HashSet::new(); - ClientListAnIpAssignmentMap::new(clients) + let ipv6_settings = + ExitIpv6RoutingSettings::new("2602:FBAD:10::/32".parse().unwrap(), 64, vec![]); + let ipv4_settings = ExitIpv4RoutingSettings::NAT; + ClientListAnIpAssignmentMap::new(clients, Some(ipv6_settings), ipv4_settings) } #[test] fn test_internet_ipv6_assignment() { - let mut data = get_test_data(); - let exit_sub = Some("2602:FBAD:10::/126".parse().unwrap()); + let mut data = get_test_config_nat(); let dummy_client = Identity { mesh_ip: "fd00::1337".parse().unwrap(), eth_address: "0x4Af6D4125f3CBF07EBAD056E2eCa7b17c58AFEa4" @@ -397,20 +547,14 @@ mod tests { }; // Generate a client subnet - let ip = data - .get_or_add_client_ipv6(dummy_client, exit_sub, 128) - .unwrap() - .unwrap(); + let ip = data.get_or_add_client_ipv6(dummy_client).unwrap().unwrap(); // Verify assignement db is correctly populated assert!(data.get_ipv6_assignments().len() == 1); assert_eq!(*data.get_ipv6_assignments().get(&ip).unwrap(), dummy_client); // Try retrieving the same client - let ip_2 = data - .get_or_add_client_ipv6(dummy_client, exit_sub, 128) - .unwrap() - .unwrap(); + let ip_2 = data.get_or_add_client_ipv6(dummy_client).unwrap().unwrap(); assert_eq!(ip, ip_2); // Make sure no new entries in assignemnt db @@ -433,7 +577,7 @@ mod tests { // Generate a client subnet let ip = data - .get_or_add_client_ipv6(dummy_client_2, exit_sub, 128) + .get_or_add_client_ipv6(dummy_client_2) .unwrap() .unwrap(); @@ -445,7 +589,7 @@ mod tests { ); let ip_2 = data - .get_or_add_client_ipv6(dummy_client_2, exit_sub, 128) + .get_or_add_client_ipv6(dummy_client_2) .unwrap() .unwrap(); assert_eq!(ip, ip_2); @@ -473,7 +617,7 @@ mod tests { // Generate a client subnet let ip = data - .get_or_add_client_ipv6(dummy_client_3, exit_sub, 128) + .get_or_add_client_ipv6(dummy_client_3) .unwrap() .unwrap(); @@ -485,11 +629,11 @@ mod tests { ); let _ = data - .get_or_add_client_ipv6(dummy_client_2, exit_sub, 128) + .get_or_add_client_ipv6(dummy_client_2) .unwrap() .unwrap(); let ip_2 = data - .get_or_add_client_ipv6(dummy_client_3, exit_sub, 128) + .get_or_add_client_ipv6(dummy_client_3) .unwrap() .unwrap(); assert_eq!(ip, ip_2); @@ -528,7 +672,7 @@ mod tests { #[test] fn test_internal_ip_assignment() { - let mut data = get_test_data(); + let mut data = get_test_config_nat(); let dummy_client = Identity { mesh_ip: "fd00::1337".parse().unwrap(), eth_address: "0x4Af6D4125f3CBF07EBAD056E2eCa7b17c58AFEa4" @@ -608,57 +752,50 @@ mod tests { #[test] fn test_generate_iterative_subnet() { // Complex subnet example - let net: IpNetwork = "2602:FBAD::/40".parse().unwrap(); + let net: Ipv6Network = "2602:FBAD::/40".parse().unwrap(); let ret = generate_iterative_client_subnet(net, 0, 64); assert_eq!( "2602:FBAD::/64".parse::().unwrap(), ret.unwrap() ); - let net: IpNetwork = "2602:FBAD::/40".parse().unwrap(); let ret = generate_iterative_client_subnet(net, 1, 64); assert_eq!( "2602:FBAD:0:1::/64".parse::().unwrap(), ret.unwrap() ); - let net: IpNetwork = "2602:FBAD::/40".parse().unwrap(); let ret = generate_iterative_client_subnet(net, 50, 64); assert_eq!( "2602:FBAD:0:32::/64".parse::().unwrap(), ret.unwrap() ); - let net: IpNetwork = "2602:FBAD::/40".parse().unwrap(); let ret = generate_iterative_client_subnet(net, 2_u64.pow(24), 64); assert!(ret.is_err()); - let net: IpNetwork = "2602:FBAD::/40".parse().unwrap(); let ret = generate_iterative_client_subnet(net, 0, 30); assert!(ret.is_err()); // Simple subnet example - let net: IpNetwork = "fd00::1337/120".parse().unwrap(); + let net: Ipv6Network = "fd00::1337/120".parse().unwrap(); let ret = generate_iterative_client_subnet(net, 0, 124); assert_eq!( "fd00::1300/124".parse::().unwrap(), ret.unwrap() ); - let net: IpNetwork = "fd00::1337/120".parse().unwrap(); let ret = generate_iterative_client_subnet(net, 2, 124); assert_eq!( "fd00::1320/124".parse::().unwrap(), ret.unwrap() ); - let net: IpNetwork = "fd00::1337/120".parse().unwrap(); let ret = generate_iterative_client_subnet(net, 15, 124); assert_eq!( "fd00::13f0/124".parse::().unwrap(), ret.unwrap() ); - let net: IpNetwork = "fd00::1337/120".parse().unwrap(); let ret = generate_iterative_client_subnet(net, 16, 124); assert!(ret.is_err()); } diff --git a/rita_exit/src/database/mod.rs b/rita_exit/src/database/mod.rs index 6aa4c59a3..65c497875 100644 --- a/rita_exit/src/database/mod.rs +++ b/rita_exit/src/database/mod.rs @@ -5,7 +5,6 @@ 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::ipddr_assignment::display_hashset; -use crate::database::ipddr_assignment::DEFAULT_CLIENT_SUBNET_SIZE; use crate::rita_loop::RitaExitData; use crate::rita_loop::EXIT_INTERFACE; use crate::rita_loop::EXIT_LOOP_TIMEOUT; @@ -44,6 +43,7 @@ use std::time::Duration; use std::time::Instant; use std::time::SystemTime; +pub mod dualmap; pub mod geoip; pub mod ipddr_assignment; @@ -667,13 +667,8 @@ pub fn enforce_exit_clients(client_data: &mut RitaExitData) -> Result<(), Box write!(f, "{a}",), RitaExitError::KernelInterfaceError(a) => write!(f, "{a}",), RitaExitError::NoClientError => write!(f, "This client has not registered yet!"), + RitaExitError::IpExhaustionError => write!(f, "No more external IPs available!"), } } } diff --git a/rita_exit/src/rita_loop/mod.rs b/rita_exit/src/rita_loop/mod.rs index 5d827793a..bb1242bee 100644 --- a/rita_exit/src/rita_loop/mod.rs +++ b/rita_exit/src/rita_loop/mod.rs @@ -26,7 +26,7 @@ use althea_types::{Identity, SignedExitServerList, WgKey}; use babel_monitor::{open_babel_stream, parse_routes}; use clarity::Address; use exit_trust_root::client_db::get_all_registered_clients; -use ipnetwork::{IpNetwork, Ipv6Network}; +use ipnetwork::Ipv6Network; use rita_common::debt_keeper::DebtAction; use rita_common::rita_loop::get_web3_server; use settings::exit::EXIT_LIST_PORT; @@ -132,13 +132,11 @@ impl RitaExitData { pub fn get_or_add_client_ipv6( &self, their_record: Identity, - exit_sub: Option, - client_subnet_size: u8, ) -> Result, Box> { self.client_list_and_ip_assignments .write() .unwrap() - .get_or_add_client_ipv6(their_record, exit_sub, client_subnet_size) + .get_or_add_client_ipv6(their_record) } pub fn get_setup_states(&self) -> ExitClientSetupStates { diff --git a/settings/src/error.rs b/settings/src/error.rs index 0cb40c74c..2d40f39de 100644 --- a/settings/src/error.rs +++ b/settings/src/error.rs @@ -11,6 +11,8 @@ pub enum SettingsError { IpNetworkError(ipnetwork::IpNetworkError), SerdeJsonError(serde_json::Error), FileNotFoundError(String), + InvalidIpv6Configuration(String), + InvalidIpv4Configuration(String), } impl From for SettingsError { @@ -50,6 +52,12 @@ impl Display for SettingsError { SettingsError::FileNotFoundError(e) => { write!(f, "Could not find config file at path {}", e) } + SettingsError::InvalidIpv6Configuration(e) => { + write!(f, "Invalid IPv6 configuration: {}", e) + } + SettingsError::InvalidIpv4Configuration(e) => { + write!(f, "Invalid IPv4 configuration: {}", e) + } } } } diff --git a/settings/src/exit.rs b/settings/src/exit.rs index 31e7bf2c4..066bf4815 100644 --- a/settings/src/exit.rs +++ b/settings/src/exit.rs @@ -54,12 +54,65 @@ pub enum ExitIpv4RoutingSettings { /// if the subnet is too small and too many clients connect there will be no more addresses to assign. Once that happens /// the exit will stop accepting new connections until a client disconnects. Be mindful, in cases where a client can not /// find another exit to connect to they will be unable to access the internet. - FLAT { + SNAT { subnet: Ipv4Network, static_assignments: Vec, }, } +impl ExitIpv4RoutingSettings { + pub fn validate(&self) -> Result<(), SettingsError> { + match self { + ExitIpv4RoutingSettings::NAT => Ok(()), + ExitIpv4RoutingSettings::CGNAT { + subnet, + static_assignments, + } => { + for assignment in static_assignments { + if !subnet.contains(assignment.client_external_ip) { + return Err(SettingsError::InvalidIpv4Configuration( + "Static assignment outside of subnet".to_string(), + )); + } + } + if static_assignments.len() as u32 > subnet.size() { + return Err(SettingsError::InvalidIpv4Configuration( + "Not enough addresses in subnet for static assignments".to_string(), + )); + } + + Ok(()) + } + ExitIpv4RoutingSettings::SNAT { + static_assignments, + subnet, + } => { + let mut used_ips = HashSet::new(); + for assignment in static_assignments { + if used_ips.contains(&assignment.client_external_ip) { + return Err(SettingsError::InvalidIpv4Configuration( + "Duplicate static assignment".to_string(), + )); + } + if !subnet.contains(assignment.client_external_ip) { + return Err(SettingsError::InvalidIpv4Configuration( + "Static assignment outside of subnet".to_string(), + )); + } + used_ips.insert(assignment.client_external_ip); + } + if static_assignments.len() as u32 > subnet.size() { + return Err(SettingsError::InvalidIpv4Configuration( + "Not enough addresses in subnet for static assignments".to_string(), + )); + } + + Ok(()) + } + } + } +} + /// This struct describes the settings for ipv6 routing out of the exit and assignment to clients /// the only knob here is the subnet size, which is the size of the subnet assigned to each client #[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] @@ -68,11 +121,33 @@ pub struct ExitIpv6RoutingSettings { pub client_subnet_size: u8, pub static_assignments: Vec, } +impl ExitIpv6RoutingSettings { + pub fn new( + subnet: Ipv6Network, + client_subnet_size: u8, + static_assignments: Vec, + ) -> Self { + ExitIpv6RoutingSettings { + subnet, + client_subnet_size, + static_assignments, + } + } +} impl ExitIpv6RoutingSettings { pub fn spit_ip_prefix(&self) -> (IpAddr, u8) { (self.subnet.ip().into(), self.subnet.prefix()) } + + pub fn validate(&self) -> Result<(), SettingsError> { + if self.client_subnet_size > self.subnet.prefix() { + return Err(SettingsError::InvalidIpv6Configuration( + "Client subnet size is larger than the exit subnet".to_string(), + )); + } + Ok(()) + } } /// The settings for the exit's internal ipv4 network, this is the internal subnet that the exit uses to @@ -183,6 +258,14 @@ impl ExitNetworkSettings { ipv6_routing: None, } } + + pub fn validate(&self) -> bool { + let ipv6_status = match self.ipv6_routing { + Some(ref x) => x.validate().is_ok(), + None => true, + }; + ipv6_status && self.ipv4_routing.validate().is_ok() + } } fn default_remote_log() -> bool { @@ -223,7 +306,7 @@ pub struct RitaExitSettingsStruct { impl RitaExitSettingsStruct { /// Returns true if the settings are valid pub fn validate(&self) -> bool { - self.payment.validate() + self.payment.validate() && self.exit_network.validate() } /// Generates a configuration that can be used in integration tests, does not use the