1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194
//! Implement GuardFilter and related types.
use tor_linkspec::ChanTarget;
// TODO(nickm): Conceivably, this type should be exposed from a lower-level crate than
// tor-netdoc.
use tor_netdoc::types::policy::AddrPortPattern;
/// An object specifying which relays are eligible to be guards.
///
/// We _always_ restrict the set of possible guards to be the set of
/// relays currently listed in the consensus directory document, and
/// tagged with the `Guard` flag. But clients may narrow the eligible set
/// even further—for example, to those supporting only a given set of ports,
/// or to those in a given country.
#[derive(Debug, Clone, Default, Eq, PartialEq)]
pub struct GuardFilter {
/// A list of filters to apply to guard or fallback selection. Each filter
/// restricts which guards may be used, and possibly how those guards may be
/// contacted.
///
/// This list of filters has "and" semantics: a relay is permitted by this
/// filter if ALL patterns in this list permit that first hop.
filters: Vec<SingleFilter>,
}
/// A single restriction places upon usable guards.
#[derive(Debug, Clone, Eq, PartialEq)]
enum SingleFilter {
/// A set of allowable addresses that we are willing to try to connect to.
///
/// This list of patterns has "or" semantics: a guard is permitted by this filter
/// if ANY pattern in this list permits one of the guard's addresses.
ReachableAddrs(Vec<AddrPortPattern>),
}
impl GuardFilter {
/// Create a new [`GuardFilter`] that doesn't restrict the set of
/// permissible guards at all.
pub fn unfiltered() -> Self {
GuardFilter::default()
}
/// Restrict this filter to only permit connections to an address permitted
/// by one of the patterns in `addrs`.
pub fn push_reachable_addresses(&mut self, addrs: impl IntoIterator<Item = AddrPortPattern>) {
self.filters
.push(SingleFilter::ReachableAddrs(addrs.into_iter().collect()));
}
/// Return true if this filter permits the provided `target`.
pub(crate) fn permits<C: ChanTarget>(&self, target: &C) -> bool {
self.filters.iter().all(|filt| filt.permits(target))
}
/// Modify `first_hop` so that it contains no elements not permitted by this
/// filter.
///
/// (For example, if we are restricted only to use certain addresses, then
/// `permits` will return true for a guard that has multiple addresses even
/// if _some_ of those addresses are not permitted. In that scenario, this
/// method will remove disallowed addresses from `first_hop`.)
pub(crate) fn modify_hop(
&self,
mut first_hop: crate::FirstHop,
) -> Result<crate::FirstHop, crate::PickGuardError> {
for filt in &self.filters {
first_hop = filt.modify_hop(first_hop)?;
}
Ok(first_hop)
}
/// Return true if this filter excludes no guards at all.
pub(crate) fn is_unfiltered(&self) -> bool {
self.filters.is_empty()
}
/// Return a fraction between 0.0 and 1.0 describing what fraction of the
/// guard bandwidth this filter permits.
pub(crate) fn frac_bw_permitted(&self, netdir: &tor_netdir::NetDir) -> f64 {
use tor_netdir::{RelayWeight, WeightRole};
let mut guard_bw: RelayWeight = 0.into();
let mut permitted_bw: RelayWeight = 0.into();
for relay in netdir.relays() {
if relay.is_flagged_guard() {
let w = netdir.relay_weight(&relay, WeightRole::Guard);
guard_bw += w;
if self.permits(&relay) {
permitted_bw += w;
}
}
}
permitted_bw.checked_div(guard_bw).unwrap_or(1.0)
}
}
impl SingleFilter {
/// Return true if this filter permits the provided target.
fn permits<C: ChanTarget>(&self, target: &C) -> bool {
match self {
SingleFilter::ReachableAddrs(patterns) => {
patterns.iter().any(|pat| {
match target.chan_method().socket_addrs() {
// Check whether _any_ address actually used by this
// method is permitted by _any_ pattern.
Some(addrs) => addrs.iter().any(|addr| pat.matches_sockaddr(addr)),
// This target doesn't use addresses: only hostnames or "None"
None => true,
}
})
}
}
}
/// Modify `first_hop` so that it contains no elements not permitted by this
/// filter.
///
/// It is an internal error to call this function on a guard not already
/// passed by `self.permits()`.
fn modify_hop(
&self,
mut first_hop: crate::FirstHop,
) -> Result<crate::FirstHop, crate::PickGuardError> {
match self {
SingleFilter::ReachableAddrs(patterns) => {
let r = first_hop
.chan_target_mut()
.chan_method_mut()
.retain_addrs(|addr| patterns.iter().any(|pat| pat.matches_sockaddr(addr)));
if r.is_err() {
// TODO(nickm): The fact that this check needs to be checked
// happen indicates a likely problem in our code design.
// Right now, we have `modify_hop` and `permits` as separate
// methods because our GuardSet logic needs a way to check
// whether a guard will be permitted by a filter without
// actually altering that guard (since another filter might
// be used in the future that would allow the same guard).
//
// To mitigate the risk of hitting this error, we try to
// make sure that modify_hop is always called right after
// (or at least soon after) the filter is checked, with the
// same filter object.
return Err(tor_error::internal!(
"Tried to apply an address filter to an unsupported guard"
)
.into());
}
}
}
Ok(first_hop)
}
}
#[cfg(test)]
mod test {
// @@ begin test lint list maintained by maint/add_warning @@
#![allow(clippy::bool_assert_comparison)]
#![allow(clippy::clone_on_copy)]
#![allow(clippy::dbg_macro)]
#![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)]
//! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
use super::*;
use float_eq::assert_float_eq;
use tor_netdir::testnet;
#[test]
fn permissiveness() {
let nd = testnet::construct_netdir().unwrap_if_sufficient().unwrap();
const TOL: f64 = 0.01;
let non_filter = GuardFilter::default();
assert_float_eq!(non_filter.frac_bw_permitted(&nd), 1.0, abs <= TOL);
let forbid_all = {
let mut f = GuardFilter::default();
f.push_reachable_addresses(vec!["*:1".parse().unwrap()]);
f
};
assert_float_eq!(forbid_all.frac_bw_permitted(&nd), 0.0, abs <= TOL);
let net_1_only = {
let mut f = GuardFilter::default();
f.push_reachable_addresses(vec!["1.0.0.0/8:*".parse().unwrap()]);
f
};
assert_float_eq!(net_1_only.frac_bw_permitted(&nd), 54.0 / 330.0, abs <= TOL);
}
}