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