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}