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(
&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<RouteId> {
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<usize> = 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,

View File

@ -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,

View File

@ -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(),

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.path" => Ok(Box::new(String::from("ws"))),
"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);
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() {

View File

@ -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

View File

@ -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,16 +1147,16 @@ 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,
) {
};
// 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)

View File

@ -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'])],
},
},
}
}

View File

@ -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<CountryCode>,
}
#[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(())
})
}

View File

@ -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"] }

View File

@ -32,6 +32,14 @@ lazy_static! {
}
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(
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<CountryCode>,
}
#[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, &[]);
}
}