Skip to main content

tor_netdoc/types/policy/
summary.rs

1//! Precise IP and port policy summarisation algorithm
2//!
3//! We don't use macrology for v4/v6, instead writing things twice.
4//! We don't fear copypasta errors because the type system almost
5//! always prevents mixing v4 and v6 information.
6//!
7//! We're using [`iprange::IpRange`] for our IP address sets.
8//! That *is* a trie, but it's a pretty unoptimised one:
9//! every node is fully boxed and there is no layer elision.
10//! But it *does* have a nice API.
11//!
12//! See [`Summariser`] for the algorithm.
13
14use std::net::{Ipv4Addr, Ipv6Addr};
15
16use derive_deftly::{Deftly, define_derive_deftly};
17use ipnet::{IpNet, Ipv4Net, Ipv6Net};
18use iprange::IpRange;
19use rangemap::RangeInclusiveMap;
20use void::{ResultVoidExt as _, Void};
21
22use crate::rangemap_mutate_range;
23
24use super::*;
25
26//---------- support materials ----------
27
28/// Ports are 16-bit.  Alias for clarity.
29type Port = u16;
30
31/// Range for all real ports (not zero)
32const ALL_PORTS: RangeInclusive<Port> = 1..=u16::MAX;
33
34/// `eprintln` but in tests only, and prefix with `"TPRINT "`
35///
36/// Called in the non-test code in various places, but elided other than actually in tests.
37macro_rules! tprintln { { $($a:tt)* } => { { {
38    #[cfg(test)]
39    eprintln!("TPRINT {}", format_args!($($a)*));
40} } } }
41
42//---------- IpNet trait for dealing with IP version generically ----------
43
44/// `Ipv4Net` or `Ipv6Net` - IP-version specific handling
45trait Net: iprange::IpNet {
46    /// How many bits?
47    const ADDR_BITS: u8;
48
49    /// Return a /0 netblock.
50    fn all() -> Self;
51
52    /// How many hosts in this netblock?
53    ///
54    /// If the answer is 2^128, gives 2^128-1 instead.
55    fn host_count_saturating(&self) -> u128 {
56        let shift = Self::ADDR_BITS - self.prefix_len();
57        1_u128.checked_shl(shift.into()).unwrap_or(u128::MAX)
58    }
59}
60
61impl Net for Ipv4Net {
62    const ADDR_BITS: u8 = 32;
63
64    fn all() -> Self {
65        Ipv4Net::new(Ipv4Addr::UNSPECIFIED, 0).expect("should be OK")
66    }
67}
68
69impl Net for Ipv6Net {
70    const ADDR_BITS: u8 = 128;
71
72    fn all() -> Self {
73        Ipv6Net::new(Ipv6Addr::UNSPECIFIED, 0).expect("should be OK")
74    }
75}
76
77//==================== principal algorithm ====================
78
79//---------- working data structure ----------
80
81/// State for summarisation algorithm
82///
83/// We walk the rules in *reverse order*.
84/// The rules are semantically first-match, but we want to walk *all* the rules,
85/// doing all the port updates in parallel, and updating the accept/reject state
86/// as we go - i.e. last match wins.
87#[derive(Debug)]
88struct Summariser {
89    /// Set of IP addresses we are rejecting for each port
90    ///
91    /// Invariants: port 0 is not in the map.
92    /// Every other port has an entry in the map, even if it's just two empty IpRanges.
93    reject: RangeInclusiveMap<Port, Rejects>,
94}
95
96/// Which V4 and V6 addresses we are rejecting for a particular port
97#[derive(Debug, Default, PartialEq, Clone)]
98struct Rejects {
99    /// Rejections for V4
100    v4: IpRange<Ipv4Net>,
101    /// Rejections for V6
102    v6: IpRange<Ipv6Net>,
103}
104
105//---------- data accumulation into Summariser ----------
106
107impl Summariser {
108    /// Start the summarisation algorithm
109    fn start() -> Self {
110        let mut reject = RangeInclusiveMap::new();
111        reject.insert(ALL_PORTS, Rejects::default());
112        Summariser { reject }
113    }
114
115    /// Apply `rule_kind` for `pat` (for all IP versions) to `self`
116    ///
117    /// Overwrites old information - so last update wins.
118    ///
119    /// Calls `Reject::apply_rule` once for every relevant combination of:
120    ///
121    ///  * port range (via [`rangemap_mutate_range`])
122    ///  * address family (open-coded, two similar calls)
123    fn apply_rule(&mut self, rule_kind: RuleKind, pat: &AddrPortPattern) {
124        let (v4, v6) = match pat.addrs {
125            IpPattern::All => (Some(Net::all()), Some(Net::all())),
126            IpPattern::Net(IpNet::V4(n)) => (Some(n), None),
127            IpPattern::Net(IpNet::V6(n)) => (None, Some(n)),
128        };
129
130        let ports = pat.ports.to_range();
131
132        tprintln!("apply_rule {ports:?} {rule_kind:?} {pat:?}");
133        rangemap_mutate_range(
134            &mut self.reject,
135            &ports,
136            |rejects: &mut Option<Rejects>, ports| {
137                let Some(rejects) = rejects else {
138                    debug_assert_eq!(*ports, 0..=0);
139                    return Ok(());
140                };
141                Rejects::apply_rule(&mut rejects.v4, rule_kind, v4, ports);
142                Rejects::apply_rule(&mut rejects.v6, rule_kind, v6, ports);
143                Ok::<_, Void>(())
144            },
145        )
146        .void_unwrap();
147    }
148}
149
150impl Rejects {
151    /// Apply `rule_kind` for `addrs` to `reject` for IP version `N`
152    fn apply_rule<N: Net>(
153        reject: &mut IpRange<N>,
154        rule_kind: RuleKind,
155        addrs: Option<N>,
156        #[cfg_attr(not(test), allow(unused))] // for debugging prints in tests, only
157        ports: &RangeInclusive<u16>,
158    ) {
159        let Some(addrs) = addrs else {
160            return;
161        };
162        match rule_kind {
163            RuleKind::Accept => reject.remove(addrs),
164            RuleKind::Reject => reject.add(addrs),
165        };
166        tprintln!("apply_rule  {ports:?} {rule_kind:?} {addrs:?} now reject={reject:?}");
167    }
168}
169
170//---------- readout core ----------
171
172impl Summariser {
173    /// Calculate the summary policy for IP version `N`
174    ///
175    /// `select_rejects` should pick the corresponding field out of `Rejects`
176    fn policy_for_one_ip_version<N: Net>(
177        &self,
178        select_rejects: impl Fn(&Rejects) -> &IpRange<N>,
179        max_reject_count: u128,
180    ) -> PortPolicy {
181        let mut allowed = PortRanges::new();
182        for (ports, reject_ranges) in self.reject.iter() {
183            tprintln!(
184                "ports {:20} rej.count,max={max_reject_count:x}",
185                format!("{ports:?}"),
186            );
187            let outcome = 'outcome: {
188                let mut reject_count = 0_u128;
189                for net in select_rejects(reject_ranges) {
190                    reject_count = reject_count.saturating_add(net.host_count_saturating());
191                    tprintln!(
192                        "ports {:20} rej.count,now={reject_count:x} including {net:?}",
193                        format!("{ports:?}"),
194                    );
195                    if reject_count > max_reject_count {
196                        break 'outcome RuleKind::Reject;
197                    }
198                }
199                debug_assert!(reject_count <= max_reject_count);
200                break 'outcome RuleKind::Accept;
201            };
202            tprintln!("ports {:22} {outcome:?}", format!("{ports:?}"));
203            match outcome {
204                RuleKind::Accept => {
205                    let ports =
206                        PortRange::from_range(ports.clone()).expect("bad range in rangemap");
207                    allowed
208                        .push_ordered(ports)
209                        .expect("disordered output from rangemap");
210                }
211                RuleKind::Reject => {}
212            }
213        }
214        PortPolicy::from_allowed_port_ranges(allowed)
215    }
216}
217
218define_derive_deftly! {
219    /// Define [`PortPolicies::from_summariser`].
220    //
221    // The `v4` and `v6` fields have the same type.
222    // Using a macro makes the otherwise-easy copy-pasta bugs impossible.
223    PortPolicies beta_deftly:
224
225    $impl {
226        /// Actually calculate the port policy summaries for both IP versions
227        fn from_summariser(
228            summariser: Summariser,
229            thresh: &PortSummaryThresholds,
230        ) -> PortPolicies {
231            PortPolicies { $(
232                $fname: summariser.policy_for_one_ip_version(|r| &r.$fname, thresh.$fname),
233            ) }
234        }
235    }
236}
237
238//====================  primary entrypoint, and output type ====================
239
240/// A pair of port policy summaries, one for IPv4 and one for IPv6
241///
242/// Returned by [`AddrPolicy::summarise_precise`].
243#[derive(Debug, Clone, Eq, PartialEq, Hash, Deftly)]
244#[derive_deftly(PortPolicies)]
245#[allow(clippy::exhaustive_structs)] // New IP version would be a breaking change
246pub struct PortPolicies {
247    /// IPv4
248    pub v4: PortPolicy,
249
250    /// IPv6
251    pub v6: PortPolicy,
252}
253
254impl AddrPolicy {
255    /// Calculate port policy summaries using a precise but unhardened algorithm
256    ///
257    /// Returns two Exit Policy Summaries, one for for each of IPv4 and IPv6.
258    /// <https://spec.torproject.org/dir-spec/computing-consensus.html#exit-summary>
259    ///
260    /// **Not generally suitable for use on untrusted input because
261    /// there is no effort to limit the computational complexity.**
262    ///
263    /// Useful for a router, when calculating
264    /// [`ipv6-policy`
265    /// ](https://spec.torproject.org/dir-spec/server-descriptor-format.html#item:ipv6-policy)
266    /// in its router descriptor, from its own (locally configured) accept/reject policy.
267    ///
268    /// The result is calculated according to
269    /// [this rule](https://spec.torproject.org/dir-spec/computing-consensus.html#exit-summary:semantics):
270    ///
271    /// > A port should be summarised as accepted iff the full exit policy
272    /// > permits “most” “public” addresses on that port.
273    ///
274    /// `summarise_precise` implements the rule precisely as specified there;
275    /// not the hardened approximate algorithm used by dirauths for IPv4 summaries.
276    ///
277    /// `private_ranges` is the ranges considered not "public".
278    /// Rejections of addresses in these ranges are disregarded when considering
279    /// whether a port is open.
280    ///
281    /// This algorithm does not handle "IPv4-mapped Addresses"
282    /// (ie, IPv6-mapped IPv4 addresses) specially.
283    /// They should normally be rejected, and be in `private_ranges`.
284    ///
285    /// `thresholds` should normally be `&PortSummaryThresholds::DEFAULT`.
286    //
287    // To generate an `ipv6-policy` line, it would be sufficient to only calculate a v6 summary.
288    // So why provide v4 too?  Because it's useful for testing of the approximate summary
289    // algorithm, and because we might want to move v4 policy summarisation to relays, too.
290    //
291    // Why return both policies, rather than providing separate entrypoints?
292    // Mostly, because it's convenient in the implementation: computing them separately
293    // would mean more of our principal code would be generic over `N`.
294    // This is not supposed to be a hot path anyway.
295    pub fn summarise_precise(
296        &self,
297        thresholds: &PortSummaryThresholds,
298        private_ranges: impl IntoIterator<Item = IpNet>,
299    ) -> PortPolicies {
300        let mut s = Summariser::start();
301        for (rule_kind, pat) in self.rules().rev() {
302            s.apply_rule(rule_kind, &pat);
303        }
304
305        tprintln!("summariser intermediate: {s:#?}");
306
307        // We handle private ranges by deleting them from rejected list, pretending they're open
308
309        let all_ports =
310            PortRange::from_range(ALL_PORTS).expect("all ports is fixedly correct range");
311
312        for private in private_ranges {
313            s.apply_rule(
314                RuleKind::Accept,
315                &AddrPortPattern {
316                    addrs: IpPattern::Net(private),
317                    ports: all_ports,
318                },
319            );
320        }
321
322        tprintln!("summariser final: {s:#?}");
323
324        PortPolicies::from_summariser(s, thresholds)
325    }
326}
327
328//---------- PortSummaryThresholds configuration type ----------
329
330/// Thresholds for deciding whether a port counts as open, for a summary
331///
332/// Each value is the maximum number of individual addresses
333/// that may be blocked before the port is considered closed.
334///
335/// The `Default` implementation, and [`PortSummaryThresholds::DEFAULT`],
336/// provide the thresholds currently specified in torspec.
337///
338/// We provide this as a controllable parameter so that the summariser is
339/// a pure function that doesn't embed these tuneables.
340/// Then if the spec changes,  the directory authority consensus calculator
341/// can provide the appropriate thresholds depending on the consensus method.
342#[derive(Debug, Clone, Eq, PartialEq, Hash, Deftly)]
343#[allow(clippy::exhaustive_structs)] // New IP version would be a breaking change
344#[derive_deftly(PortSummaryThresholds)]
345pub struct PortSummaryThresholds {
346    /// IPv4
347    ///
348    /// Currently, the spec says
349    ///
350    /// > no more than 2^25 IPv4 addresses (two /8's worth, or one /7's worth)
351    #[deftly(default_prefix_len = 7)]
352    pub v4: u128,
353
354    /// IPv6
355    ///
356    /// Currently, the spec says
357    ///
358    /// > no more than 2^112 IPv6 addresses (one /16's worth)
359    #[deftly(default_prefix_len = 16)]
360    pub v6: u128,
361}
362
363define_derive_deftly! {
364    /// Define impls on `PortSummaryThresholds`
365    ///
366    /// This is a macro because there's no type-based safeguard against copy-paste bugs.
367    PortSummaryThresholds beta_deftly, meta_quoted rigorous:
368
369    ${define N $<Ip $fname Net>}
370
371    $impl {
372        /// PortSummaryThresholds from prefix lengths
373        ///
374        /// Returns a `PortSummaryThresholds` whose thresholds are
375        /// "one /`v4`'s worth" for IPv4
376        /// and
377        /// "one /`v6`'s worth" for IPv6.
378        pub const fn from_prefix_lengths( $(
379            $<$fname _prefix_len>: u8,
380        ) ) -> PortSummaryThresholds {
381            PortSummaryThresholds { $(
382                $fname: 1_u128 << $N::ADDR_BITS - $<$fname _prefix_len>,
383            ) }
384        }
385
386        /// Default value, from the Tor Specifications
387        pub const DEFAULT: PortSummaryThresholds = PortSummaryThresholds::from_prefix_lengths( $(
388            ${fmeta(default_prefix_len) as expr},
389        ) );
390    }
391}
392use derive_deftly_template_PortSummaryThresholds;
393
394impl Default for PortSummaryThresholds {
395    fn default() -> Self {
396        PortSummaryThresholds::DEFAULT
397    }
398}
399
400#[cfg(test)]
401mod test {
402    // @@ begin test lint list maintained by maint/add_warning @@
403    #![allow(clippy::bool_assert_comparison)]
404    #![allow(clippy::clone_on_copy)]
405    #![allow(clippy::dbg_macro)]
406    #![allow(clippy::mixed_attributes_style)]
407    #![allow(clippy::print_stderr)]
408    #![allow(clippy::print_stdout)]
409    #![allow(clippy::single_char_pattern)]
410    #![allow(clippy::unwrap_used)]
411    #![allow(clippy::unchecked_time_subtraction)]
412    #![allow(clippy::useless_vec)]
413    #![allow(clippy::needless_pass_by_value)]
414    #![allow(clippy::string_slice)] // See arti#2571
415    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
416    use super::*;
417    use crate::parse_testcase_from_netdoc;
418    use itertools::Itertools;
419
420    #[derive(Deftly)]
421    #[derive_deftly(NetdocParseableFields)]
422    struct TestCase {
423        /// Input policy, `accept` and `reject` lines
424        #[deftly(netdoc(flatten))]
425        full: AddrPolicy,
426
427        /// Expected IPv4 summary
428        p4: PortPolicy,
429
430        /// Expected IPv6 summary
431        p6: PortPolicy,
432    }
433
434    /// Run one test case
435    ///
436    /// This is the implementation of `chk`.
437    ///
438    /// It returns `Result`, just for the benefit of its self-test.
439    fn chk_inner(input_doc: &str) -> anyhow::Result<()> {
440        let case: TestCase = parse_testcase_from_netdoc(input_doc);
441
442        let summary = case.full.summarise_precise(
443            &PortSummaryThresholds::DEFAULT,
444            [
445                // hardly a complete list
446                "0.0.0.0/8",
447                "::/8",
448                "::faff:0:0/96",
449                "10.0.0.0/8",
450                "172.16.0.0/12",
451                "192.168.0.0/16",
452                "fd00::/8",
453            ]
454            .into_iter()
455            .map(|s| s.parse::<IpNet>().expect(s))
456            .collect_vec(),
457        );
458
459        /// Like `assert_eq`  combined with `anyhow::ensure` - throws `Err(anyhow::Error)`
460        macro_rules! ensure_eq { { $a:expr, $b:expr } => {
461            anyhow::ensure!($a == $b, "{:?} != {:?}", $a, $b);
462        } }
463
464        ensure_eq!(summary.v4, case.p4);
465        ensure_eq!(summary.v6, case.p6);
466
467        Ok(())
468    }
469
470    /// Run one test case
471    ///
472    /// Test cases are strings in netdoc format, for `TestCase`,
473    /// but without the intro item.
474    ///
475    /// Whitespace will be normalised and `#`-comments stripped.
476    fn chk(input_doc: &str) {
477        chk_inner(input_doc).expect("test failed");
478    }
479
480    #[test]
481    fn basics() {
482        chk(r"
483                p4 accept 1-65535
484                p6 accept 1-65535
485        ");
486        chk(r"
487                accept *:*
488                p4 accept 1-65535
489                p6 accept 1-65535
490        ");
491        chk(r"
492                reject *:*
493                p4 reject 1-65535
494                p6 reject 1-65535
495        ");
496        chk(r"
497                reject 0.0.0.0/0:*
498                p4 reject 1-65535
499                p6 accept 1-65535
500        ");
501        chk(r"
502                reject [::]/0:*
503                p4 accept 1-65535
504                p6 reject 1-65535
505        ");
506    }
507
508    #[test]
509    fn edge_cases() {
510        // Reject nearly enough addresses to reject
511        let reject_precisely_allowed_amount = r"
512                accept *:100
513                reject 1.0.0.0/8:400-419
514                reject 2.0.0.0/8:410-429
515                reject [2002::]/17:600-619
516                reject [2003::]/17:610-629
517                reject 0.0.0.0/0:1-399
518                reject 0.0.0.0/0:430-65535
519                reject [::]/0:1-599
520                reject [::]/0:630-65535
521        ";
522
523        chk(&format!(
524            r"  {reject_precisely_allowed_amount}
525
526                # reject some private nets, proving they are disregarded
527                reject 10.0.0.0/8:*
528                reject [fd00::]/8:*
529
530                p4 accept 100,400-429
531                p6 accept 100,600-629 "
532        ));
533
534        // Reject one more address
535        chk(&format!(
536            r"  {reject_precisely_allowed_amount}
537
538                reject 4.0.0.0/32:415-425
539                reject [2001:a::1]/128:615-625
540
541                p4 accept 100,400-414,420-429
542                p6 accept 100,600-614,620-629 "
543        ));
544    }
545
546    #[test]
547    fn chk_detects_discrepancies() {
548        // The output is supposed to be `p4 reject 25` ...
549        let input_doc = r"
550                reject *:25
551                p4 reject 26
552                p6 reject 26
553        ";
554        let e = chk_inner(input_doc).expect_err("was supposed to fail");
555        let e = format!("{e:#}"); // # to make anyhow print sources
556
557        assert!(
558            e.contains("!= PortPolicy { allowed: PortRanges([PortRange(1-25)"),
559            "error: {e}"
560        );
561    }
562}