use crate::{LowLevelRelayPredicate, RelayExclusion, RelayRestriction, RelayUsage};
use tor_basic_utils::iter::FilterCount;
use tor_netdir::{NetDir, Relay, WeightRole};
use std::fmt;
#[derive(Clone, Debug)]
pub struct RelaySelector<'a> {
usage: Restr<'a>,
exclusion: Restr<'a>,
other_restrictions: Vec<Restr<'a>>,
}
#[derive(Clone, Debug)]
struct Restr<'a> {
restriction: RelayRestriction<'a>,
strict: bool,
}
impl<'a> Restr<'a> {
fn maybe_relax(&self) -> Self {
if self.strict {
self.clone()
} else {
Self {
restriction: self.restriction.relax(),
strict: true,
}
}
}
}
#[derive(Debug, Clone)]
pub struct SelectionInfo<'a> {
first_try: FilterCounts,
relaxed_try: Option<FilterCounts>,
succeeded: bool,
in_selection: &'a RelaySelector<'a>,
}
impl<'a> SelectionInfo<'a> {
pub fn success(&self) -> bool {
self.succeeded
}
pub fn result_is_relaxed_success(&self) -> bool {
self.relaxed_try.is_some() && self.succeeded
}
}
impl<'a> fmt::Display for SelectionInfo<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match (self.succeeded, &self.relaxed_try) {
(true, None) => write!(f, "Success: {}", FcDisp(&self.first_try, self.in_selection))?,
(false, None) => write!(f, "Failed: {}", FcDisp(&self.first_try, self.in_selection))?,
(true, Some(retry)) => write!(
f,
"Failed at first, then succeeded. At first, {}. After relaxing requirements, {}",
FcDisp(&self.first_try, self.in_selection),
FcDisp(retry, self.in_selection)
)?,
(false, Some(retry)) => write!(
f,
"Failed even after relaxing requirement. At first, {}. After relaxing requirements, {}",
FcDisp(&self.first_try, self.in_selection),
FcDisp(retry, self.in_selection)
)?,
};
Ok(())
}
}
#[derive(Debug, Clone)]
struct FilterCounts {
counts: Vec<FilterCount>,
}
impl FilterCounts {
fn new(selector: &RelaySelector) -> Self {
let counts = vec![FilterCount::default(); selector.n_restrictions()];
FilterCounts { counts }
}
}
struct FcDisp<'a>(&'a FilterCounts, &'a RelaySelector<'a>);
impl<'a> fmt::Display for FcDisp<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let counts = &self.0.counts;
let restrictions = self.1.all_restrictions();
write!(f, "rejected ")?;
let mut first = true;
let mut found_any_rejected = false;
for (c, r) in counts.iter().zip(restrictions) {
if let Some(desc) = r.restriction.rejection_description() {
if first {
first = false;
} else {
write!(f, "; ")?;
}
write!(f, "{} as {}", c.display_frac_rejected(), desc)?;
found_any_rejected = true;
} else {
debug_assert_eq!(c.n_rejected, 0);
}
}
if !found_any_rejected {
write!(f, "none")?;
}
Ok(())
}
}
impl<'a> RelaySelector<'a> {
pub fn new(usage: RelayUsage, exclusion: RelayExclusion<'a>) -> Self {
Self {
usage: Restr {
restriction: RelayRestriction::for_usage(usage),
strict: true,
},
exclusion: Restr {
restriction: exclusion.into(),
strict: true,
},
other_restrictions: vec![],
}
}
pub fn mark_usage_flexible(&mut self) {
self.usage.strict = false;
}
pub fn mark_exclusion_flexible(&mut self) {
self.exclusion.strict = false;
}
pub fn push_restriction(&mut self, restriction: RelayRestriction<'a>) {
self.push_inner(restriction, true);
}
pub fn push_flexible_restriction(&mut self, restriction: RelayRestriction<'a>) {
self.push_inner(restriction, false);
}
fn push_inner(&mut self, restriction: RelayRestriction<'a>, strict: bool) {
self.other_restrictions.push(Restr {
restriction,
strict,
});
}
pub fn usage(&self) -> &RelayUsage {
self.usage
.restriction
.as_usage()
.expect("Usage not a usage!?")
}
fn weight_role(&self) -> WeightRole {
self.usage().selection_weight_role()
}
pub fn permits_relay(&self, relay: &tor_netdir::Relay<'_>) -> bool {
self.low_level_predicate_permits_relay(relay)
}
fn all_restrictions(&self) -> impl Iterator<Item = &Restr<'a>> {
use std::iter::once;
once(&self.usage)
.chain(once(&self.exclusion))
.chain(self.other_restrictions.iter())
}
fn n_restrictions(&self) -> usize {
self.other_restrictions.len() + 2
}
pub fn select_relay<'s, 'd, R: rand::Rng>(
&'s self,
rng: &mut R,
netdir: &'d NetDir,
) -> (Option<Relay<'d>>, SelectionInfo<'s>) {
with_possible_relaxation(
self,
|selector| {
let role = selector.weight_role();
let mut fc = FilterCounts::new(selector);
let relay = netdir.pick_relay(rng, role, |r| selector.relay_usable(r, &mut fc));
(relay, fc)
},
Option::is_some,
)
}
pub fn select_n_relays<'s, 'd, R: rand::Rng>(
&'s self,
rng: &mut R,
n_relays: usize,
netdir: &'d NetDir,
) -> (Vec<Relay<'d>>, SelectionInfo<'s>) {
with_possible_relaxation(
self,
|selector| {
let role = selector.weight_role();
let mut fc = FilterCounts::new(selector);
let relays = netdir
.pick_n_relays(rng, n_relays, role, |r| selector.relay_usable(r, &mut fc));
(relays, fc)
},
|relays| !relays.is_empty(),
)
}
fn relay_usable(&self, r: &Relay<'_>, fc: &mut FilterCounts) -> bool {
debug_assert_eq!(self.n_restrictions(), fc.counts.len());
self.all_restrictions()
.zip(fc.counts.iter_mut())
.all(|(restr, restr_count)| {
restr_count.count(restr.restriction.low_level_predicate_permits_relay(r))
})
}
fn can_relax(&self) -> bool {
self.all_restrictions().any(|restr| !restr.strict)
}
fn relax(&self) -> Self {
let new_selector = RelaySelector {
usage: self.usage.maybe_relax(),
exclusion: self.exclusion.maybe_relax(),
other_restrictions: self
.other_restrictions
.iter()
.map(Restr::maybe_relax)
.collect(),
};
debug_assert!(!new_selector.can_relax());
new_selector
}
}
impl<'a> LowLevelRelayPredicate for RelaySelector<'a> {
fn low_level_predicate_permits_relay(&self, relay: &tor_netdir::Relay<'_>) -> bool {
self.all_restrictions()
.all(|r| r.restriction.low_level_predicate_permits_relay(relay))
}
}
fn with_possible_relaxation<'a, SEL, OK, T>(
selector: &'a RelaySelector,
mut select: SEL,
ok: OK,
) -> (T, SelectionInfo<'a>)
where
SEL: FnMut(&RelaySelector) -> (T, FilterCounts),
OK: Fn(&T) -> bool,
{
let (outcome, count_strict) = select(selector);
let succeeded = ok(&outcome);
if succeeded || !selector.can_relax() {
let info = SelectionInfo {
first_try: count_strict,
relaxed_try: None,
succeeded,
in_selection: selector,
};
return (outcome, info);
}
let relaxed_selector = selector.relax();
let (relaxed_outcome, count_relaxed) = select(&relaxed_selector);
let info = SelectionInfo {
first_try: count_strict,
relaxed_try: Some(count_relaxed),
succeeded: ok(&relaxed_outcome),
in_selection: selector,
};
(relaxed_outcome, info)
}
#[cfg(test)]
mod test {
#![allow(clippy::bool_assert_comparison)]
#![allow(clippy::clone_on_copy)]
#![allow(clippy::dbg_macro)]
#![allow(clippy::mixed_attributes_style)]
#![allow(clippy::print_stderr)]
#![allow(clippy::print_stdout)]
#![allow(clippy::single_char_pattern)]
#![allow(clippy::unwrap_used)]
#![allow(clippy::unchecked_duration_subtraction)]
#![allow(clippy::useless_vec)]
#![allow(clippy::needless_pass_by_value)]
use std::collections::HashSet;
use tor_basic_utils::test_rng::testing_rng;
use tor_linkspec::{HasRelayIds, RelayId};
use tor_netdir::{Relay, SubnetConfig};
use super::*;
use crate::{
testing::{cfg, split_netdir, testnet},
RelaySelectionConfig, TargetPort,
};
#[test]
fn selector_as_predicate() {
let nd = testnet();
let id_4 = "$0404040404040404040404040404040404040404".parse().unwrap();
let usage = RelayUsage::middle_relay(None);
let exclusion = RelayExclusion::exclude_identities([id_4].into_iter().collect());
let sel = RelaySelector::new(usage.clone(), exclusion.clone());
let (yes, no) = split_netdir(&nd, &sel);
let p = |r: &Relay<'_>| {
usage.low_level_predicate_permits_relay(r)
&& exclusion.low_level_predicate_permits_relay(r)
};
assert!(yes.iter().all(p));
assert!(no.iter().all(|r| !p(r)));
}
#[test]
fn selector_as_filter() {
let nd = testnet();
let id_4 = "$0404040404040404040404040404040404040404".parse().unwrap();
let usage = RelayUsage::middle_relay(None);
let exclusion = RelayExclusion::exclude_identities([id_4].into_iter().collect());
let sel = RelaySelector::new(usage.clone(), exclusion.clone());
let mut fc = FilterCounts::new(&sel);
let (yes, _no) = split_netdir(&nd, &sel);
let filtered: Vec<_> = nd
.relays()
.filter(|r| sel.relay_usable(r, &mut fc))
.collect();
assert_eq!(yes.len(), filtered.len());
let k1: HashSet<_> = yes.iter().map(|r| r.rsa_identity().unwrap()).collect();
let k2: HashSet<_> = filtered.iter().map(|r| r.rsa_identity().unwrap()).collect();
assert_eq!(k1, k2);
assert_eq!(fc.counts[0].n_rejected, 12);
assert_eq!(fc.counts[1].n_rejected, 1);
assert_eq!(fc.counts[1].n_accepted, yes.len());
}
#[test]
fn selector_pick_random() {
let nd = testnet();
let id_4 = "$0404040404040404040404040404040404040404".parse().unwrap();
let usage = RelayUsage::middle_relay(None);
let exclusion = RelayExclusion::exclude_identities([id_4].into_iter().collect());
let sel = RelaySelector::new(usage.clone(), exclusion.clone());
let (yes, _no) = split_netdir(&nd, &sel);
let k_yes: HashSet<_> = yes.iter().map(|r| r.rsa_identity().unwrap()).collect();
let p = |r: Relay<'_>| k_yes.contains(r.rsa_identity().unwrap());
let mut rng = testing_rng();
for _ in 0..50 {
let (r_rand, si) = sel.select_relay(&mut rng, &nd);
assert!(si.success());
assert!(!si.result_is_relaxed_success());
assert!(p(r_rand.unwrap()));
let (rs_rand, si) = sel.select_n_relays(&mut rng, 20, &nd);
assert_eq!(rs_rand.len(), 20);
assert!(si.success());
assert!(!si.result_is_relaxed_success());
assert!(rs_rand.iter().cloned().all(p));
let k_got: HashSet<_> = rs_rand.iter().map(|r| r.rsa_identity().unwrap()).collect();
assert_eq!(k_got.len(), 20);
}
}
#[test]
fn selector_report() {
let nd = testnet();
let id_4 = "$0404040404040404040404040404040404040404".parse().unwrap();
let usage = RelayUsage::middle_relay(None);
let exclusion = RelayExclusion::exclude_identities([id_4].into_iter().collect());
let sel = RelaySelector::new(usage.clone(), exclusion.clone());
let mut rng = testing_rng();
let (_, si) = sel.select_relay(&mut rng, &nd);
assert_eq!(
si.to_string(),
"Success: rejected 12/40 as useless for middle relay; 1/28 as already selected"
);
let unreachable_port = TargetPort::ipv6(80);
let sel = RelaySelector::new(
RelayUsage::exit_to_all_ports(&cfg(), vec![unreachable_port]),
exclusion.clone(),
);
let (r_none, si) = sel.select_relay(&mut rng, &nd);
assert!(r_none.is_none());
assert_eq!(
si.to_string(),
"Failed: rejected 40/40 as not exiting to desired ports; 0/0 as already selected"
);
}
#[test]
fn relax() {
let nd = testnet();
let id_4: RelayId = "$0404040404040404040404040404040404040404".parse().unwrap();
let r4 = nd.by_id(&id_4).unwrap();
let usage = RelayUsage::middle_relay(None);
let very_silly_cfg = RelaySelectionConfig {
long_lived_ports: cfg().long_lived_ports,
subnet_config: SubnetConfig::new(1, 1),
};
let exclude_relays = vec![r4];
let exclude_everyone =
RelayExclusion::exclude_relays_in_same_family(&very_silly_cfg, exclude_relays);
let mut sel = RelaySelector::new(usage.clone(), exclude_everyone.clone());
let mut rng = testing_rng();
let (r_none, _) = sel.select_relay(&mut rng, &nd);
assert!(r_none.is_none());
sel.mark_exclusion_flexible();
let (r_some, si) = sel.select_relay(&mut rng, &nd);
assert!(r_some.is_some());
assert_eq!(si.to_string(), "Failed at first, then succeeded. At first, rejected 12/40 as useless for middle relay; \
28/28 as in same family as already selected. \
After relaxing requirements, rejected 12/40 as useless for middle relay; \
0/28 as in same family as already selected");
}
}