#![forbid(unsafe_code)]
use std::collections::HashMap;
#[cfg_attr(not(feature = "nym"), allow(dead_code))]
pub(crate) const UNHEALTHY_FAILURE_THRESHOLD: u32 = 2;
#[cfg_attr(not(feature = "nym"), allow(dead_code))]
const MINIMUM_ELIGIBLE_DESTINATIONS: usize = 4;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FaultDomain {
Tunnel,
Destination,
Unknown,
}
pub(crate) fn fault_domain(stage: &zingo_net_diag::NetOpStage) -> FaultDomain {
use zingo_net_diag::NetOpStage;
match stage {
NetOpStage::RouteResolution
| NetOpStage::LocalProxyConnect
| NetOpStage::SocksHandshake
| NetOpStage::TunnelTransport => FaultDomain::Tunnel,
NetOpStage::RemoteTls | NetOpStage::RemoteHttp | NetOpStage::PayloadDecode => {
FaultDomain::Destination
}
_ => FaultDomain::Unknown,
}
}
#[derive(Clone, Copy, Debug, Default)]
struct Standing {
successes: u32,
failures: u32,
}
#[derive(Debug, Default)]
pub struct Health {
standings: HashMap<super::Host, Standing>,
}
impl Health {
pub(crate) fn note(
&mut self,
host: &super::Host,
failed: bool,
fault_domain: Option<FaultDomain>,
) {
let standing = self.standings.entry(host.clone()).or_default();
if !failed {
standing.successes += 1;
} else if fault_domain == Some(FaultDomain::Destination) {
standing.failures += 1;
}
}
#[cfg_attr(not(feature = "nym"), allow(dead_code))]
pub(crate) fn is_healthy(&self, host: &super::Host) -> bool {
match self.standings.get(host) {
None => true,
Some(standing) => {
standing.successes > 0 || standing.failures < UNHEALTHY_FAILURE_THRESHOLD
}
}
}
#[cfg_attr(not(feature = "nym"), allow(dead_code))]
pub(crate) fn filter_with_floor(&self, candidates: Vec<http::Uri>) -> Vec<http::Uri> {
let healthy: Vec<http::Uri> = candidates
.iter()
.filter(|uri| {
uri.host()
.is_none_or(|host| self.is_healthy(&super::Host::of_host_str(host)))
})
.cloned()
.collect();
if healthy.len() < MINIMUM_ELIGIBLE_DESTINATIONS {
candidates
} else {
healthy
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn uris(hosts: &[&str]) -> Vec<http::Uri> {
hosts
.iter()
.map(|host| format!("https://{host}:443").parse().unwrap())
.collect()
}
fn host(name: &str) -> super::super::Host {
super::super::Host::of_host_str(name)
}
#[test]
fn a_tunnel_failure_is_never_attributed_to_the_destination() {
let mut health = Health::default();
for _ in 0..UNHEALTHY_FAILURE_THRESHOLD {
health.note(&host("tunnelled.example"), true, Some(FaultDomain::Tunnel));
health.note(&host("unknown.example"), true, None);
health.note(
&host("refusing.example"),
true,
Some(FaultDomain::Destination),
);
}
assert!(health.is_healthy(&host("tunnelled.example")));
assert!(health.is_healthy(&host("unknown.example")));
assert!(!health.is_healthy(&host("refusing.example")));
}
#[test]
fn a_success_redeems_a_failing_destination() {
let mut health = Health::default();
for _ in 0..UNHEALTHY_FAILURE_THRESHOLD {
health.note(&host("flaky.example"), true, Some(FaultDomain::Destination));
}
assert!(!health.is_healthy(&host("flaky.example")));
health.note(&host("flaky.example"), false, None);
assert!(health.is_healthy(&host("flaky.example")));
}
#[test]
fn the_floor_outranks_the_filter() {
let mut health = Health::default();
let roomy = uris(&["a", "b", "c", "d", "e", "f"]);
for _ in 0..UNHEALTHY_FAILURE_THRESHOLD {
health.note(&host("a"), true, Some(FaultDomain::Destination));
}
let filtered = health.filter_with_floor(roomy.clone());
assert_eq!(filtered.len(), roomy.len() - 1, "the unhealthy one goes");
let scarce = uris(&["a", "b", "c", "d"]);
assert_eq!(
health.filter_with_floor(scarce.clone()),
scarce,
"the floor keeps the draw's anonymity set whole"
);
}
}