#[cfg(feature = "geoip")]
use tor_geoip::HasCountryCode;
use tor_linkspec::{ChanTarget, HasAddrs, HasRelayIds, RelayIdSet};
use tor_netdir::{FamilyRules, NetDir, Relay, SubnetConfig};
use tor_netdoc::types::policy::AddrPortPattern;
use crate::{LowLevelRelayPredicate, RelaySelectionConfig, RelayUsage};
use std::{fmt, net::IpAddr};
#[derive(Clone, Debug)]
pub struct RelayRestriction<'a> {
inner: RestrictionInner<'a>,
}
#[derive(Clone, Debug)]
enum RestrictionInner<'a> {
NoRestriction,
SupportsUsage(crate::RelayUsage),
Exclude(RelayExclusion<'a>),
HasAddrInSet(Vec<AddrPortPattern>),
#[cfg(feature = "geoip")]
RequireCountry(tor_geoip::CountryCode),
}
impl<'a> RelayRestriction<'a> {
pub(crate) fn no_restriction() -> Self {
RelayRestriction {
inner: RestrictionInner::NoRestriction,
}
}
pub(crate) fn for_usage(usage: crate::RelayUsage) -> Self {
RelayRestriction {
inner: RestrictionInner::SupportsUsage(usage),
}
}
#[cfg(feature = "geoip")]
pub fn require_country_code(cc: tor_geoip::CountryCode) -> Self {
RelayRestriction {
inner: RestrictionInner::RequireCountry(cc),
}
}
pub fn require_address(addr_patterns: Vec<AddrPortPattern>) -> Self {
RelayRestriction {
inner: RestrictionInner::HasAddrInSet(addr_patterns),
}
}
pub(crate) fn relax(&self) -> Self {
use RestrictionInner::*;
match &self.inner {
SupportsUsage(usage) => Self::for_usage(RelayUsage::middle_relay(Some(usage))),
_ => Self::no_restriction(),
}
}
pub(crate) fn as_usage(&self) -> Option<&RelayUsage> {
use RestrictionInner::*;
match &self.inner {
SupportsUsage(usage) => Some(usage),
_ => None,
}
}
pub(crate) fn rejection_description(&self) -> Option<&'static str> {
use RestrictionInner::*;
match &self.inner {
NoRestriction => None,
SupportsUsage(u) => Some(u.rejection_description()),
Exclude(e) => e.rejection_description(),
HasAddrInSet(_) => Some("not reachable (according to address filter)"),
#[cfg(feature = "geoip")]
RequireCountry(_) => Some("not in correct country"),
}
}
}
impl<'a> LowLevelRelayPredicate for RelayRestriction<'a> {
fn low_level_predicate_permits_relay(&self, relay: &tor_netdir::Relay<'_>) -> bool {
use RestrictionInner::*;
match &self.inner {
NoRestriction => true,
SupportsUsage(usage) => usage.low_level_predicate_permits_relay(relay),
Exclude(exclusion) => exclusion.low_level_predicate_permits_relay(relay),
HasAddrInSet(patterns) => relay_has_addr_in_set(relay, patterns),
#[cfg(feature = "geoip")]
RequireCountry(cc) => relay.country_code() == Some(*cc),
}
}
}
impl<'a> From<RelayExclusion<'a>> for RelayRestriction<'a> {
fn from(value: RelayExclusion<'a>) -> Self {
RelayRestriction {
inner: RestrictionInner::Exclude(value),
}
}
}
fn relay_has_addr_in_set(relay: &Relay<'_>, patterns: &[AddrPortPattern]) -> bool {
relay
.addrs()
.any(|addr| patterns.iter().any(|pat| pat.matches_sockaddr(&addr)))
}
#[derive(Clone, Debug)]
pub struct RelayExclusion<'a> {
exclude_ids: RelayIdSet,
exclude_subnets: Vec<IpAddr>,
exclude_relay_families: RelayList<'a>,
subnet_config: SubnetConfig,
family_rules: FamilyRules,
}
#[derive(Clone)]
struct RelayList<'a>(Vec<Relay<'a>>);
impl<'a> fmt::Debug for RelayList<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[ ")?;
for r in &self.0 {
write!(f, "{}, ", r.display_relay_ids())?;
}
write!(f, "]")
}
}
impl<'a> RelayExclusion<'a> {
pub fn no_relays_excluded() -> Self {
RelayExclusion {
exclude_ids: RelayIdSet::new(),
exclude_subnets: Vec::new(),
exclude_relay_families: RelayList(Vec::new()),
subnet_config: SubnetConfig::no_addresses_match(),
family_rules: FamilyRules::ignore_declared_families(),
}
}
pub fn exclude_identities(ids: RelayIdSet) -> Self {
RelayExclusion {
exclude_ids: ids,
..RelayExclusion::no_relays_excluded()
}
}
pub fn exclude_specific_relays(relays: &[Relay<'a>]) -> Self {
let ids: RelayIdSet = relays
.iter()
.flat_map(Relay::identities)
.map(|id_ref| id_ref.to_owned())
.collect();
Self::exclude_identities(ids)
}
pub fn exclude_channel_target_family<CT: ChanTarget>(
cfg: &RelaySelectionConfig,
ct: &CT,
netdir: &'a NetDir,
) -> Self {
if let Some(r) = netdir.by_ids(ct) {
return Self::exclude_relays_in_same_family(
cfg,
vec![r],
FamilyRules::from(netdir.params()),
);
}
let exclude_ids = ct.identities().map(|id_ref| id_ref.to_owned()).collect();
let exclude_addr_families = ct.addrs().map(|a| a.ip()).collect();
Self {
exclude_ids,
exclude_subnets: exclude_addr_families,
subnet_config: cfg.subnet_config,
..Self::no_relays_excluded()
}
}
pub fn exclude_relays_in_same_family(
cfg: &RelaySelectionConfig,
relays: Vec<Relay<'a>>,
family_rules: FamilyRules,
) -> Self {
RelayExclusion {
exclude_relay_families: RelayList(relays),
subnet_config: cfg.subnet_config,
family_rules,
..RelayExclusion::no_relays_excluded()
}
}
pub fn extend(&mut self, other: &RelayExclusion<'a>) {
let RelayExclusion {
exclude_ids,
exclude_subnets: exclude_addr_families,
exclude_relay_families,
subnet_config,
family_rules,
} = other;
self.exclude_ids
.extend(exclude_ids.iter().map(|id_ref| id_ref.to_owned()));
self.exclude_subnets
.extend_from_slice(&exclude_addr_families[..]);
self.exclude_relay_families
.0
.extend_from_slice(&exclude_relay_families.0[..]);
self.subnet_config = self.subnet_config.union(subnet_config);
self.family_rules = self.family_rules.union(family_rules);
}
pub(crate) fn rejection_description(&self) -> Option<&'static str> {
if self.exclude_relay_families.0.is_empty() && self.exclude_subnets.is_empty() {
if self.exclude_ids.is_empty() {
None
} else {
Some("already selected")
}
} else {
Some("in same family as already selected")
}
}
}
impl<'a> LowLevelRelayPredicate for RelayExclusion<'a> {
fn low_level_predicate_permits_relay(&self, relay: &Relay<'_>) -> bool {
if relay.identities().any(|id| self.exclude_ids.contains(id)) {
return false;
}
if relay.addrs().any(|addr| {
self.exclude_subnets
.iter()
.any(|fam| self.subnet_config.addrs_in_same_subnet(&addr.ip(), fam))
}) {
return false;
}
if self.exclude_relay_families.0.iter().any(|r| {
relays_in_same_extended_family(&self.subnet_config, relay, r, self.family_rules)
}) {
return false;
}
true
}
}
fn relays_in_same_extended_family(
subnet_config: &SubnetConfig,
r1: &Relay<'_>,
r2: &Relay<'_>,
family_rules: FamilyRules,
) -> bool {
r1.low_level_details().in_same_family(r2, family_rules)
|| subnet_config.any_addrs_in_same_subnet(r1, r2)
}
#[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_time_subtraction)]
#![allow(clippy::useless_vec)]
#![allow(clippy::needless_pass_by_value)]
use tor_linkspec::RelayId;
use tor_netdir::testnet::construct_custom_netdir;
use super::*;
use crate::testing::{cfg, split_netdir, testnet};
#[test]
fn exclude_nothing() {
let nd = testnet();
let usage = RelayExclusion::no_relays_excluded();
assert!(
nd.relays()
.all(|r| usage.low_level_predicate_permits_relay(&r))
);
}
#[test]
fn exclude_ids() {
let nd = testnet();
let id_0 = "$0000000000000000000000000000000000000000".parse().unwrap();
let id_5 = "ed25519:BQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU"
.parse()
.unwrap();
let ids: RelayIdSet = [id_0, id_5].into_iter().collect();
let (yes, no) = split_netdir(&nd, &RelayExclusion::exclude_identities(ids));
let p = |r: &Relay<'_>| !(r.has_identity(id_0.as_ref()) || r.has_identity(id_5.as_ref()));
assert_eq!(yes.len(), 38);
assert_eq!(no.len(), 2);
assert!(yes.iter().all(p));
assert!(no.iter().all(|r| !p(r)));
}
#[test]
fn exclude_relays() {
let nd = testnet();
let id_0: RelayId = "$0000000000000000000000000000000000000000".parse().unwrap();
let id_5: RelayId = "ed25519:BQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU"
.parse()
.unwrap();
let relay_0 = nd.by_id(&id_0).unwrap();
let relay_5 = nd.by_id(&id_5).unwrap();
let (yes, no) = split_netdir(
&nd,
&RelayExclusion::exclude_specific_relays(&[relay_0.clone(), relay_5.clone()]),
);
let p = |r: &Relay<'_>| !(r.same_relay_ids(&relay_0) || r.same_relay_ids(&relay_5));
assert_eq!(yes.len(), 38);
assert_eq!(no.len(), 2);
assert!(yes.iter().all(p));
assert!(no.iter().all(|r| !p(r)));
}
fn exclude_families_impl(nd: &NetDir, family_rules: FamilyRules) {
let id_0: RelayId = "$0000000000000000000000000000000000000000".parse().unwrap();
let id_5: RelayId = "ed25519:BQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU"
.parse()
.unwrap();
let relay_0 = nd.by_id(&id_0).unwrap();
let relay_5 = nd.by_id(&id_5).unwrap();
let excluding_relays = vec![relay_0, relay_5];
let id_1 = "$0101010101010101010101010101010101010101".parse().unwrap();
let id_4 = "$0404040404040404040404040404040404040404".parse().unwrap();
let expect_excluded_ids: RelayIdSet = [id_0, id_1, id_4, id_5].into_iter().collect();
let cfg_no_subnet = RelaySelectionConfig {
long_lived_ports: cfg().long_lived_ports,
subnet_config: SubnetConfig::new(255, 255),
};
let (yes, no) = split_netdir(
nd,
&RelayExclusion::exclude_relays_in_same_family(
&cfg_no_subnet,
excluding_relays.clone(),
family_rules,
),
);
let p = |r: &Relay<'_>| !r.identities().any(|id| expect_excluded_ids.contains(id));
assert_eq!(yes.len(), 36);
assert_eq!(no.len(), 4);
assert!(yes.iter().all(p));
assert!(no.iter().all(|r| !p(r)));
let expect_excluded_ids: RelayIdSet = nd
.relays()
.filter_map(|r| {
let rsa = r.rsa_identity().unwrap();
let b = rsa.as_bytes()[0];
if [0, 1, 4, 5].contains(&b) || [0, 5].contains(&(b % 5)) {
Some(RelayId::from(*rsa))
} else {
None
}
})
.collect();
let (yes, no) = split_netdir(
nd,
&RelayExclusion::exclude_relays_in_same_family(&cfg(), excluding_relays, family_rules),
);
for r in &no {
dbg!(r.rsa_identity().unwrap());
}
dbg!(&expect_excluded_ids);
dbg!(expect_excluded_ids.len());
let p = |r: &Relay<'_>| !r.identities().any(|id| expect_excluded_ids.contains(id));
assert_eq!(yes.len(), 30);
assert_eq!(no.len(), 10);
assert!(yes.iter().all(p));
assert!(no.iter().all(|r| { !p(r) }));
}
#[test]
fn exclude_families_by_list() {
exclude_families_impl(
&testnet(),
*FamilyRules::ignore_declared_families().use_family_lists(true),
);
}
#[test]
fn exclude_families_by_id() {
let netdir = construct_custom_netdir(|pos, nb, _| {
nb.md.family("".parse().unwrap());
let fam_id = format!("pos:{}", pos / 2);
nb.md.add_family_id(fam_id.parse().unwrap());
})
.unwrap()
.unwrap_if_sufficient()
.unwrap();
exclude_families_impl(
&netdir,
*FamilyRules::ignore_declared_families().use_family_ids(true),
);
}
#[test]
fn filter_addresses() {
let nd = testnet();
let reachable = vec![
"1.0.0.0/8:*".parse().unwrap(),
"2.0.0.0/8:*".parse().unwrap(),
];
let reachable = RelayRestriction::require_address(reachable);
let (yes, no) = split_netdir(&nd, &reachable);
assert_eq!(yes.len(), 16);
assert_eq!(no.len(), 24);
let expected = ["1.0.0.3".parse().unwrap(), "2.0.0.3".parse().unwrap()];
let p = |r: &Relay<'_>| expected.contains(&r.addrs().next().unwrap().ip());
assert!(yes.iter().all(p));
assert!(no.iter().all(|r| !p(r)));
}
}