#![cfg_attr(docsrs, feature(doc_cfg))]
#![doc = include_str!("../README.md")]
#![allow(renamed_and_removed_lints)] #![allow(unknown_lints)] #![warn(missing_docs)]
#![warn(noop_method_call)]
#![warn(unreachable_pub)]
#![warn(clippy::all)]
#![deny(clippy::await_holding_lock)]
#![deny(clippy::cargo_common_metadata)]
#![deny(clippy::cast_lossless)]
#![deny(clippy::checked_conversions)]
#![warn(clippy::cognitive_complexity)]
#![deny(clippy::debug_assert_with_mut_call)]
#![deny(clippy::exhaustive_enums)]
#![deny(clippy::exhaustive_structs)]
#![deny(clippy::expl_impl_clone_on_copy)]
#![deny(clippy::fallible_impl_from)]
#![deny(clippy::implicit_clone)]
#![deny(clippy::large_stack_arrays)]
#![warn(clippy::manual_ok_or)]
#![deny(clippy::missing_docs_in_private_items)]
#![warn(clippy::needless_borrow)]
#![warn(clippy::needless_pass_by_value)]
#![warn(clippy::option_option)]
#![deny(clippy::print_stderr)]
#![deny(clippy::print_stdout)]
#![warn(clippy::rc_buffer)]
#![deny(clippy::ref_option_ref)]
#![warn(clippy::semicolon_if_nothing_returned)]
#![warn(clippy::trait_duplication_in_bounds)]
#![deny(clippy::unchecked_time_subtraction)]
#![deny(clippy::unnecessary_wraps)]
#![warn(clippy::unseparated_literal_suffix)]
#![deny(clippy::unwrap_used)]
#![deny(clippy::mod_module_files)]
#![allow(clippy::let_unit_value)] #![allow(clippy::uninlined_format_args)]
#![allow(clippy::significant_drop_in_scrutinee)] #![allow(clippy::result_large_err)] #![allow(clippy::needless_raw_string_hashes)] #![allow(clippy::needless_lifetimes)] #![allow(mismatched_lifetime_syntaxes)] #![allow(clippy::collapsible_if)] #![deny(clippy::unused_async)]
pub mod details;
mod err;
#[cfg(feature = "hs-common")]
mod hsdir_params;
#[cfg(feature = "hs-common")]
mod hsdir_ring;
pub mod params;
mod weight;
#[cfg(any(test, feature = "testing"))]
pub mod testnet;
#[cfg(feature = "testing")]
pub mod testprovider;
use async_trait::async_trait;
#[cfg(feature = "hs-service")]
use itertools::chain;
use tor_error::warn_report;
#[cfg(feature = "hs-common")]
use tor_linkspec::OwnedCircTarget;
use tor_linkspec::{
ChanTarget, DirectChanMethodsHelper, HasAddrs, HasRelayIds, RelayIdRef, RelayIdType,
};
use tor_llcrypto as ll;
use tor_llcrypto::pk::{ed25519::Ed25519Identity, rsa::RsaIdentity};
use tor_netdoc::doc::microdesc::{MdDigest, Microdesc};
use tor_netdoc::doc::netstatus::{self, MdConsensus, MdRouterStatus};
#[cfg(feature = "hs-common")]
use {hsdir_ring::HsDirRing, std::iter};
use derive_more::{From, Into};
use futures::{StreamExt, stream::BoxStream};
use num_enum::{IntoPrimitive, TryFromPrimitive};
use rand::seq::{IndexedRandom as _, SliceRandom as _, WeightError};
use serde::Deserialize;
use std::collections::HashMap;
use std::net::IpAddr;
use std::ops::Deref;
use std::sync::Arc;
use std::time::SystemTime;
use strum::{EnumCount, EnumIter};
use tracing::warn;
use typed_index_collections::{TiSlice, TiVec};
#[cfg(feature = "hs-common")]
use {
itertools::Itertools,
std::collections::HashSet,
std::result::Result as StdResult,
tor_error::{Bug, internal},
tor_hscrypto::{pk::HsBlindId, time::TimePeriod},
tor_linkspec::{OwnedChanTargetBuilder, verbatim::VerbatimLinkSpecCircTarget},
tor_llcrypto::pk::curve25519,
};
pub use err::Error;
pub use weight::WeightRole;
pub type Result<T> = std::result::Result<T, Error>;
#[cfg(feature = "hs-common")]
pub use err::{OnionDirLookupError, VerbatimCircTargetDecodeError};
use params::NetParameters;
#[cfg(feature = "geoip")]
use tor_geoip::{CountryCode, GeoipDb, HasCountryCode};
#[cfg(feature = "hs-common")]
pub use hsdir_params::HsDirParams;
#[derive(Debug, From, Into, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
pub(crate) struct RouterStatusIdx(usize);
pub(crate) trait ConsensusRelays {
fn c_relays(&self) -> &TiSlice<RouterStatusIdx, MdRouterStatus>;
}
impl ConsensusRelays for MdConsensus {
fn c_relays(&self) -> &TiSlice<RouterStatusIdx, MdRouterStatus> {
TiSlice::from_ref(MdConsensus::relays(self))
}
}
impl ConsensusRelays for NetDir {
fn c_relays(&self) -> &TiSlice<RouterStatusIdx, MdRouterStatus> {
self.consensus.c_relays()
}
}
#[derive(Deserialize, Debug, Clone, Copy, Eq, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct SubnetConfig {
subnets_family_v4: u8,
subnets_family_v6: u8,
}
impl Default for SubnetConfig {
fn default() -> Self {
Self::new(16, 32)
}
}
impl SubnetConfig {
pub fn new(subnets_family_v4: u8, subnets_family_v6: u8) -> Self {
Self {
subnets_family_v4,
subnets_family_v6,
}
}
pub fn no_addresses_match() -> SubnetConfig {
SubnetConfig {
subnets_family_v4: 33,
subnets_family_v6: 129,
}
}
pub fn addrs_in_same_subnet(&self, a: &IpAddr, b: &IpAddr) -> bool {
match (a, b) {
(IpAddr::V4(a), IpAddr::V4(b)) => {
let bits = self.subnets_family_v4;
if bits > 32 {
return false;
}
let a = u32::from_be_bytes(a.octets());
let b = u32::from_be_bytes(b.octets());
(a >> (32 - bits)) == (b >> (32 - bits))
}
(IpAddr::V6(a), IpAddr::V6(b)) => {
let bits = self.subnets_family_v6;
if bits > 128 {
return false;
}
let a = u128::from_be_bytes(a.octets());
let b = u128::from_be_bytes(b.octets());
(a >> (128 - bits)) == (b >> (128 - bits))
}
_ => false,
}
}
pub fn any_addrs_in_same_subnet<T, U>(&self, a: &T, b: &U) -> bool
where
T: tor_linkspec::HasAddrs,
U: tor_linkspec::HasAddrs,
{
a.addrs().any(|aa| {
b.addrs()
.any(|bb| self.addrs_in_same_subnet(&aa.ip(), &bb.ip()))
})
}
pub fn union(&self, other: &Self) -> Self {
use std::cmp::min;
Self {
subnets_family_v4: min(self.subnets_family_v4, other.subnets_family_v4),
subnets_family_v6: min(self.subnets_family_v6, other.subnets_family_v6),
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct FamilyRules {
use_family_lists: bool,
use_family_ids: bool,
}
impl<'a> From<&'a NetParameters> for FamilyRules {
fn from(params: &'a NetParameters) -> Self {
FamilyRules {
use_family_lists: bool::from(params.use_family_lists),
use_family_ids: bool::from(params.use_family_ids),
}
}
}
impl FamilyRules {
pub fn all_family_info() -> Self {
Self {
use_family_lists: true,
use_family_ids: true,
}
}
pub fn ignore_declared_families() -> Self {
Self {
use_family_lists: false,
use_family_ids: false,
}
}
pub fn use_family_lists(&mut self, val: bool) -> &mut Self {
self.use_family_lists = val;
self
}
pub fn use_family_ids(&mut self, val: bool) -> &mut Self {
self.use_family_ids = val;
self
}
pub fn union(&self, other: &Self) -> Self {
Self {
use_family_lists: self.use_family_lists || other.use_family_lists,
use_family_ids: self.use_family_ids || other.use_family_ids,
}
}
}
#[derive(
Copy,
Clone,
Debug,
derive_more::Add,
derive_more::Sum,
derive_more::AddAssign,
Eq,
PartialEq,
Ord,
PartialOrd,
)]
pub struct RelayWeight(u64);
impl RelayWeight {
pub fn checked_div(&self, rhs: RelayWeight) -> Option<f64> {
if rhs.0 == 0 {
None
} else {
Some((self.0 as f64) / (rhs.0 as f64))
}
}
pub fn ratio(&self, frac: f64) -> Option<RelayWeight> {
let product = (self.0 as f64) * frac;
if product >= 0.0 && product.is_finite() {
Some(RelayWeight(product as u64))
} else {
None
}
}
}
impl From<u64> for RelayWeight {
fn from(val: u64) -> Self {
RelayWeight(val)
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
#[non_exhaustive]
pub enum HsDirOp {
#[cfg(feature = "hs-service")]
Upload,
Download,
}
#[derive(Debug, Clone)]
pub struct NetDir {
consensus: Arc<MdConsensus>,
params: NetParameters,
mds: TiVec<RouterStatusIdx, Option<Arc<Microdesc>>>,
rsidx_by_missing: HashMap<MdDigest, RouterStatusIdx>,
rsidx_by_ed: HashMap<Ed25519Identity, RouterStatusIdx>,
rsidx_by_rsa: Arc<HashMap<RsaIdentity, RouterStatusIdx>>,
#[cfg(feature = "hs-common")]
hsdir_rings: Arc<HsDirs<HsDirRing>>,
weights: weight::WeightSet,
#[cfg(feature = "geoip")]
country_codes: Vec<Option<CountryCode>>,
}
#[derive(Debug, Clone)]
#[cfg(feature = "hs-common")]
pub(crate) struct HsDirs<D> {
current: D,
#[cfg(feature = "hs-service")]
secondary: Vec<D>,
}
#[cfg(feature = "hs-common")]
impl<D> HsDirs<D> {
pub(crate) fn map<D2>(self, mut f: impl FnMut(D) -> D2) -> HsDirs<D2> {
HsDirs {
current: f(self.current),
#[cfg(feature = "hs-service")]
secondary: self.secondary.into_iter().map(f).collect(),
}
}
fn iter_filter_secondary(&self, secondary: bool) -> impl Iterator<Item = &D> {
let i = iter::once(&self.current);
let _ = secondary;
#[cfg(feature = "hs-service")]
let i = chain!(i, self.secondary.iter().filter(move |_| secondary));
i
}
pub(crate) fn iter(&self) -> impl Iterator<Item = &D> {
self.iter_filter_secondary(true)
}
pub(crate) fn iter_for_op(&self, op: HsDirOp) -> impl Iterator<Item = &D> {
self.iter_filter_secondary(match op {
#[cfg(feature = "hs-service")]
HsDirOp::Upload => true,
HsDirOp::Download => false,
})
}
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, EnumIter, EnumCount, IntoPrimitive, TryFromPrimitive,
)]
#[non_exhaustive]
#[repr(u16)]
pub enum DirEvent {
NewConsensus,
NewDescriptors,
NewProtocolRecommendation,
}
#[derive(Clone, Copy, Debug, thiserror::Error)]
#[error("Network directory provider is shutting down")]
#[non_exhaustive]
pub struct NetdirProviderShutdown;
impl tor_error::HasKind for NetdirProviderShutdown {
fn kind(&self) -> tor_error::ErrorKind {
tor_error::ErrorKind::ArtiShuttingDown
}
}
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
#[allow(clippy::exhaustive_enums)]
pub enum Timeliness {
Strict,
Timely,
Unchecked,
}
#[async_trait]
pub trait NetDirProvider: UpcastArcNetDirProvider + Send + Sync {
fn netdir(&self, timeliness: Timeliness) -> Result<Arc<NetDir>>;
fn timely_netdir(&self) -> Result<Arc<NetDir>> {
self.netdir(Timeliness::Timely)
}
fn events(&self) -> BoxStream<'static, DirEvent>;
fn params(&self) -> Arc<dyn AsRef<NetParameters>>;
async fn wait_for_netdir(
&self,
timeliness: Timeliness,
) -> std::result::Result<Arc<NetDir>, NetdirProviderShutdown> {
if let Ok(nd) = self.netdir(timeliness) {
return Ok(nd);
}
let mut stream = self.events();
loop {
if let Ok(nd) = self.netdir(timeliness) {
return Ok(nd);
}
match stream.next().await {
Some(_) => {}
None => {
return Err(NetdirProviderShutdown);
}
}
}
}
async fn wait_for_netdir_to_list(
&self,
target: &tor_linkspec::RelayIds,
timeliness: Timeliness,
) -> std::result::Result<(), NetdirProviderShutdown> {
let mut events = self.events();
loop {
{
let netdir = self.wait_for_netdir(timeliness).await?;
if netdir.ids_listed(target) == Some(true) {
return Ok(());
}
}
if events.next().await.is_none() {
return Err(NetdirProviderShutdown);
}
}
}
fn protocol_statuses(&self) -> Option<(SystemTime, Arc<netstatus::ProtoStatuses>)>;
}
impl<T> NetDirProvider for Arc<T>
where
T: NetDirProvider,
{
fn netdir(&self, timeliness: Timeliness) -> Result<Arc<NetDir>> {
self.deref().netdir(timeliness)
}
fn timely_netdir(&self) -> Result<Arc<NetDir>> {
self.deref().timely_netdir()
}
fn events(&self) -> BoxStream<'static, DirEvent> {
self.deref().events()
}
fn params(&self) -> Arc<dyn AsRef<NetParameters>> {
self.deref().params()
}
fn protocol_statuses(&self) -> Option<(SystemTime, Arc<netstatus::ProtoStatuses>)> {
self.deref().protocol_statuses()
}
}
pub trait UpcastArcNetDirProvider {
fn upcast_arc<'a>(self: Arc<Self>) -> Arc<dyn NetDirProvider + 'a>
where
Self: 'a;
}
impl<T> UpcastArcNetDirProvider for T
where
T: NetDirProvider + Sized,
{
fn upcast_arc<'a>(self: Arc<Self>) -> Arc<dyn NetDirProvider + 'a>
where
Self: 'a,
{
self
}
}
impl AsRef<NetParameters> for NetDir {
fn as_ref(&self) -> &NetParameters {
self.params()
}
}
#[derive(Debug, Clone)]
pub struct PartialNetDir {
netdir: NetDir,
#[cfg(feature = "hs-common")]
prev_netdir: Option<Arc<NetDir>>,
}
#[derive(Clone)]
pub struct Relay<'a> {
rs: &'a netstatus::MdRouterStatus,
md: &'a Microdesc,
#[cfg(feature = "geoip")]
cc: Option<CountryCode>,
}
#[derive(Debug)]
pub struct UncheckedRelay<'a> {
rs: &'a netstatus::MdRouterStatus,
md: Option<&'a Microdesc>,
#[cfg(feature = "geoip")]
cc: Option<CountryCode>,
}
pub trait MdReceiver {
fn missing_microdescs(&self) -> Box<dyn Iterator<Item = &MdDigest> + '_>;
fn add_microdesc(&mut self, md: Microdesc) -> bool;
fn n_missing(&self) -> usize;
}
impl PartialNetDir {
pub fn new(
consensus: MdConsensus,
replacement_params: Option<&netstatus::NetParams<i32>>,
) -> Self {
Self::new_inner(
consensus,
replacement_params,
#[cfg(feature = "geoip")]
None,
)
}
#[cfg(feature = "geoip")]
pub fn new_with_geoip(
consensus: MdConsensus,
replacement_params: Option<&netstatus::NetParams<i32>>,
geoip_db: &GeoipDb,
) -> Self {
Self::new_inner(consensus, replacement_params, Some(geoip_db))
}
fn new_inner(
consensus: MdConsensus,
replacement_params: Option<&netstatus::NetParams<i32>>,
#[cfg(feature = "geoip")] geoip_db: Option<&GeoipDb>,
) -> Self {
let mut params = NetParameters::default();
let _ = params.saturating_update(consensus.params().iter());
if let Some(replacement) = replacement_params {
for u in params.saturating_update(replacement.iter()) {
warn!("Unrecognized option: override_net_params.{}", u);
}
}
let weights = weight::WeightSet::from_consensus(&consensus, ¶ms);
let n_relays = consensus.c_relays().len();
let rsidx_by_missing = consensus
.c_relays()
.iter_enumerated()
.map(|(rsidx, rs)| (*rs.md_digest(), rsidx))
.collect();
let rsidx_by_rsa = consensus
.c_relays()
.iter_enumerated()
.map(|(rsidx, rs)| (*rs.rsa_identity(), rsidx))
.collect();
#[cfg(feature = "geoip")]
let country_codes = if let Some(db) = geoip_db {
consensus
.c_relays()
.iter()
.map(|rs| {
db.lookup_country_code_multi(rs.addrs().map(|x| x.ip()))
.cloned()
})
.collect()
} else {
Default::default()
};
#[cfg(feature = "hs-common")]
let hsdir_rings = Arc::new({
let params = HsDirParams::compute(&consensus, ¶ms).expect("Invalid consensus!");
params.map(HsDirRing::empty_from_params)
});
let netdir = NetDir {
consensus: Arc::new(consensus),
params,
mds: vec![None; n_relays].into(),
rsidx_by_missing,
rsidx_by_rsa: Arc::new(rsidx_by_rsa),
rsidx_by_ed: HashMap::with_capacity(n_relays),
#[cfg(feature = "hs-common")]
hsdir_rings,
weights,
#[cfg(feature = "geoip")]
country_codes,
};
PartialNetDir {
netdir,
#[cfg(feature = "hs-common")]
prev_netdir: None,
}
}
pub fn lifetime(&self) -> &netstatus::Lifetime {
self.netdir.lifetime()
}
#[allow(clippy::needless_pass_by_value)] pub fn fill_from_previous_netdir(&mut self, prev: Arc<NetDir>) {
for md in prev.mds.iter().flatten() {
self.netdir.add_arc_microdesc(md.clone());
}
#[cfg(feature = "hs-common")]
{
self.prev_netdir = Some(prev);
}
}
#[cfg(feature = "hs-common")]
fn compute_rings(&mut self) {
let params = HsDirParams::compute(&self.netdir.consensus, &self.netdir.params)
.expect("Invalid consensus");
self.netdir.hsdir_rings =
Arc::new(params.map(|params| {
HsDirRing::compute(params, &self.netdir, self.prev_netdir.as_deref())
}));
}
pub fn have_enough_paths(&self) -> bool {
self.netdir.have_enough_paths()
}
pub fn unwrap_if_sufficient(
#[allow(unused_mut)] mut self,
) -> std::result::Result<NetDir, PartialNetDir> {
if self.netdir.have_enough_paths() {
#[cfg(feature = "hs-common")]
self.compute_rings();
Ok(self.netdir)
} else {
Err(self)
}
}
}
impl MdReceiver for PartialNetDir {
fn missing_microdescs(&self) -> Box<dyn Iterator<Item = &MdDigest> + '_> {
self.netdir.missing_microdescs()
}
fn add_microdesc(&mut self, md: Microdesc) -> bool {
self.netdir.add_microdesc(md)
}
fn n_missing(&self) -> usize {
self.netdir.n_missing()
}
}
impl NetDir {
pub fn lifetime(&self) -> &netstatus::Lifetime {
self.consensus.lifetime()
}
fn add_arc_microdesc(&mut self, md: Arc<Microdesc>) -> bool {
if let Some(rsidx) = self.rsidx_by_missing.remove(md.digest()) {
assert_eq!(self.c_relays()[rsidx].md_digest(), md.digest());
self.rsidx_by_ed.insert(*md.ed25519_id(), rsidx);
self.mds[rsidx] = Some(md);
if self.rsidx_by_missing.len() < self.rsidx_by_missing.capacity() / 4 {
self.rsidx_by_missing.shrink_to_fit();
}
return true;
}
false
}
fn relay_from_rs_and_rsidx<'a>(
&'a self,
rs: &'a netstatus::MdRouterStatus,
rsidx: RouterStatusIdx,
) -> UncheckedRelay<'a> {
debug_assert_eq!(self.c_relays()[rsidx].rsa_identity(), rs.rsa_identity());
let md = self.mds[rsidx].as_deref();
if let Some(md) = md {
debug_assert_eq!(rs.md_digest(), md.digest());
}
UncheckedRelay {
rs,
md,
#[cfg(feature = "geoip")]
cc: self.country_codes.get(rsidx.0).copied().flatten(),
}
}
#[cfg(feature = "hs-common")]
fn n_replicas(&self) -> u8 {
self.params
.hsdir_n_replicas
.get()
.try_into()
.expect("BoundedInt did not enforce bounds")
}
#[cfg(feature = "hs-common")]
fn spread(&self, op: HsDirOp) -> usize {
let spread = match op {
HsDirOp::Download => self.params.hsdir_spread_fetch,
#[cfg(feature = "hs-service")]
HsDirOp::Upload => self.params.hsdir_spread_store,
};
spread
.get()
.try_into()
.expect("BoundedInt did not enforce bounds!")
}
#[cfg(feature = "hs-common")]
fn select_hsdirs<'h, 'r: 'h>(
&'r self,
hsid: HsBlindId,
ring: &'h HsDirRing,
spread: usize,
) -> impl Iterator<Item = Relay<'r>> + 'h {
let n_replicas = self.n_replicas();
(1..=n_replicas) .flat_map({
let mut selected_nodes = HashSet::new();
move |replica: u8| {
let hsdir_idx = hsdir_ring::service_hsdir_index(&hsid, replica, ring.params());
ring.ring_items_at(hsdir_idx, spread, |(hsdir_idx, _)| {
selected_nodes.insert(*hsdir_idx)
})
.collect::<Vec<_>>()
}
})
.filter_map(move |(_hsdir_idx, rs_idx)| {
self.relay_by_rs_idx(*rs_idx)
})
}
pub fn replace_overridden_parameters(&mut self, new_replacement: &netstatus::NetParams<i32>) {
let mut new_params = NetParameters::default();
let _ = new_params.saturating_update(self.consensus.params().iter());
for u in new_params.saturating_update(new_replacement.iter()) {
warn!("Unrecognized option: override_net_params.{}", u);
}
self.params = new_params;
}
pub fn all_relays(&self) -> impl Iterator<Item = UncheckedRelay<'_>> {
self.c_relays()
.iter_enumerated()
.map(move |(rsidx, rs)| self.relay_from_rs_and_rsidx(rs, rsidx))
}
pub fn relays(&self) -> impl Iterator<Item = Relay<'_>> {
self.all_relays().filter_map(UncheckedRelay::into_relay)
}
#[cfg_attr(not(feature = "hs-common"), allow(dead_code))]
pub(crate) fn md_by_rsidx(&self, rsidx: RouterStatusIdx) -> Option<&Microdesc> {
self.mds.get(rsidx)?.as_deref()
}
pub fn by_id<'a, T>(&self, id: T) -> Option<Relay<'_>>
where
T: Into<RelayIdRef<'a>>,
{
let id = id.into();
let answer = match id {
RelayIdRef::Ed25519(ed25519) => {
let rsidx = *self.rsidx_by_ed.get(ed25519)?;
let rs = self.c_relays().get(rsidx).expect("Corrupt index");
self.relay_from_rs_and_rsidx(rs, rsidx).into_relay()?
}
RelayIdRef::Rsa(rsa) => self
.by_rsa_id_unchecked(rsa)
.and_then(UncheckedRelay::into_relay)?,
other_type => self.relays().find(|r| r.has_identity(other_type))?,
};
assert!(answer.has_identity(id));
Some(answer)
}
#[cfg_attr(not(feature = "hs-common"), allow(dead_code))]
pub(crate) fn relay_by_rs_idx(&self, rs_idx: RouterStatusIdx) -> Option<Relay<'_>> {
let rs = self.c_relays().get(rs_idx)?;
let md = self.mds.get(rs_idx)?.as_deref();
UncheckedRelay {
rs,
md,
#[cfg(feature = "geoip")]
cc: self.country_codes.get(rs_idx.0).copied().flatten(),
}
.into_relay()
}
pub fn by_ids<T>(&self, target: &T) -> Option<Relay<'_>>
where
T: HasRelayIds + ?Sized,
{
let mut identities = target.identities();
let first_id = identities.next()?;
let candidate = self.by_id(first_id)?;
if identities.all(|wanted_id| candidate.has_identity(wanted_id)) {
Some(candidate)
} else {
None
}
}
#[cfg(feature = "hs-common")]
pub fn by_ids_detailed<T>(
&self,
target: &T,
) -> std::result::Result<Option<Relay<'_>>, RelayLookupError>
where
T: HasRelayIds + ?Sized,
{
let candidate = target
.identities()
.filter_map(|id| self.by_id(id))
.unique_by(|r| r.rs.rsa_identity())
.at_most_one()
.map_err(|_| RelayLookupError::Impossible)?;
let candidate = match candidate {
Some(relay) => relay,
None => return Ok(None),
};
if target
.identities()
.all(|wanted_id| match candidate.identity(wanted_id.id_type()) {
None => true,
Some(id) => id == wanted_id,
})
{
Ok(Some(candidate))
} else {
Err(RelayLookupError::Impossible)
}
}
fn id_pair_listed(&self, ed_id: &Ed25519Identity, rsa_id: &RsaIdentity) -> Option<bool> {
let r = self.by_rsa_id_unchecked(rsa_id);
match r {
Some(unchecked) => {
if !unchecked.rs.ed25519_id_is_usable() {
return Some(false);
}
unchecked.md.map(|md| md.ed25519_id() == ed_id)
}
None => {
Some(false)
}
}
}
pub fn ids_listed<T>(&self, target: &T) -> Option<bool>
where
T: HasRelayIds + ?Sized,
{
let rsa_id = target.rsa_identity();
let ed25519_id = target.ed_identity();
const _: () = assert!(RelayIdType::COUNT == 2);
match (rsa_id, ed25519_id) {
(Some(r), Some(e)) => self.id_pair_listed(e, r),
(Some(r), None) => Some(self.rsa_id_is_listed(r)),
(None, Some(e)) => {
if self.rsidx_by_ed.contains_key(e) {
Some(true)
} else {
None
}
}
(None, None) => None,
}
}
#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
#[cfg_attr(docsrs, doc(cfg(feature = "experimental-api")))]
fn by_rsa_id_unchecked(&self, rsa_id: &RsaIdentity) -> Option<UncheckedRelay<'_>> {
let rsidx = *self.rsidx_by_rsa.get(rsa_id)?;
let rs = self.c_relays().get(rsidx).expect("Corrupt index");
assert_eq!(rs.rsa_identity(), rsa_id);
Some(self.relay_from_rs_and_rsidx(rs, rsidx))
}
fn by_rsa_id(&self, rsa_id: &RsaIdentity) -> Option<Relay<'_>> {
self.by_rsa_id_unchecked(rsa_id)?.into_relay()
}
#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
#[cfg_attr(docsrs, doc(cfg(feature = "experimental-api")))]
fn rsa_id_is_listed(&self, rsa_id: &RsaIdentity) -> bool {
self.by_rsa_id_unchecked(rsa_id).is_some()
}
#[cfg(feature = "hs-common")]
fn all_hsdirs(&self) -> impl Iterator<Item = (RouterStatusIdx, Relay<'_>)> {
self.c_relays().iter_enumerated().filter_map(|(rsidx, rs)| {
let relay = self.relay_from_rs_and_rsidx(rs, rsidx);
relay.is_hsdir_for_ring().then_some(())?;
let relay = relay.into_relay()?;
Some((rsidx, relay))
})
}
pub fn params(&self) -> &NetParameters {
&self.params
}
#[cfg(feature = "hs-common")]
pub fn relay_protocol_status(&self) -> &netstatus::ProtoStatus {
self.consensus.relay_protocol_status()
}
#[cfg(feature = "hs-common")]
pub fn client_protocol_status(&self) -> &netstatus::ProtoStatus {
self.consensus.client_protocol_status()
}
#[cfg(feature = "hs-common")]
pub fn circ_target_from_verbatim_linkspecs(
&self,
linkspecs: &[tor_linkspec::EncodedLinkSpec],
ntor_onion_key: &curve25519::PublicKey,
) -> StdResult<VerbatimLinkSpecCircTarget<OwnedCircTarget>, VerbatimCircTargetDecodeError> {
use VerbatimCircTargetDecodeError as E;
use tor_linkspec::CircTarget as _;
use tor_linkspec::decode::Strictness;
let mut bld = OwnedCircTarget::builder();
use tor_error::into_internal;
*bld.chan_target() =
OwnedChanTargetBuilder::from_encoded_linkspecs(Strictness::Standard, linkspecs)?;
let protocols = {
let chan_target = bld.chan_target().build().map_err(into_internal!(
"from_encoded_linkspecs gave an invalid output"
))?;
match self
.by_ids_detailed(&chan_target)
.map_err(E::ImpossibleIds)?
{
Some(relay) => relay.protovers().clone(),
None => self.relay_protocol_status().required_protocols().clone(),
}
};
bld.protocols(protocols);
bld.ntor_onion_key(*ntor_onion_key);
Ok(VerbatimLinkSpecCircTarget::new(
bld.build()
.map_err(into_internal!("Failed to construct a valid circtarget"))?,
linkspecs.to_vec(),
))
}
fn frac_for_role<'a, F>(&'a self, role: WeightRole, usable: F) -> f64
where
F: Fn(&UncheckedRelay<'a>) -> bool,
{
let mut total_weight = 0_u64;
let mut have_weight = 0_u64;
let mut have_count = 0_usize;
let mut total_count = 0_usize;
for r in self.all_relays() {
if !usable(&r) {
continue;
}
let w = self.weights.weight_rs_for_role(r.rs, role);
total_weight += w;
total_count += 1;
if r.is_usable() {
have_weight += w;
have_count += 1;
}
}
if total_weight > 0 {
(have_weight as f64) / (total_weight as f64)
} else if total_count > 0 {
(have_count as f64) / (total_count as f64)
} else {
0.0
}
}
fn frac_usable_paths(&self) -> f64 {
let f_g = self.frac_for_role(WeightRole::Guard, |u| {
u.low_level_details().is_suitable_as_guard()
});
let f_m = self.frac_for_role(WeightRole::Middle, |_| true);
let f_e = if self.all_relays().any(|u| u.rs.is_flagged_exit()) {
self.frac_for_role(WeightRole::Exit, |u| u.rs.is_flagged_exit())
} else {
f_m
};
f_g * f_m * f_e
}
fn have_enough_paths(&self) -> bool {
let min_frac_paths: f64 = self.params().min_circuit_path_threshold.as_fraction();
let available = self.frac_usable_paths();
available >= min_frac_paths
}
pub fn pick_relay<'a, R, P>(
&'a self,
rng: &mut R,
role: WeightRole,
usable: P,
) -> Option<Relay<'a>>
where
R: rand::Rng,
P: FnMut(&Relay<'a>) -> bool,
{
let relays: Vec<_> = self.relays().filter(usable).collect();
match relays[..].choose_weighted(rng, |r| self.weights.weight_rs_for_role(r.rs, role)) {
Ok(relay) => Some(relay.clone()),
Err(WeightError::InsufficientNonZero) => {
if relays.is_empty() {
None
} else {
warn!(?self.weights, ?role,
"After filtering, all {} relays had zero weight. Choosing one at random. See bug #1907.",
relays.len());
relays.choose(rng).cloned()
}
}
Err(e) => {
warn_report!(e, "Unexpected error while sampling a relay");
None
}
}
}
#[allow(clippy::cognitive_complexity)] pub fn pick_n_relays<'a, R, P>(
&'a self,
rng: &mut R,
n: usize,
role: WeightRole,
usable: P,
) -> Vec<Relay<'a>>
where
R: rand::Rng,
P: FnMut(&Relay<'a>) -> bool,
{
let relays: Vec<_> = self.relays().filter(usable).collect();
let mut relays = match relays[..].choose_multiple_weighted(rng, n, |r| {
self.weights.weight_rs_for_role(r.rs, role) as f64
}) {
Err(WeightError::InsufficientNonZero) => {
let remaining: Vec<_> = relays
.iter()
.filter(|r| self.weights.weight_rs_for_role(r.rs, role) > 0)
.cloned()
.collect();
if remaining.is_empty() {
warn!(?self.weights, ?role,
"After filtering, all {} relays had zero weight! Picking some at random. See bug #1907.",
relays.len());
if relays.len() >= n {
relays.choose_multiple(rng, n).cloned().collect()
} else {
relays
}
} else {
warn!(?self.weights, ?role,
"After filtering, only had {}/{} relays with nonzero weight. Returning them all. See bug #1907.",
remaining.len(), relays.len());
remaining
}
}
Err(e) => {
warn_report!(e, "Unexpected error while sampling a set of relays");
Vec::new()
}
Ok(iter) => {
let selection: Vec<_> = iter.map(Relay::clone).collect();
if selection.len() < n && selection.len() < relays.len() {
warn!(?self.weights, ?role,
"choose_multiple_weighted returned only {returned}, despite requesting {n}, \
and having {filtered_len} available after filtering. See bug #1907.",
returned=selection.len(), filtered_len=relays.len());
}
selection
}
};
relays.shuffle(rng);
relays
}
pub fn relay_weight<'a>(&'a self, relay: &Relay<'a>, role: WeightRole) -> RelayWeight {
RelayWeight(self.weights.weight_rs_for_role(relay.rs, role))
}
pub fn total_weight<P>(&self, role: WeightRole, usable: P) -> RelayWeight
where
P: Fn(&UncheckedRelay<'_>) -> bool,
{
self.all_relays()
.filter_map(|unchecked| {
if usable(&unchecked) {
Some(RelayWeight(
self.weights.weight_rs_for_role(unchecked.rs, role),
))
} else {
None
}
})
.sum()
}
pub fn weight_by_rsa_id(&self, rsa_id: &RsaIdentity, role: WeightRole) -> Option<RelayWeight> {
self.by_rsa_id_unchecked(rsa_id)
.map(|unchecked| RelayWeight(self.weights.weight_rs_for_role(unchecked.rs, role)))
}
pub fn known_family_members<'a>(
&'a self,
relay: &'a Relay<'a>,
) -> impl Iterator<Item = Relay<'a>> {
let relay_rsa_id = relay.rsa_id();
relay.md.family().members().filter_map(move |other_rsa_id| {
self.by_rsa_id(other_rsa_id)
.filter(|other_relay| other_relay.md.family().contains(relay_rsa_id))
})
}
#[cfg(feature = "hs-common")]
pub fn hs_time_period(&self) -> TimePeriod {
self.hsdir_rings.current.time_period()
}
#[cfg(feature = "hs-service")]
pub fn hs_all_time_periods(&self) -> Vec<HsDirParams> {
self.hsdir_rings
.iter()
.map(|r| r.params().clone())
.collect()
}
#[cfg(feature = "hs-common")]
pub fn hs_dirs_download<'r, R>(
&'r self,
hsid: HsBlindId,
period: TimePeriod,
rng: &mut R,
) -> std::result::Result<Vec<Relay<'r>>, Bug>
where
R: rand::Rng,
{
let spread = self.spread(HsDirOp::Download);
let ring = &self.hsdir_rings.current;
if ring.params().time_period != period {
return Err(internal!(
"our current ring is not associated with the requested time period!"
));
}
let mut hs_dirs = self.select_hsdirs(hsid, ring, spread).collect_vec();
hs_dirs.shuffle(rng);
Ok(hs_dirs)
}
#[cfg(feature = "hs-service")]
pub fn hs_dirs_upload(
&self,
hsid: HsBlindId,
period: TimePeriod,
) -> std::result::Result<impl Iterator<Item = Relay<'_>>, Bug> {
let spread = self.spread(HsDirOp::Upload);
let rings = self
.hsdir_rings
.iter()
.filter_map(move |ring| {
(ring.params().time_period == period).then_some((ring, hsid, period))
})
.collect::<Vec<_>>();
if !rings.iter().any(|(_, _, tp)| *tp == period) {
return Err(internal!(
"the specified time period does not have an associated ring"
));
};
Ok(rings.into_iter().flat_map(move |(ring, hsid, period)| {
assert_eq!(period, ring.params().time_period());
self.select_hsdirs(hsid, ring, spread)
}))
}
#[cfg(feature = "hs-common")]
#[deprecated(note = "Use hs_dirs_upload or hs_dirs_download instead")]
pub fn hs_dirs<'r, R>(&'r self, hsid: &HsBlindId, op: HsDirOp, rng: &mut R) -> Vec<Relay<'r>>
where
R: rand::Rng,
{
let n_replicas = self
.params
.hsdir_n_replicas
.get()
.try_into()
.expect("BoundedInt did not enforce bounds");
let spread = match op {
HsDirOp::Download => self.params.hsdir_spread_fetch,
#[cfg(feature = "hs-service")]
HsDirOp::Upload => self.params.hsdir_spread_store,
};
let spread = spread
.get()
.try_into()
.expect("BoundedInt did not enforce bounds!");
let mut hs_dirs = self
.hsdir_rings
.iter_for_op(op)
.cartesian_product(1..=n_replicas) .flat_map({
let mut selected_nodes = HashSet::new();
move |(ring, replica): (&HsDirRing, u8)| {
let hsdir_idx = hsdir_ring::service_hsdir_index(hsid, replica, ring.params());
ring.ring_items_at(hsdir_idx, spread, |(hsdir_idx, _)| {
selected_nodes.insert(*hsdir_idx)
})
.collect::<Vec<_>>()
}
})
.filter_map(|(_hsdir_idx, rs_idx)| {
self.relay_by_rs_idx(*rs_idx)
})
.collect_vec();
match op {
HsDirOp::Download => {
hs_dirs.shuffle(rng);
}
#[cfg(feature = "hs-service")]
HsDirOp::Upload => {
}
}
hs_dirs
}
}
impl MdReceiver for NetDir {
fn missing_microdescs(&self) -> Box<dyn Iterator<Item = &MdDigest> + '_> {
Box::new(self.rsidx_by_missing.keys())
}
fn add_microdesc(&mut self, md: Microdesc) -> bool {
self.add_arc_microdesc(Arc::new(md))
}
fn n_missing(&self) -> usize {
self.rsidx_by_missing.len()
}
}
impl<'a> UncheckedRelay<'a> {
pub fn low_level_details(&self) -> details::UncheckedRelayDetails<'_> {
details::UncheckedRelayDetails(self)
}
pub fn is_usable(&self) -> bool {
self.md.is_some() && self.rs.ed25519_id_is_usable()
}
pub fn into_relay(self) -> Option<Relay<'a>> {
if self.is_usable() {
Some(Relay {
rs: self.rs,
md: self.md?,
#[cfg(feature = "geoip")]
cc: self.cc,
})
} else {
None
}
}
#[cfg(feature = "hs-common")]
pub(crate) fn is_hsdir_for_ring(&self) -> bool {
self.rs.is_flagged_hsdir()
}
}
impl<'a> Relay<'a> {
pub fn low_level_details(&self) -> details::RelayDetails<'_> {
details::RelayDetails(self)
}
pub fn id(&self) -> &Ed25519Identity {
self.md.ed25519_id()
}
pub fn rsa_id(&self) -> &RsaIdentity {
self.rs.rsa_identity()
}
#[cfg(feature = "experimental-api")]
pub fn rs(&self) -> &netstatus::MdRouterStatus {
self.rs
}
#[cfg(feature = "experimental-api")]
pub fn md(&self) -> &Microdesc {
self.md
}
}
#[cfg(feature = "hs-common")]
#[derive(Clone, Debug, thiserror::Error)]
#[non_exhaustive]
pub enum RelayLookupError {
#[error("Provided set of identities is impossible according to consensus.")]
Impossible,
}
impl<'a> HasAddrs for Relay<'a> {
fn addrs(&self) -> impl Iterator<Item = std::net::SocketAddr> {
self.rs.addrs()
}
}
#[cfg(feature = "geoip")]
impl<'a> HasCountryCode for Relay<'a> {
fn country_code(&self) -> Option<CountryCode> {
self.cc
}
}
impl<'a> tor_linkspec::HasRelayIdsLegacy for Relay<'a> {
fn ed_identity(&self) -> &Ed25519Identity {
self.id()
}
fn rsa_identity(&self) -> &RsaIdentity {
self.rsa_id()
}
}
impl<'a> HasRelayIds for UncheckedRelay<'a> {
fn identity(&self, key_type: RelayIdType) -> Option<RelayIdRef<'_>> {
match key_type {
RelayIdType::Ed25519 if self.rs.ed25519_id_is_usable() => {
self.md.map(|m| m.ed25519_id().into())
}
RelayIdType::Rsa => Some(self.rs.rsa_identity().into()),
_ => None,
}
}
}
#[cfg(feature = "geoip")]
impl<'a> HasCountryCode for UncheckedRelay<'a> {
fn country_code(&self) -> Option<CountryCode> {
self.cc
}
}
impl<'a> DirectChanMethodsHelper for Relay<'a> {}
impl<'a> ChanTarget for Relay<'a> {}
impl<'a> tor_linkspec::CircTarget for Relay<'a> {
fn ntor_onion_key(&self) -> &ll::pk::curve25519::PublicKey {
self.md.ntor_key()
}
fn protovers(&self) -> &tor_protover::Protocols {
self.rs.protovers()
}
}
#[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)]
#![allow(clippy::cognitive_complexity)]
use super::*;
use crate::testnet::*;
use float_eq::assert_float_eq;
use std::collections::HashSet;
use std::time::Duration;
use tor_basic_utils::test_rng::{self, testing_rng};
use tor_linkspec::{RelayIdType, RelayIds};
#[cfg(feature = "hs-common")]
fn dummy_hs_blind_id() -> HsBlindId {
let hsid = [2, 1, 1, 1].iter().cycle().take(32).cloned().collect_vec();
let hsid = Ed25519Identity::new(hsid[..].try_into().unwrap());
HsBlindId::from(hsid)
}
#[test]
fn partial_netdir() {
let (consensus, microdescs) = construct_network().unwrap();
let dir = PartialNetDir::new(consensus, None);
let lifetime = dir.lifetime();
assert_eq!(
lifetime
.valid_until()
.duration_since(lifetime.valid_after())
.unwrap(),
Duration::new(86400, 0)
);
assert!(!dir.have_enough_paths());
let mut dir = match dir.unwrap_if_sufficient() {
Ok(_) => panic!(),
Err(d) => d,
};
let missing: HashSet<_> = dir.missing_microdescs().collect();
assert_eq!(missing.len(), 40);
assert_eq!(missing.len(), dir.netdir.c_relays().len());
for md in µdescs {
assert!(missing.contains(md.digest()));
}
for md in microdescs {
let wanted = dir.add_microdesc(md);
assert!(wanted);
}
let missing: HashSet<_> = dir.missing_microdescs().collect();
assert!(missing.is_empty());
assert!(dir.have_enough_paths());
let _complete = match dir.unwrap_if_sufficient() {
Ok(d) => d,
Err(_) => panic!(),
};
}
#[test]
fn override_params() {
let (consensus, _microdescs) = construct_network().unwrap();
let override_p = "bwweightscale=2 doesnotexist=77 circwindow=500"
.parse()
.unwrap();
let dir = PartialNetDir::new(consensus.clone(), Some(&override_p));
let params = &dir.netdir.params;
assert_eq!(params.bw_weight_scale.get(), 2);
assert_eq!(params.circuit_window.get(), 500_i32);
let dir = PartialNetDir::new(consensus, None);
let params = &dir.netdir.params;
assert_eq!(params.bw_weight_scale.get(), 1_i32);
assert_eq!(params.circuit_window.get(), 1000_i32);
}
#[test]
fn fill_from_previous() {
let (consensus, microdescs) = construct_network().unwrap();
let mut dir = PartialNetDir::new(consensus.clone(), None);
for md in microdescs.iter().skip(2) {
let wanted = dir.add_microdesc(md.clone());
assert!(wanted);
}
let dir1 = dir.unwrap_if_sufficient().unwrap();
assert_eq!(dir1.missing_microdescs().count(), 2);
let mut dir = PartialNetDir::new(consensus, None);
assert_eq!(dir.missing_microdescs().count(), 40);
dir.fill_from_previous_netdir(Arc::new(dir1));
assert_eq!(dir.missing_microdescs().count(), 2);
}
#[test]
fn path_count() {
let low_threshold = "min_paths_for_circs_pct=64".parse().unwrap();
let high_threshold = "min_paths_for_circs_pct=65".parse().unwrap();
let (consensus, microdescs) = construct_network().unwrap();
let mut dir = PartialNetDir::new(consensus.clone(), Some(&low_threshold));
for (pos, md) in microdescs.iter().enumerate() {
if pos % 7 == 2 {
continue; }
dir.add_microdesc(md.clone());
}
let dir = dir.unwrap_if_sufficient().unwrap();
assert_eq!(dir.all_relays().count(), 40);
assert_eq!(dir.relays().count(), 34);
let f = dir.frac_for_role(WeightRole::Guard, |u| u.rs.is_flagged_guard());
assert!(((97.0 / 110.0) - f).abs() < 0.000001);
let f = dir.frac_for_role(WeightRole::Exit, |u| u.rs.is_flagged_exit());
assert!(((94.0 / 110.0) - f).abs() < 0.000001);
let f = dir.frac_for_role(WeightRole::Middle, |_| true);
assert!(((187.0 / 220.0) - f).abs() < 0.000001);
let f = dir.frac_usable_paths();
assert!((f - 0.64052066).abs() < 0.000001);
let mut dir = PartialNetDir::new(consensus, Some(&high_threshold));
for (pos, md) in microdescs.into_iter().enumerate() {
if pos % 7 == 2 {
continue; }
dir.add_microdesc(md);
}
assert!(dir.unwrap_if_sufficient().is_err());
}
fn testing_rng_with_tolerances() -> (impl rand::Rng, usize, f64) {
let config = test_rng::Config::from_env().unwrap_or(test_rng::Config::Deterministic);
let (iters, tolerance) = match config {
test_rng::Config::Deterministic => (5000, 0.02),
_ => (50000, 0.01),
};
(config.into_rng(), iters, tolerance)
}
#[test]
fn test_pick() {
let (consensus, microdescs) = construct_network().unwrap();
let mut dir = PartialNetDir::new(consensus, None);
for md in microdescs.into_iter() {
let wanted = dir.add_microdesc(md.clone());
assert!(wanted);
}
let dir = dir.unwrap_if_sufficient().unwrap();
let (mut rng, total, tolerance) = testing_rng_with_tolerances();
let mut picked = [0_isize; 40];
for _ in 0..total {
let r = dir.pick_relay(&mut rng, WeightRole::Middle, |r| {
r.low_level_details().supports_exit_port_ipv4(80)
});
let r = r.unwrap();
let id_byte = r.identity(RelayIdType::Rsa).unwrap().as_bytes()[0];
picked[id_byte as usize] += 1;
}
picked[0..10].iter().for_each(|x| assert_eq!(*x, 0));
picked[20..30].iter().for_each(|x| assert_eq!(*x, 0));
let picked_f: Vec<_> = picked.iter().map(|x| *x as f64 / total as f64).collect();
assert_float_eq!(picked_f[19], (10.0 / 110.0), abs <= tolerance);
assert_float_eq!(picked_f[38], (9.0 / 110.0), abs <= tolerance);
assert_float_eq!(picked_f[39], (10.0 / 110.0), abs <= tolerance);
}
#[test]
fn test_pick_multiple() {
let dir = construct_netdir().unwrap_if_sufficient().unwrap();
let (mut rng, total, tolerance) = testing_rng_with_tolerances();
let mut picked = [0_isize; 40];
for _ in 0..total / 4 {
let relays = dir.pick_n_relays(&mut rng, 4, WeightRole::Middle, |r| {
r.low_level_details().supports_exit_port_ipv4(80)
});
assert_eq!(relays.len(), 4);
for r in relays {
let id_byte = r.identity(RelayIdType::Rsa).unwrap().as_bytes()[0];
picked[id_byte as usize] += 1;
}
}
picked[0..10].iter().for_each(|x| assert_eq!(*x, 0));
picked[20..30].iter().for_each(|x| assert_eq!(*x, 0));
let picked_f: Vec<_> = picked.iter().map(|x| *x as f64 / total as f64).collect();
assert_float_eq!(picked_f[19], (10.0 / 110.0), abs <= tolerance);
assert_float_eq!(picked_f[36], (7.0 / 110.0), abs <= tolerance);
assert_float_eq!(picked_f[39], (10.0 / 110.0), abs <= tolerance);
}
#[test]
fn subnets() {
let cfg = SubnetConfig::default();
fn same_net(cfg: &SubnetConfig, a: &str, b: &str) -> bool {
cfg.addrs_in_same_subnet(&a.parse().unwrap(), &b.parse().unwrap())
}
assert!(same_net(&cfg, "127.15.3.3", "127.15.9.9"));
assert!(!same_net(&cfg, "127.15.3.3", "127.16.9.9"));
assert!(!same_net(&cfg, "127.15.3.3", "127::"));
assert!(same_net(&cfg, "ffff:ffff:90:33::", "ffff:ffff:91:34::"));
assert!(!same_net(&cfg, "ffff:ffff:90:33::", "ffff:fffe:91:34::"));
let cfg = SubnetConfig {
subnets_family_v4: 32,
subnets_family_v6: 128,
};
assert!(!same_net(&cfg, "127.15.3.3", "127.15.9.9"));
assert!(!same_net(&cfg, "ffff:ffff:90:33::", "ffff:ffff:91:34::"));
assert!(same_net(&cfg, "127.0.0.1", "127.0.0.1"));
assert!(!same_net(&cfg, "127.0.0.1", "127.0.0.2"));
assert!(same_net(&cfg, "ffff:ffff:90:33::", "ffff:ffff:90:33::"));
let cfg = SubnetConfig {
subnets_family_v4: 33,
subnets_family_v6: 129,
};
assert!(!same_net(&cfg, "127.0.0.1", "127.0.0.1"));
assert!(!same_net(&cfg, "::", "::"));
}
#[test]
fn subnet_union() {
let cfg1 = SubnetConfig {
subnets_family_v4: 16,
subnets_family_v6: 64,
};
let cfg2 = SubnetConfig {
subnets_family_v4: 24,
subnets_family_v6: 32,
};
let a1 = "1.2.3.4".parse().unwrap();
let a2 = "1.2.10.10".parse().unwrap();
let a3 = "ffff:ffff::7".parse().unwrap();
let a4 = "ffff:ffff:1234::8".parse().unwrap();
assert_eq!(cfg1.addrs_in_same_subnet(&a1, &a2), true);
assert_eq!(cfg2.addrs_in_same_subnet(&a1, &a2), false);
assert_eq!(cfg1.addrs_in_same_subnet(&a3, &a4), false);
assert_eq!(cfg2.addrs_in_same_subnet(&a3, &a4), true);
let cfg_u = cfg1.union(&cfg2);
assert_eq!(
cfg_u,
SubnetConfig {
subnets_family_v4: 16,
subnets_family_v6: 32,
}
);
assert_eq!(cfg_u.addrs_in_same_subnet(&a1, &a2), true);
assert_eq!(cfg_u.addrs_in_same_subnet(&a3, &a4), true);
assert_eq!(cfg1.union(&cfg1), cfg1);
assert_eq!(cfg1.union(&SubnetConfig::no_addresses_match()), cfg1);
}
#[test]
fn relay_funcs() {
let (consensus, microdescs) = construct_custom_network(
|pos, nb, _| {
if pos == 15 {
nb.rs.add_or_port("[f0f0::30]:9001".parse().unwrap());
} else if pos == 20 {
nb.rs.add_or_port("[f0f0::3131]:9001".parse().unwrap());
}
},
None,
)
.unwrap();
let subnet_config = SubnetConfig::default();
let all_family_info = FamilyRules::all_family_info();
let mut dir = PartialNetDir::new(consensus, None);
for md in microdescs.into_iter() {
let wanted = dir.add_microdesc(md.clone());
assert!(wanted);
}
let dir = dir.unwrap_if_sufficient().unwrap();
let k0 = Ed25519Identity::from([0; 32]);
let k1 = Ed25519Identity::from([1; 32]);
let k2 = Ed25519Identity::from([2; 32]);
let k3 = Ed25519Identity::from([3; 32]);
let k10 = Ed25519Identity::from([10; 32]);
let k15 = Ed25519Identity::from([15; 32]);
let k20 = Ed25519Identity::from([20; 32]);
let r0 = dir.by_id(&k0).unwrap();
let r1 = dir.by_id(&k1).unwrap();
let r2 = dir.by_id(&k2).unwrap();
let r3 = dir.by_id(&k3).unwrap();
let r10 = dir.by_id(&k10).unwrap();
let r15 = dir.by_id(&k15).unwrap();
let r20 = dir.by_id(&k20).unwrap();
assert_eq!(r0.id(), &[0; 32].into());
assert_eq!(r0.rsa_id(), &[0; 20].into());
assert_eq!(r1.id(), &[1; 32].into());
assert_eq!(r1.rsa_id(), &[1; 20].into());
assert!(r0.same_relay_ids(&r0));
assert!(r1.same_relay_ids(&r1));
assert!(!r1.same_relay_ids(&r0));
assert!(r0.low_level_details().is_dir_cache());
assert!(!r1.low_level_details().is_dir_cache());
assert!(r2.low_level_details().is_dir_cache());
assert!(!r3.low_level_details().is_dir_cache());
assert!(!r0.low_level_details().supports_exit_port_ipv4(80));
assert!(!r1.low_level_details().supports_exit_port_ipv4(80));
assert!(!r2.low_level_details().supports_exit_port_ipv4(80));
assert!(!r3.low_level_details().supports_exit_port_ipv4(80));
assert!(!r0.low_level_details().policies_allow_some_port());
assert!(!r1.low_level_details().policies_allow_some_port());
assert!(!r2.low_level_details().policies_allow_some_port());
assert!(!r3.low_level_details().policies_allow_some_port());
assert!(r10.low_level_details().policies_allow_some_port());
assert!(r0.low_level_details().in_same_family(&r0, all_family_info));
assert!(r0.low_level_details().in_same_family(&r1, all_family_info));
assert!(r1.low_level_details().in_same_family(&r0, all_family_info));
assert!(r1.low_level_details().in_same_family(&r1, all_family_info));
assert!(!r0.low_level_details().in_same_family(&r2, all_family_info));
assert!(!r2.low_level_details().in_same_family(&r0, all_family_info));
assert!(r2.low_level_details().in_same_family(&r2, all_family_info));
assert!(r2.low_level_details().in_same_family(&r3, all_family_info));
assert!(r0.low_level_details().in_same_subnet(&r10, &subnet_config));
assert!(r10.low_level_details().in_same_subnet(&r10, &subnet_config));
assert!(r0.low_level_details().in_same_subnet(&r0, &subnet_config));
assert!(r1.low_level_details().in_same_subnet(&r1, &subnet_config));
assert!(!r1.low_level_details().in_same_subnet(&r2, &subnet_config));
assert!(!r2.low_level_details().in_same_subnet(&r3, &subnet_config));
let subnet_config = SubnetConfig {
subnets_family_v4: 128,
subnets_family_v6: 96,
};
assert!(r15.low_level_details().in_same_subnet(&r20, &subnet_config));
assert!(!r15.low_level_details().in_same_subnet(&r1, &subnet_config));
let subnet_config = SubnetConfig {
subnets_family_v4: 255,
subnets_family_v6: 255,
};
assert!(!r15.low_level_details().in_same_subnet(&r20, &subnet_config));
}
#[test]
fn test_badexit() {
use tor_netdoc::types::relay_flags::RelayFlag;
let netdir = construct_custom_netdir(|pos, nb, _| {
if (10..20).contains(&pos) {
nb.rs.add_flags(RelayFlag::BadExit);
}
nb.md.parse_ipv6_policy("accept 443").unwrap();
})
.unwrap()
.unwrap_if_sufficient()
.unwrap();
let e12 = netdir.by_id(&Ed25519Identity::from([12; 32])).unwrap();
let e32 = netdir.by_id(&Ed25519Identity::from([32; 32])).unwrap();
assert!(!e12.low_level_details().supports_exit_port_ipv4(80));
assert!(e32.low_level_details().supports_exit_port_ipv4(80));
assert!(!e12.low_level_details().supports_exit_port_ipv6(443));
assert!(e32.low_level_details().supports_exit_port_ipv6(443));
assert!(!e32.low_level_details().supports_exit_port_ipv6(555));
assert!(!e12.low_level_details().policies_allow_some_port());
assert!(e32.low_level_details().policies_allow_some_port());
assert!(!e12.low_level_details().ipv4_policy().allows_some_port());
assert!(!e12.low_level_details().ipv6_policy().allows_some_port());
assert!(e32.low_level_details().ipv4_policy().allows_some_port());
assert!(e32.low_level_details().ipv6_policy().allows_some_port());
assert!(
e12.low_level_details()
.ipv4_declared_policy()
.allows_some_port()
);
assert!(
e12.low_level_details()
.ipv6_declared_policy()
.allows_some_port()
);
}
#[cfg(feature = "experimental-api")]
#[test]
fn test_accessors() {
let netdir = construct_netdir().unwrap_if_sufficient().unwrap();
let r4 = netdir.by_id(&Ed25519Identity::from([4; 32])).unwrap();
let r16 = netdir.by_id(&Ed25519Identity::from([16; 32])).unwrap();
assert!(!r4.md().ipv4_policy().allows_some_port());
assert!(r16.md().ipv4_policy().allows_some_port());
assert!(!r4.rs().is_flagged_exit());
assert!(r16.rs().is_flagged_exit());
}
#[test]
fn test_by_id() {
let netdir = construct_custom_netdir(|pos, nb, _| {
nb.omit_md = pos == 13;
})
.unwrap();
let netdir = netdir.unwrap_if_sufficient().unwrap();
let r = netdir.by_id(&Ed25519Identity::from([0; 32])).unwrap();
assert_eq!(r.id().as_bytes(), &[0; 32]);
assert!(netdir.by_id(&Ed25519Identity::from([13; 32])).is_none());
let r = netdir.by_rsa_id(&[12; 20].into()).unwrap();
assert_eq!(r.rsa_id().as_bytes(), &[12; 20]);
assert!(netdir.rsa_id_is_listed(&[12; 20].into()));
assert!(netdir.by_rsa_id(&[13; 20].into()).is_none());
assert!(netdir.by_rsa_id_unchecked(&[99; 20].into()).is_none());
assert!(!netdir.rsa_id_is_listed(&[99; 20].into()));
let r = netdir.by_rsa_id_unchecked(&[13; 20].into()).unwrap();
assert_eq!(r.rs.rsa_identity().as_bytes(), &[13; 20]);
assert!(netdir.rsa_id_is_listed(&[13; 20].into()));
let pair_13_13 = RelayIds::builder()
.ed_identity([13; 32].into())
.rsa_identity([13; 20].into())
.build()
.unwrap();
let pair_14_14 = RelayIds::builder()
.ed_identity([14; 32].into())
.rsa_identity([14; 20].into())
.build()
.unwrap();
let pair_14_99 = RelayIds::builder()
.ed_identity([14; 32].into())
.rsa_identity([99; 20].into())
.build()
.unwrap();
let r = netdir.by_ids(&pair_13_13);
assert!(r.is_none());
let r = netdir.by_ids(&pair_14_14).unwrap();
assert_eq!(r.identity(RelayIdType::Rsa).unwrap().as_bytes(), &[14; 20]);
assert_eq!(
r.identity(RelayIdType::Ed25519).unwrap().as_bytes(),
&[14; 32]
);
let r = netdir.by_ids(&pair_14_99);
assert!(r.is_none());
assert_eq!(
netdir.id_pair_listed(&[13; 32].into(), &[13; 20].into()),
None
);
assert_eq!(
netdir.id_pair_listed(&[15; 32].into(), &[15; 20].into()),
Some(true)
);
assert_eq!(
netdir.id_pair_listed(&[15; 32].into(), &[99; 20].into()),
Some(false)
);
}
#[test]
#[cfg(feature = "hs-common")]
fn test_by_ids_detailed() {
let netdir = construct_custom_netdir(|pos, nb, _| {
nb.omit_md = pos == 13;
})
.unwrap();
let netdir = netdir.unwrap_if_sufficient().unwrap();
let id13_13 = RelayIds::builder()
.ed_identity([13; 32].into())
.rsa_identity([13; 20].into())
.build()
.unwrap();
let id15_15 = RelayIds::builder()
.ed_identity([15; 32].into())
.rsa_identity([15; 20].into())
.build()
.unwrap();
let id15_99 = RelayIds::builder()
.ed_identity([15; 32].into())
.rsa_identity([99; 20].into())
.build()
.unwrap();
let id99_15 = RelayIds::builder()
.ed_identity([99; 32].into())
.rsa_identity([15; 20].into())
.build()
.unwrap();
let id99_99 = RelayIds::builder()
.ed_identity([99; 32].into())
.rsa_identity([99; 20].into())
.build()
.unwrap();
let id15_xx = RelayIds::builder()
.ed_identity([15; 32].into())
.build()
.unwrap();
let idxx_15 = RelayIds::builder()
.rsa_identity([15; 20].into())
.build()
.unwrap();
assert!(matches!(netdir.by_ids_detailed(&id13_13), Ok(None)));
assert!(matches!(netdir.by_ids_detailed(&id15_15), Ok(Some(_))));
assert!(matches!(
netdir.by_ids_detailed(&id15_99),
Err(RelayLookupError::Impossible)
));
assert!(matches!(
netdir.by_ids_detailed(&id99_15),
Err(RelayLookupError::Impossible)
));
assert!(matches!(netdir.by_ids_detailed(&id99_99), Ok(None)));
assert!(matches!(netdir.by_ids_detailed(&id15_xx), Ok(Some(_))));
assert!(matches!(netdir.by_ids_detailed(&idxx_15), Ok(Some(_))));
}
#[test]
fn weight_type() {
let r0 = RelayWeight(0);
let r100 = RelayWeight(100);
let r200 = RelayWeight(200);
let r300 = RelayWeight(300);
assert_eq!(r100 + r200, r300);
assert_eq!(r100.checked_div(r200), Some(0.5));
assert!(r100.checked_div(r0).is_none());
assert_eq!(r200.ratio(0.5), Some(r100));
assert!(r200.ratio(-1.0).is_none());
}
#[test]
fn weight_accessors() {
let netdir = construct_netdir().unwrap_if_sufficient().unwrap();
let g_total = netdir.total_weight(WeightRole::Guard, |r| r.rs.is_flagged_guard());
assert_eq!(g_total, RelayWeight(110_000));
let g_total = netdir.total_weight(WeightRole::Guard, |_| false);
assert_eq!(g_total, RelayWeight(0));
let relay = netdir.by_id(&Ed25519Identity::from([35; 32])).unwrap();
assert!(relay.rs.is_flagged_guard());
let w = netdir.relay_weight(&relay, WeightRole::Guard);
assert_eq!(w, RelayWeight(6_000));
let w = netdir
.weight_by_rsa_id(&[33; 20].into(), WeightRole::Guard)
.unwrap();
assert_eq!(w, RelayWeight(4_000));
assert!(
netdir
.weight_by_rsa_id(&[99; 20].into(), WeightRole::Guard)
.is_none()
);
}
#[test]
fn family_list() {
let netdir = construct_custom_netdir(|pos, n, _| {
if pos == 0x0a {
n.md.family(
"$0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B \
$0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C \
$0D0D0D0D0D0D0D0D0D0D0D0D0D0D0D0D0D0D0D0D"
.parse()
.unwrap(),
);
} else if pos == 0x0c {
n.md.family("$0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A".parse().unwrap());
}
})
.unwrap()
.unwrap_if_sufficient()
.unwrap();
let r0 = netdir.by_id(&Ed25519Identity::from([0; 32])).unwrap();
let family: Vec<_> = netdir.known_family_members(&r0).collect();
assert_eq!(family.len(), 1);
assert_eq!(family[0].id(), &Ed25519Identity::from([1; 32]));
let r10 = netdir.by_id(&Ed25519Identity::from([10; 32])).unwrap();
let family: HashSet<_> = netdir.known_family_members(&r10).map(|r| *r.id()).collect();
assert_eq!(family.len(), 2);
assert!(family.contains(&Ed25519Identity::from([11; 32])));
assert!(family.contains(&Ed25519Identity::from([12; 32])));
}
#[test]
#[cfg(feature = "geoip")]
fn relay_has_country_code() {
let src_v6 = r#"
fe80:dead:beef::,fe80:dead:ffff::,US
fe80:feed:eeee::1,fe80:feed:eeee::2,AT
fe80:feed:eeee::2,fe80:feed:ffff::,DE
"#;
let db = GeoipDb::new_from_legacy_format("", src_v6).unwrap();
let netdir = construct_custom_netdir_with_geoip(
|pos, n, _| {
if pos == 0x01 {
n.rs.add_or_port("[fe80:dead:beef::1]:42".parse().unwrap());
}
if pos == 0x02 {
n.rs.add_or_port("[fe80:feed:eeee::1]:42".parse().unwrap());
n.rs.add_or_port("[fe80:feed:eeee::2]:42".parse().unwrap());
}
if pos == 0x03 {
n.rs.add_or_port("[fe80:dead:beef::1]:42".parse().unwrap());
n.rs.add_or_port("[fe80:dead:beef::2]:42".parse().unwrap());
}
},
&db,
)
.unwrap()
.unwrap_if_sufficient()
.unwrap();
let r0 = netdir.by_id(&Ed25519Identity::from([0; 32])).unwrap();
assert_eq!(r0.cc, None);
let r1 = netdir.by_id(&Ed25519Identity::from([1; 32])).unwrap();
assert_eq!(r1.cc.as_ref().map(|x| x.as_ref()), Some("US"));
let r2 = netdir.by_id(&Ed25519Identity::from([2; 32])).unwrap();
assert_eq!(r2.cc, None);
let r3 = netdir.by_id(&Ed25519Identity::from([3; 32])).unwrap();
assert_eq!(r3.cc.as_ref().map(|x| x.as_ref()), Some("US"));
}
#[test]
#[cfg(feature = "hs-common")]
#[allow(deprecated)]
fn hs_dirs_selection() {
use tor_basic_utils::test_rng::testing_rng;
const HSDIR_SPREAD_STORE: i32 = 6;
const HSDIR_SPREAD_FETCH: i32 = 2;
const PARAMS: [(&str, i32); 2] = [
("hsdir_spread_store", HSDIR_SPREAD_STORE),
("hsdir_spread_fetch", HSDIR_SPREAD_FETCH),
];
let netdir: Arc<NetDir> =
crate::testnet::construct_custom_netdir_with_params(|_, _, _| {}, PARAMS, None)
.unwrap()
.unwrap_if_sufficient()
.unwrap()
.into();
let hsid = dummy_hs_blind_id();
const OP_RELAY_COUNT: &[(HsDirOp, usize)] = &[
#[cfg(feature = "hs-service")]
(HsDirOp::Upload, 10),
(HsDirOp::Download, 4),
];
for (op, relay_count) in OP_RELAY_COUNT {
let relays = netdir.hs_dirs(&hsid, *op, &mut testing_rng());
assert_eq!(relays.len(), *relay_count);
let unique = relays
.iter()
.map(|relay| relay.ed_identity())
.collect::<HashSet<_>>();
assert_eq!(unique.len(), relays.len());
}
}
#[test]
fn zero_weights() {
let items = vec![1, 2, 3];
let mut rng = testing_rng();
let a = items.choose_weighted(&mut rng, |_| 0);
assert!(matches!(a, Err(WeightError::InsufficientNonZero)));
let x = items.choose_multiple_weighted(&mut rng, 2, |_| 0);
let xs: Vec<_> = x.unwrap().collect();
assert!(xs.is_empty());
let only_one = |n: &i32| if *n == 1 { 1 } else { 0 };
let x = items.choose_multiple_weighted(&mut rng, 2, only_one);
let xs: Vec<_> = x.unwrap().collect();
assert_eq!(&xs[..], &[&1]);
for _ in 0..100 {
let a = items.choose_weighted(&mut rng, only_one);
assert_eq!(a.unwrap(), &1);
let x = items
.choose_multiple_weighted(&mut rng, 1, only_one)
.unwrap()
.collect::<Vec<_>>();
assert_eq!(x, vec![&1]);
}
}
#[test]
fn insufficient_but_nonzero() {
let items = vec![1, 2, 3];
let mut rng = testing_rng();
let mut a = items
.choose_multiple_weighted(&mut rng, 10, |_| 1)
.unwrap()
.copied()
.collect::<Vec<_>>();
a.sort();
assert_eq!(a, items);
}
}