Merge branch 'geolocation-3' into 'main'

Country code denylist for route creation

See merge request veilid/veilid!337
This commit is contained in:
Christien Rioux 2025-01-16 14:52:24 +00:00
commit 95d61855a8
10 changed files with 197 additions and 51 deletions

View File

@ -175,9 +175,7 @@ impl RouteSpecStore {
pub fn allocate_route( pub fn allocate_route(
&self, &self,
crypto_kinds: &[CryptoKind], crypto_kinds: &[CryptoKind],
stability: Stability, safety_spec: &SafetySpec,
sequencing: Sequencing,
hop_count: usize,
directions: DirectionSet, directions: DirectionSet,
avoid_nodes: &[TypedKey], avoid_nodes: &[TypedKey],
automatic: bool, automatic: bool,
@ -190,9 +188,7 @@ impl RouteSpecStore {
inner, inner,
rti, rti,
crypto_kinds, crypto_kinds,
stability, safety_spec,
sequencing,
hop_count,
directions, directions,
avoid_nodes, avoid_nodes,
automatic, automatic,
@ -206,15 +202,17 @@ impl RouteSpecStore {
inner: &mut RouteSpecStoreInner, inner: &mut RouteSpecStoreInner,
rti: &mut RoutingTableInner, rti: &mut RoutingTableInner,
crypto_kinds: &[CryptoKind], crypto_kinds: &[CryptoKind],
stability: Stability, safety_spec: &SafetySpec,
sequencing: Sequencing,
hop_count: usize,
directions: DirectionSet, directions: DirectionSet,
avoid_nodes: &[TypedKey], avoid_nodes: &[TypedKey],
automatic: bool, automatic: bool,
) -> VeilidAPIResult<RouteId> { ) -> VeilidAPIResult<RouteId> {
use core::cmp::Ordering; 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 let ip6_prefix_size = rti
.unlocked_inner .unlocked_inner
.config .config
@ -222,19 +220,19 @@ impl RouteSpecStore {
.network .network
.max_connections_per_ip6_prefix_size as usize; .max_connections_per_ip6_prefix_size as usize;
if hop_count < 1 { if safety_spec.hop_count < 1 {
apibail_invalid_argument!( apibail_invalid_argument!(
"Not allocating route less than one hop in length", "Not allocating route less than one hop in length",
"hop_count", "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!( apibail_invalid_argument!(
"Not allocating route longer than max route hop count", "Not allocating route longer than max route hop count",
"hop_count", "hop_count",
hop_count safety_spec.hop_count
); );
} }
@ -291,6 +289,83 @@ impl RouteSpecStore {
return false; 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 // 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 // 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| { entry.with_inner(|e| {
e.signed_node_info(RoutingDomain::PublicInternet) e.signed_node_info(RoutingDomain::PublicInternet)
.map(|sni| { .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) && sni.node_info().has_capability(CAP_ROUTE)
}) })
.unwrap_or(false) .unwrap_or(false)
@ -387,16 +462,16 @@ impl RouteSpecStore {
// apply sequencing preference // apply sequencing preference
// ensureordered will be taken care of by filter // ensureordered will be taken care of by filter
// and nopreference doesn't care // and nopreference doesn't care
if matches!(sequencing, Sequencing::PreferOrdered) { if matches!(safety_spec.sequencing, Sequencing::PreferOrdered) {
let cmp_seq = entry1.with_inner(|e1| { let cmp_seq = entry1.with_inner(|e1| {
entry2.with_inner(|e2| { entry2.with_inner(|e2| {
let e1_can_do_ordered = e1 let e1_can_do_ordered = e1
.signed_node_info(RoutingDomain::PublicInternet) .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); .unwrap_or(false);
let e2_can_do_ordered = e2 let e2_can_do_ordered = e2
.signed_node_info(RoutingDomain::PublicInternet) .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); .unwrap_or(false);
// Reverse this comparison because ordered is preferable (less) // Reverse this comparison because ordered is preferable (less)
e2_can_do_ordered.cmp(&e1_can_do_ordered) e2_can_do_ordered.cmp(&e1_can_do_ordered)
@ -410,7 +485,7 @@ impl RouteSpecStore {
// apply stability preference // apply stability preference
// always prioritize reliable nodes, but sort by oldest or fastest // always prioritize reliable nodes, but sort by oldest or fastest
entry1.with_inner(|e1| { 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::LowLatency => BucketEntryInner::cmp_fastest_reliable(cur_ts, e1, e2),
Stability::Reliable => BucketEntryInner::cmp_oldest_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); 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 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"); apibail_try_again!("not enough nodes to construct route at this time");
} }
@ -498,7 +573,7 @@ impl RouteSpecStore {
previous_node.clone(), previous_node.clone(),
current_node.clone(), current_node.clone(),
DialInfoFilter::all(), DialInfoFilter::all(),
sequencing, safety_spec.sequencing,
None, None,
); );
if matches!(cm, ContactMethod::Unreachable) { if matches!(cm, ContactMethod::Unreachable) {
@ -537,7 +612,7 @@ impl RouteSpecStore {
next_node.clone(), next_node.clone(),
current_node.clone(), current_node.clone(),
DialInfoFilter::all(), DialInfoFilter::all(),
sequencing, safety_spec.sequencing,
None, None,
); );
if matches!(cm, ContactMethod::Unreachable) { if matches!(cm, ContactMethod::Unreachable) {
@ -573,9 +648,11 @@ impl RouteSpecStore {
let mut route_nodes: Vec<usize> = Vec::new(); let mut route_nodes: Vec<usize> = Vec::new();
let mut can_do_sequenced: bool = true; 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' // 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; route_nodes = rn;
can_do_sequenced = cds; can_do_sequenced = cds;
break; break;
@ -625,7 +702,7 @@ impl RouteSpecStore {
route_set, route_set,
hop_node_refs, hop_node_refs,
directions, directions,
stability, safety_spec.stability,
can_do_sequenced, can_do_sequenced,
automatic, automatic,
); );
@ -1333,9 +1410,7 @@ impl RouteSpecStore {
inner, inner,
rti, rti,
&[crypto_kind], &[crypto_kind],
safety_spec.stability, safety_spec,
safety_spec.sequencing,
safety_spec.hop_count,
direction, direction,
avoid_nodes, avoid_nodes,
true, true,

View File

@ -207,11 +207,15 @@ impl RoutingTable {
for _n in 0..routes_to_allocate { for _n in 0..routes_to_allocate {
// Parameters here must be the most inclusive safety route spec // Parameters here must be the most inclusive safety route spec
// These will be used by test_remote_route as well // 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( match rss.allocate_route(
&VALID_CRYPTO_KINDS, &VALID_CRYPTO_KINDS,
Stability::Reliable, &safety_spec,
Sequencing::PreferOrdered,
default_route_hop_count,
DirectionSet::all(), DirectionSet::all(),
&[], &[],
true, true,

View File

@ -172,6 +172,7 @@ impl RPCProcessor {
// Ensure the route is validated, and construct a return safetyspec that matches the inbound preferences // Ensure the route is validated, and construct a return safetyspec that matches the inbound preferences
let rss = self.routing_table().route_spec_store(); let rss = self.routing_table().route_spec_store();
let preferred_route = rss.get_route_id_for_key(&pr_pubkey.value); let preferred_route = rss.get_route_id_for_key(&pr_pubkey.value);
let Some((secret_key, safety_spec)) = rss.with_signature_validated_route( let Some((secret_key, safety_spec)) = rss.with_signature_validated_route(
&pr_pubkey, &pr_pubkey,
routed_operation.signatures(), routed_operation.signatures(),

View File

@ -282,6 +282,8 @@ pub fn config_callback(key: String) -> ConfigCallbackReturn {
"network.protocol.wss.listen_address" => Ok(Box::new("".to_owned())), "network.protocol.wss.listen_address" => Ok(Box::new("".to_owned())),
"network.protocol.wss.path" => Ok(Box::new(String::from("ws"))), "network.protocol.wss.path" => Ok(Box::new(String::from("ws"))),
"network.protocol.wss.url" => Ok(Box::new(Option::<String>::None)), "network.protocol.wss.url" => Ok(Box::new(Option::<String>::None)),
#[cfg(feature = "geolocation")]
"network.privacy.country_code_denylist" => Ok(Box::new(Vec::<CountryCode>::new())),
_ => { _ => {
let err = format!("config key '{}' doesn't exist", key); let err = format!("config key '{}' doesn't exist", key);
debug!("{}", err); 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.listen_address, "");
assert_eq!(inner.network.protocol.wss.path, "ws"); assert_eq!(inner.network.protocol.wss.path, "ws");
assert_eq!(inner.network.protocol.wss.url, None); 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() { pub async fn test_all() {

View File

@ -300,16 +300,16 @@ impl VeilidAPI {
c.network.rpc.default_route_hop_count.into() c.network.rpc.default_route_hop_count.into()
}; };
let rss = self.routing_table()?.route_spec_store(); let safety_spec = SafetySpec {
let route_id = rss.allocate_route( preferred_route: None,
crypto_kinds, hop_count: default_route_hop_count,
stability, stability,
sequencing, sequencing,
default_route_hop_count, };
DirectionSet::all(),
&[], let rss = self.routing_table()?.route_spec_store();
false, let route_id =
)?; rss.allocate_route(crypto_kinds, &safety_spec, DirectionSet::all(), &[], false)?;
match rss.test_route(route_id).await? { match rss.test_route(route_id).await? {
Some(true) => { Some(true) => {
// route tested okay // route tested okay

View File

@ -185,6 +185,7 @@ fn get_safety_selection(routing_table: RoutingTable) -> impl Fn(&str) -> Option<
sequencing = s; sequencing = s;
} }
} }
let ss = SafetySpec { let ss = SafetySpec {
preferred_route, preferred_route,
hop_count, hop_count,
@ -1146,16 +1147,16 @@ impl VeilidAPI {
ai += 1; ai += 1;
} }
// Allocate route let safety_spec = SafetySpec {
let out = match rss.allocate_route( preferred_route: None,
&VALID_CRYPTO_KINDS, hop_count,
stability, stability,
sequencing, sequencing,
hop_count, };
directions,
&[], // Allocate route
false, let out =
) { match rss.allocate_route(&VALID_CRYPTO_KINDS, &safety_spec, directions, &[], false) {
Ok(v) => v.to_string(), Ok(v) => v.to_string(),
Err(e) => { Err(e) => {
format!("Route allocation failed: {}", e) format!("Route allocation failed: {}", e)

View File

@ -237,6 +237,10 @@ pub fn fix_veilidconfiginner() -> VeilidConfigInner {
url: Some("https://veilid.com/wss".to_string()), url: Some("https://veilid.com/wss".to_string()),
}, },
}, },
#[cfg(feature = "geolocation")]
privacy: VeilidConfigPrivacy {
country_code_denylist: vec![CountryCode([b'N', b'Z'])],
},
}, },
} }
} }

View File

@ -275,6 +275,28 @@ pub struct VeilidConfigProtocol {
pub wss: VeilidConfigWSS, 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<CountryCode>,
}
#[cfg(feature = "geolocation")]
impl Default for VeilidConfigPrivacy {
fn default() -> Self {
Self {
country_code_denylist: Vec::new(),
}
}
}
/// Configure TLS. /// Configure TLS.
/// ///
/// ```yaml /// ```yaml
@ -503,6 +525,8 @@ pub struct VeilidConfigNetwork {
pub tls: VeilidConfigTLS, pub tls: VeilidConfigTLS,
pub application: VeilidConfigApplication, pub application: VeilidConfigApplication,
pub protocol: VeilidConfigProtocol, pub protocol: VeilidConfigProtocol,
#[cfg(feature = "geolocation")]
pub privacy: VeilidConfigPrivacy,
} }
impl Default for VeilidConfigNetwork { impl Default for VeilidConfigNetwork {
@ -527,6 +551,8 @@ impl Default for VeilidConfigNetwork {
tls: VeilidConfigTLS::default(), tls: VeilidConfigTLS::default(),
application: VeilidConfigApplication::default(), application: VeilidConfigApplication::default(),
protocol: VeilidConfigProtocol::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.listen_address);
get_config!(inner.network.protocol.wss.path); get_config!(inner.network.protocol.wss.path);
get_config!(inner.network.protocol.wss.url); get_config!(inner.network.protocol.wss.url);
#[cfg(feature = "geolocation")]
get_config!(inner.network.privacy.country_code_denylist);
Ok(()) Ok(())
}) })
} }

View File

@ -42,6 +42,8 @@ tracking = ["veilid-core/tracking"]
debug-json-api = [] debug-json-api = []
debug-locks = ["veilid-core/debug-locks"] debug-locks = ["veilid-core/debug-locks"]
geolocation = ["veilid-core/geolocation"]
[dependencies] [dependencies]
veilid-core = { path = "../veilid-core", default-features = false } veilid-core = { path = "../veilid-core", default-features = false }
tracing = { version = "^0.1.40", features = ["log", "attributes"] } tracing = { version = "^0.1.40", features = ["log", "attributes"] }

View File

@ -32,6 +32,14 @@ lazy_static! {
} }
pub fn load_default_config() -> EyreResult<config::Config> { pub fn load_default_config() -> EyreResult<config::Config> {
#[cfg(not(feature = "geolocation"))]
let privacy_section = "";
#[cfg(feature = "geolocation")]
let privacy_section = r#"
privacy:
country_code_denylist: []
"#;
let mut default_config = String::from( let mut default_config = String::from(
r#"--- r#"---
daemon: daemon:
@ -188,6 +196,7 @@ core:
listen_address: ':5150' listen_address: ':5150'
path: 'ws' path: 'ws'
# url: '' # url: ''
%PRIVACY_SECTION%
"#, "#,
) )
.replace( .replace(
@ -217,7 +226,8 @@ core:
.replace( .replace(
"%REMOTE_MAX_SUBKEY_CACHE_MEMORY_MB%", "%REMOTE_MAX_SUBKEY_CACHE_MEMORY_MB%",
&Settings::get_default_remote_max_subkey_cache_memory_mb().to_string(), &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") { let dek_password = if let Some(dek_password) = std::env::var_os("DEK_PASSWORD") {
dek_password dek_password
@ -584,6 +594,12 @@ pub struct Protocol {
pub wss: Wss, pub wss: Wss,
} }
#[cfg(feature = "geolocation")]
#[derive(Debug, Deserialize, Serialize)]
pub struct Privacy {
pub country_code_denylist: Vec<CountryCode>,
}
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct Tls { pub struct Tls {
pub certificate_path: String, pub certificate_path: String,
@ -661,6 +677,8 @@ pub struct Network {
pub tls: Tls, pub tls: Tls,
pub application: Application, pub application: Application,
pub protocol: Protocol, pub protocol: Protocol,
#[cfg(feature = "geolocation")]
pub privacy: Privacy,
} }
#[derive(Debug, Deserialize, Serialize)] #[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.listen_address, value);
set_config_value!(inner.core.network.protocol.wss.path, value); set_config_value!(inner.core.network.protocol.wss.path, value);
set_config_value!(inner.core.network.protocol.wss.url, 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")) Err(eyre!("settings key '{key}' not found"))
} }
@ -1548,6 +1568,10 @@ impl Settings {
.as_ref() .as_ref()
.map(|a| a.urlstring.clone()), .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!( _ => Err(VeilidAPIError::generic(format!(
"config key '{}' doesn't exist", "config key '{}' doesn't exist",
key key
@ -1788,5 +1812,7 @@ mod tests {
); );
assert_eq!(s.core.network.protocol.wss.url, None); assert_eq!(s.core.network.protocol.wss.url, None);
// //
#[cfg(feature = "geolocation")]
assert_eq!(s.core.network.privacy.country_code_denylist, &[]);
} }
} }