diff --git a/veilid-core/src/routing_table/route_spec_store/mod.rs b/veilid-core/src/routing_table/route_spec_store/mod.rs index 2538f441..529c2d10 100644 --- a/veilid-core/src/routing_table/route_spec_store/mod.rs +++ b/veilid-core/src/routing_table/route_spec_store/mod.rs @@ -175,9 +175,7 @@ impl RouteSpecStore { pub fn allocate_route( &self, crypto_kinds: &[CryptoKind], - stability: Stability, - sequencing: Sequencing, - hop_count: usize, + safety_spec: &SafetySpec, directions: DirectionSet, avoid_nodes: &[TypedKey], automatic: bool, @@ -190,9 +188,7 @@ impl RouteSpecStore { inner, rti, crypto_kinds, - stability, - sequencing, - hop_count, + safety_spec, directions, avoid_nodes, automatic, @@ -206,15 +202,17 @@ impl RouteSpecStore { inner: &mut RouteSpecStoreInner, rti: &mut RoutingTableInner, crypto_kinds: &[CryptoKind], - stability: Stability, - sequencing: Sequencing, - hop_count: usize, + safety_spec: &SafetySpec, directions: DirectionSet, avoid_nodes: &[TypedKey], automatic: bool, ) -> VeilidAPIResult { use core::cmp::Ordering; + if safety_spec.preferred_route.is_some() { + apibail_generic!("safety_spec.preferred_route must be empty when allocating new route"); + } + let ip6_prefix_size = rti .unlocked_inner .config @@ -222,19 +220,19 @@ impl RouteSpecStore { .network .max_connections_per_ip6_prefix_size as usize; - if hop_count < 1 { + if safety_spec.hop_count < 1 { apibail_invalid_argument!( "Not allocating route less than one hop in length", "hop_count", - hop_count + safety_spec.hop_count ); } - if hop_count > self.unlocked_inner.max_route_hop_count { + if safety_spec.hop_count > self.unlocked_inner.max_route_hop_count { apibail_invalid_argument!( "Not allocating route longer than max route hop count", "hop_count", - hop_count + safety_spec.hop_count ); } @@ -291,6 +289,83 @@ impl RouteSpecStore { return false; }; + // Exclude nodes from blacklisted countries + #[cfg(feature = "geolocation")] + { + let country_code_denylist = self + .unlocked_inner + .routing_table + .config + .get() + .network + .privacy + .country_code_denylist + .clone(); + + if !country_code_denylist.is_empty() { + let geolocation_info = + sni.get_geolocation_info(RoutingDomain::PublicInternet); + + // Since denylist is used, consider nodes with unknown countries to be automatically + // excluded as well + if geolocation_info.country_code().is_none() { + log_rtab!( + debug "allocate_route_inner: skipping node {} from unknown country", + e.best_node_id() + ); + return false; + } + // Same thing applies to relays used by the node + if geolocation_info + .relay_country_codes() + .iter() + .any(Option::is_none) + { + log_rtab!( + debug "allocate_route_inner: skipping node {} using relay from unknown country", + e.best_node_id() + ); + return false; + } + + // Ensure that node is not excluded + // Safe to unwrap here, checked above + if country_code_denylist.contains(&geolocation_info.country_code().unwrap()) + { + log_rtab!( + debug "allocate_route_inner: skipping node {} from excluded country {}", + e.best_node_id(), + geolocation_info.country_code().unwrap() + ); + return false; + } + + // Ensure that node relays are not excluded + // Safe to unwrap here, checked above + if geolocation_info + .relay_country_codes() + .iter() + .cloned() + .map(Option::unwrap) + .any(|cc| country_code_denylist.contains(&cc)) + { + log_rtab!( + debug "allocate_route_inner: skipping node {} using relay from excluded country {:?}", + e.best_node_id(), + geolocation_info + .relay_country_codes() + .iter() + .cloned() + .map(Option::unwrap) + .filter(|cc| country_code_denylist.contains(&cc)) + .next() + .unwrap() + ); + return false; + } + } + } + // Exclude nodes on our same ipblock, or their relay is on our same ipblock // or our relay is on their ipblock, or their relay is on our relays same ipblock @@ -350,7 +425,7 @@ impl RouteSpecStore { entry.with_inner(|e| { e.signed_node_info(RoutingDomain::PublicInternet) .map(|sni| { - sni.has_sequencing_matched_dial_info(sequencing) + sni.has_sequencing_matched_dial_info(safety_spec.sequencing) && sni.node_info().has_capability(CAP_ROUTE) }) .unwrap_or(false) @@ -387,16 +462,16 @@ impl RouteSpecStore { // apply sequencing preference // ensureordered will be taken care of by filter // and nopreference doesn't care - if matches!(sequencing, Sequencing::PreferOrdered) { + if matches!(safety_spec.sequencing, Sequencing::PreferOrdered) { let cmp_seq = entry1.with_inner(|e1| { entry2.with_inner(|e2| { let e1_can_do_ordered = e1 .signed_node_info(RoutingDomain::PublicInternet) - .map(|sni| sni.has_sequencing_matched_dial_info(sequencing)) + .map(|sni| sni.has_sequencing_matched_dial_info(safety_spec.sequencing)) .unwrap_or(false); let e2_can_do_ordered = e2 .signed_node_info(RoutingDomain::PublicInternet) - .map(|sni| sni.has_sequencing_matched_dial_info(sequencing)) + .map(|sni| sni.has_sequencing_matched_dial_info(safety_spec.sequencing)) .unwrap_or(false); // Reverse this comparison because ordered is preferable (less) e2_can_do_ordered.cmp(&e1_can_do_ordered) @@ -410,7 +485,7 @@ impl RouteSpecStore { // apply stability preference // always prioritize reliable nodes, but sort by oldest or fastest entry1.with_inner(|e1| { - entry2.with_inner(|e2| match stability { + entry2.with_inner(|e2| match safety_spec.stability { Stability::LowLatency => BucketEntryInner::cmp_fastest_reliable(cur_ts, e1, e2), Stability::Reliable => BucketEntryInner::cmp_oldest_reliable(cur_ts, e1, e2), }) @@ -427,7 +502,7 @@ impl RouteSpecStore { rti.find_peers_with_sort_and_filter(usize::MAX, cur_ts, filters, compare, transform); // If we couldn't find enough nodes, wait until we have more nodes in the routing table - if nodes.len() < hop_count { + if nodes.len() < safety_spec.hop_count { apibail_try_again!("not enough nodes to construct route at this time"); } @@ -498,7 +573,7 @@ impl RouteSpecStore { previous_node.clone(), current_node.clone(), DialInfoFilter::all(), - sequencing, + safety_spec.sequencing, None, ); if matches!(cm, ContactMethod::Unreachable) { @@ -537,7 +612,7 @@ impl RouteSpecStore { next_node.clone(), current_node.clone(), DialInfoFilter::all(), - sequencing, + safety_spec.sequencing, None, ); if matches!(cm, ContactMethod::Unreachable) { @@ -573,9 +648,11 @@ impl RouteSpecStore { let mut route_nodes: Vec = Vec::new(); let mut can_do_sequenced: bool = true; - for start in 0..(nodes.len() - hop_count) { + for start in 0..(nodes.len() - safety_spec.hop_count) { // Try the permutations available starting with 'start' - if let Some((rn, cds)) = with_route_permutations(hop_count, start, &mut perm_func) { + if let Some((rn, cds)) = + with_route_permutations(safety_spec.hop_count, start, &mut perm_func) + { route_nodes = rn; can_do_sequenced = cds; break; @@ -625,7 +702,7 @@ impl RouteSpecStore { route_set, hop_node_refs, directions, - stability, + safety_spec.stability, can_do_sequenced, automatic, ); @@ -1333,9 +1410,7 @@ impl RouteSpecStore { inner, rti, &[crypto_kind], - safety_spec.stability, - safety_spec.sequencing, - safety_spec.hop_count, + safety_spec, direction, avoid_nodes, true, diff --git a/veilid-core/src/routing_table/tasks/private_route_management.rs b/veilid-core/src/routing_table/tasks/private_route_management.rs index 5ce6a5c1..4b708b80 100644 --- a/veilid-core/src/routing_table/tasks/private_route_management.rs +++ b/veilid-core/src/routing_table/tasks/private_route_management.rs @@ -207,11 +207,15 @@ impl RoutingTable { for _n in 0..routes_to_allocate { // Parameters here must be the most inclusive safety route spec // These will be used by test_remote_route as well + let safety_spec = SafetySpec { + preferred_route: None, + hop_count: default_route_hop_count, + stability: Stability::Reliable, + sequencing: Sequencing::PreferOrdered, + }; match rss.allocate_route( &VALID_CRYPTO_KINDS, - Stability::Reliable, - Sequencing::PreferOrdered, - default_route_hop_count, + &safety_spec, DirectionSet::all(), &[], true, diff --git a/veilid-core/src/rpc_processor/rpc_route.rs b/veilid-core/src/rpc_processor/rpc_route.rs index 2380b7b3..d061207d 100644 --- a/veilid-core/src/rpc_processor/rpc_route.rs +++ b/veilid-core/src/rpc_processor/rpc_route.rs @@ -172,6 +172,7 @@ impl RPCProcessor { // Ensure the route is validated, and construct a return safetyspec that matches the inbound preferences let rss = self.routing_table().route_spec_store(); let preferred_route = rss.get_route_id_for_key(&pr_pubkey.value); + let Some((secret_key, safety_spec)) = rss.with_signature_validated_route( &pr_pubkey, routed_operation.signatures(), diff --git a/veilid-core/src/tests/common/test_veilid_config.rs b/veilid-core/src/tests/common/test_veilid_config.rs index e6f9dff3..dbabbf69 100644 --- a/veilid-core/src/tests/common/test_veilid_config.rs +++ b/veilid-core/src/tests/common/test_veilid_config.rs @@ -282,6 +282,8 @@ pub fn config_callback(key: String) -> ConfigCallbackReturn { "network.protocol.wss.listen_address" => Ok(Box::new("".to_owned())), "network.protocol.wss.path" => Ok(Box::new(String::from("ws"))), "network.protocol.wss.url" => Ok(Box::new(Option::::None)), + #[cfg(feature = "geolocation")] + "network.privacy.country_code_denylist" => Ok(Box::new(Vec::::new())), _ => { let err = format!("config key '{}' doesn't exist", key); debug!("{}", err); @@ -419,6 +421,9 @@ pub async fn test_config() { assert_eq!(inner.network.protocol.wss.listen_address, ""); assert_eq!(inner.network.protocol.wss.path, "ws"); assert_eq!(inner.network.protocol.wss.url, None); + + #[cfg(feature = "geolocation")] + assert_eq!(inner.network.privacy.country_code_denylist, Vec::new()); } pub async fn test_all() { diff --git a/veilid-core/src/veilid_api/api.rs b/veilid-core/src/veilid_api/api.rs index b116e672..77e346eb 100644 --- a/veilid-core/src/veilid_api/api.rs +++ b/veilid-core/src/veilid_api/api.rs @@ -300,16 +300,16 @@ impl VeilidAPI { c.network.rpc.default_route_hop_count.into() }; - let rss = self.routing_table()?.route_spec_store(); - let route_id = rss.allocate_route( - crypto_kinds, + let safety_spec = SafetySpec { + preferred_route: None, + hop_count: default_route_hop_count, stability, sequencing, - default_route_hop_count, - DirectionSet::all(), - &[], - false, - )?; + }; + + let rss = self.routing_table()?.route_spec_store(); + let route_id = + rss.allocate_route(crypto_kinds, &safety_spec, DirectionSet::all(), &[], false)?; match rss.test_route(route_id).await? { Some(true) => { // route tested okay diff --git a/veilid-core/src/veilid_api/debug.rs b/veilid-core/src/veilid_api/debug.rs index a6f881ed..f13841cd 100644 --- a/veilid-core/src/veilid_api/debug.rs +++ b/veilid-core/src/veilid_api/debug.rs @@ -185,6 +185,7 @@ fn get_safety_selection(routing_table: RoutingTable) -> impl Fn(&str) -> Option< sequencing = s; } } + let ss = SafetySpec { preferred_route, hop_count, @@ -1146,22 +1147,22 @@ impl VeilidAPI { ai += 1; } - // Allocate route - let out = match rss.allocate_route( - &VALID_CRYPTO_KINDS, + let safety_spec = SafetySpec { + preferred_route: None, + hop_count, stability, sequencing, - hop_count, - directions, - &[], - false, - ) { - Ok(v) => v.to_string(), - Err(e) => { - format!("Route allocation failed: {}", e) - } }; + // Allocate route + let out = + match rss.allocate_route(&VALID_CRYPTO_KINDS, &safety_spec, directions, &[], false) { + Ok(v) => v.to_string(), + Err(e) => { + format!("Route allocation failed: {}", e) + } + }; + Ok(out) } async fn debug_route_release(&self, args: Vec) -> VeilidAPIResult { diff --git a/veilid-core/src/veilid_api/tests/fixtures.rs b/veilid-core/src/veilid_api/tests/fixtures.rs index 2b93e3ae..8ba61d49 100644 --- a/veilid-core/src/veilid_api/tests/fixtures.rs +++ b/veilid-core/src/veilid_api/tests/fixtures.rs @@ -237,6 +237,10 @@ pub fn fix_veilidconfiginner() -> VeilidConfigInner { url: Some("https://veilid.com/wss".to_string()), }, }, + #[cfg(feature = "geolocation")] + privacy: VeilidConfigPrivacy { + country_code_denylist: vec![CountryCode([b'N', b'Z'])], + }, }, } } diff --git a/veilid-core/src/veilid_config.rs b/veilid-core/src/veilid_config.rs index 9eecae67..abd7a1b4 100644 --- a/veilid-core/src/veilid_config.rs +++ b/veilid-core/src/veilid_config.rs @@ -275,6 +275,28 @@ pub struct VeilidConfigProtocol { pub wss: VeilidConfigWSS, } +/// Privacy preferences for routes. +/// +/// ```yaml +/// privacy: +/// country_code_denylist: [] +/// ``` +#[cfg(feature = "geolocation")] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(target_arch = "wasm32", derive(Tsify))] +pub struct VeilidConfigPrivacy { + pub country_code_denylist: Vec, +} + +#[cfg(feature = "geolocation")] +impl Default for VeilidConfigPrivacy { + fn default() -> Self { + Self { + country_code_denylist: Vec::new(), + } + } +} + /// Configure TLS. /// /// ```yaml @@ -503,6 +525,8 @@ pub struct VeilidConfigNetwork { pub tls: VeilidConfigTLS, pub application: VeilidConfigApplication, pub protocol: VeilidConfigProtocol, + #[cfg(feature = "geolocation")] + pub privacy: VeilidConfigPrivacy, } impl Default for VeilidConfigNetwork { @@ -527,6 +551,8 @@ impl Default for VeilidConfigNetwork { tls: VeilidConfigTLS::default(), application: VeilidConfigApplication::default(), protocol: VeilidConfigProtocol::default(), + #[cfg(feature = "geolocation")] + privacy: VeilidConfigPrivacy::default(), } } } @@ -970,6 +996,8 @@ impl VeilidConfig { get_config!(inner.network.protocol.wss.listen_address); get_config!(inner.network.protocol.wss.path); get_config!(inner.network.protocol.wss.url); + #[cfg(feature = "geolocation")] + get_config!(inner.network.privacy.country_code_denylist); Ok(()) }) } diff --git a/veilid-server/Cargo.toml b/veilid-server/Cargo.toml index 8f4d3051..55e3e0f4 100644 --- a/veilid-server/Cargo.toml +++ b/veilid-server/Cargo.toml @@ -42,6 +42,8 @@ tracking = ["veilid-core/tracking"] debug-json-api = [] debug-locks = ["veilid-core/debug-locks"] +geolocation = ["veilid-core/geolocation"] + [dependencies] veilid-core = { path = "../veilid-core", default-features = false } tracing = { version = "^0.1.40", features = ["log", "attributes"] } diff --git a/veilid-server/src/settings.rs b/veilid-server/src/settings.rs index 57e677f2..501f74e3 100644 --- a/veilid-server/src/settings.rs +++ b/veilid-server/src/settings.rs @@ -32,6 +32,14 @@ lazy_static! { } pub fn load_default_config() -> EyreResult { + #[cfg(not(feature = "geolocation"))] + let privacy_section = ""; + #[cfg(feature = "geolocation")] + let privacy_section = r#" + privacy: + country_code_denylist: [] + "#; + let mut default_config = String::from( r#"--- daemon: @@ -188,6 +196,7 @@ core: listen_address: ':5150' path: 'ws' # url: '' + %PRIVACY_SECTION% "#, ) .replace( @@ -217,7 +226,8 @@ core: .replace( "%REMOTE_MAX_SUBKEY_CACHE_MEMORY_MB%", &Settings::get_default_remote_max_subkey_cache_memory_mb().to_string(), - ); + ) + .replace("%PRIVACY_SECTION%", privacy_section); let dek_password = if let Some(dek_password) = std::env::var_os("DEK_PASSWORD") { dek_password @@ -584,6 +594,12 @@ pub struct Protocol { pub wss: Wss, } +#[cfg(feature = "geolocation")] +#[derive(Debug, Deserialize, Serialize)] +pub struct Privacy { + pub country_code_denylist: Vec, +} + #[derive(Debug, Deserialize, Serialize)] pub struct Tls { pub certificate_path: String, @@ -661,6 +677,8 @@ pub struct Network { pub tls: Tls, pub application: Application, pub protocol: Protocol, + #[cfg(feature = "geolocation")] + pub privacy: Privacy, } #[derive(Debug, Deserialize, Serialize)] @@ -1164,6 +1182,8 @@ impl Settings { set_config_value!(inner.core.network.protocol.wss.listen_address, value); set_config_value!(inner.core.network.protocol.wss.path, value); set_config_value!(inner.core.network.protocol.wss.url, value); + #[cfg(feature = "geolocation")] + set_config_value!(inner.core.network.privacy.country_code_denylist, value); Err(eyre!("settings key '{key}' not found")) } @@ -1548,6 +1568,10 @@ impl Settings { .as_ref() .map(|a| a.urlstring.clone()), )), + #[cfg(feature = "geolocation")] + "network.privacy.country_code_denylist" => Ok(Box::new( + inner.core.network.privacy.country_code_denylist.clone(), + )), _ => Err(VeilidAPIError::generic(format!( "config key '{}' doesn't exist", key @@ -1788,5 +1812,7 @@ mod tests { ); assert_eq!(s.core.network.protocol.wss.url, None); // + #[cfg(feature = "geolocation")] + assert_eq!(s.core.network.privacy.country_code_denylist, &[]); } }