1
//! IPv4 port policy summarisation - approximate algorithm
2
//!
3
//! <https://spec.torproject.org/dir-spec/computing-microdescriptors.html#item:p>
4
//!
5
//! Contrast precise summaries, in `tor-netdoc/src/types/policy/summary.rs`.
6

            
7
use super::*;
8

            
9
// These are very specific to this area; let's not have them in the crate prelude.
10
use tor_netdoc::types::policy::{
11
    AddrPolicy, AddrPortPattern, IpPattern, PortPolicy, PortRange, RuleKind,
12
};
13

            
14
/// How many rejected IPv4 addresses are allowed before we consider the port closed
15
///
16
/// Has the same value as [`tor_netdoc::types::policy::PortSummaryThresholds::v4`].
17
/// But we recapitulate it here, because we mustn't change the output of *this* algorithm
18
/// without a consensus method change.
19
///
20
/// I.e., changing this requires a consensus method change.
21
const MAX_REJECTED: u32 = 1 << 25;
22

            
23
/// Allows us to write the private ranges in CIDR-like format
24
macro_rules! ipnet_consts { { $( $a:literal $( , $bcd:literal )* / $p:literal; )* } => {
25
    &[ $(
26
        Ipv4Net::new_assert(Ipv4Addr::new($a $(, $bcd)*), $p),
27
    )* ]
28
} }
29

            
30
/// Private networks, disregarded for counting rejected number of addresses
31
///
32
/// Cut and paste from the spec, with light formatting editing:
33
/// add semicolon separators; change `.` to `,`; use `//` for comments.
34
// (We can't use `.` because bits of the input IPv4 addresses end up looking like float literals!)
35
///
36
/// <https://spec.torproject.org/dir-spec/computing-microdescriptors.html#item:p:public-ipv4>
37
///
38
/// Changing this requires a consensus method change.
39
/// If we ever do that, we may want to mark these entries with consensus method(s)
40
/// that they're used in, or have multiple lists, or something.
41
const PRIVATE_NETWORKS: &[Ipv4Net] = ipnet_consts![
42
    0,0,0,0/8; // This Network, RFC791 3.2
43
    10,0,0,0/8; 172,16,0,0/12; 192,168,0,0/16; // Private-Use, RFC1918
44
    100,64,0,0/10; // Shared Address Space, RFC6598
45
    127,0,0,0/8; // Loopback, RFC1122 3.2.1.3
46
    169,254,0,0/16; // Link Local, RFC3927
47
    192,0,0,0/24; // IETF Protocol Assignments, RFC6890
48
    192,0,2,0/24; 198,51,100,0/24; 203,0,113,0/24; // Documentation (TEST-NET-[123]), RFC5737
49
    198,18,0,0/15; // Benchmarking, RFC2544
50
    192,31,196,0/24; // AS112-v4 (reverse lookup for private addrs) RFC7535
51
    192,175,48,0/24; // Direct Delegation AS112 RFC5734
52
    255,255,255,255/32; // “Limited Broadcast”, RFC8190, RFC919 s7
53
];
54

            
55
/// Port resolution algorithm, main state
56
///
57
/// We implement the algorithm specified in
58
/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:p>.
59
///
60
/// As noted there, we perform the algorithm in parallel, and independently, for each port.
61
/// But, we use a rangemap so that we can deal with ranges rather than individual ports.
62
///
63
/// We loop over all the rules, once, in `summarise_policy_v4_approximate`.
64
/// This is done in *forward* order; we handle the summarisation algorithm's early exit
65
/// by having an explicit `Stopped` state in the per-port state.
66
///
67
/// We interpret each rule, in `ResolutionState::apply_rule`.
68
/// That decides what effect the rule has on the relevant ports,
69
/// and calls `ResolutionState::update_for_ports` to make the appropriate state change.
70
#[derive(Debug)]
71
struct ResolutionState {
72
    /// State of the algorithm for each port
73
    ///
74
    /// This always contains *some* entry for 1..65535, but nothing for 0.
75
    port_map: RangeInclusiveMap<u16, PortState>,
76

            
77
    /// How many ports we have resolve (ie, are in state `Stopped`)
78
    ///
79
    /// When this reaches 2^16-1, *all* our parallel loops have stopped,
80
    /// and we can skip processing the rest of the rules.
81
    total_resolved: u16,
82
}
83

            
84
/// The state of the algorithm for any one port.
85
///
86
/// (Actually, one of these is stored for a *range* of ports.
87
/// They are split up and joined as necessary by `rangemap_mutate_range`
88
/// and `RangeInclusiveMap`.)
89
#[derive(Debug, Clone, Eq, PartialEq)]
90
enum PortState {
91
    /// The algorithm for this port has stopped, yielding `RuleKind`
92
    Stopped(RuleKind),
93

            
94
    /// The algorithm for this port is continuing
95
    Running(PortStateRunning),
96
}
97
use PortState as PS;
98

            
99
/// State of the still-running algorithm for any one port
100
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
101
struct PortStateRunning {
102
    /// Rejected address count (saturating)
103
    rejected: u32,
104
}
105

            
106
/// "Error" thrown by algorithm computations
107
#[derive(Debug, derive_more::From)]
108
enum EarlyTermination {
109
    /// Pseudo-error, thrown to stop the algorithm early when every port has been decided
110
    EveryPortResolved,
111

            
112
    /// Bug, "crash" - we don't want to panic, ever
113
    Bug(#[from] Bug),
114
}
115

            
116
/// Return the number of hosts in this network, clamped to `u32::MAX`
117
///
118
/// Very like `Net::host_count_saturating` in tor-netdoc `summary.rs`,
119
/// but that returns `u128` and isn't public (and maybe doesn't want to be),
120
/// and takes less care to avoid impossible panics,
121
/// whereas this is simpler because it can be IPv4-specific.
122
74
fn host_count_saturating(net: Ipv4Net) -> u32 {
123
74
    let shift = 32_u8.saturating_sub(net.prefix_len());
124
74
    1_u32.checked_shl(shift.into()).unwrap_or(u32::MAX)
125
74
}
126

            
127
impl ResolutionState {
128
    /// Update the state, for those of the relevant ports which are still running
129
    ///
130
    /// For each port in `ports` that hasn't `Stopped`, calls `update`.
131
    /// `update` should return the new state, which might be `Running` or `Stopped`.
132
    ///
133
    /// Updates `total_resolved`, and throws `EveryPortResolved` if appropriate.
134
1290
    fn update_for_ports(
135
1290
        &mut self,
136
1290
        ports: RangeInclusive<u16>,
137
1290
        mut update: impl FnMut(PortStateRunning) -> PortState,
138
1290
    ) -> Result<(), EarlyTermination> {
139
        // Silently disregard port 0 as per the spec
140
        //  https://spec.torproject.org/dir-spec/computing-consensus.html#exit-summary:semantics
141
1290
        let ports = if *ports.start() == 0 {
142
            1..=*ports.end()
143
        } else {
144
1290
            ports
145
        };
146

            
147
1290
        rangemap_mutate_range(
148
1290
            &mut self.port_map,
149
1290
            &ports,
150
            // important that we shadow `ports` here
151
2910
            |state, ports| {
152
2910
                let state = state
153
2910
                    .as_mut()
154
2910
                    .ok_or_else(|| internal!("state entry missing for {ports:?}"))?;
155
2910
                let running = match state {
156
2104
                    PS::Running(y) => y,
157
806
                    PS::Stopped(_) => return Ok(()),
158
                };
159
2104
                *state = update(*running);
160
2104
                if let PS::Stopped(_) = state {
161
                    // Fine points, that make this correct:
162
                    //
163
                    // We were Running before, and now we're Stopped, so all these ports
164
                    // are indeed *newly* Stopped.
165
                    //
166
                    // If we return `Err`, the update we make to *this* port range
167
                    // will indeed be applied (see docs for rangemap_mutate_range),
168
                    // so we don't lose *this* port range's store.
169
                    //
170
                    // And, in that case ranges overlapping with the outer ports
171
                    // will not be processed, but that's OK because we only throw Err
172
                    // if we know they've already been made Stopped.
173
                    //
174
                    // Use checked arithmetic to avoid panics, and an IEFE to catch the Nones.
175
2060
                    self.total_resolved = (|| {
176
2060
                        self.total_resolved.checked_add(
177
2060
                            ports
178
2060
                                .end()
179
2060
                                .checked_sub(*ports.start())?
180
                                // start is always >0, so this is at most u16::MAX, OK
181
2060
                                .checked_add(1)?,
182
                        )
183
                    })()
184
2060
                    .ok_or_else(|| internal!("overflow in resolved port counts"))?;
185

            
186
                    // Compare with 65535, not 65536, since we never add anything for port 0
187
                    #[allow(clippy::absurd_extreme_comparisons)] // clippy wants ==, urgh
188
2060
                    if self.total_resolved >= u16::MAX {
189
78
                        return Err(EarlyTermination::EveryPortResolved);
190
1982
                    }
191
44
                }
192
2026
                Ok(())
193
2910
            },
194
        )
195
1290
    }
196

            
197
    /// Perform one step of the resolution/summarisation algorithm: apply one rule
198
    ///
199
    /// This applies the rule, in parallel, to all the ports it applies to.
200
    /// Ports where the algorithm has already stopped are skipped
201
    /// (this is done by code in `update_for_ports`).
202
1348
    fn apply_rule(
203
1348
        &mut self,
204
1348
        rule_kind: RuleKind,
205
1348
        pat: &AddrPortPattern,
206
1348
    ) -> Result<(), EarlyTermination> {
207
        use IpPattern as IPP;
208

            
209
1348
        let ports = pat.ports.to_range();
210

            
211
        // This code is fairly specific to the fact that we're doing this only for IPv4.
212
        // To support IPv6 generically, we'd need a whole panoply of v4/v6 generics
213
        // for IpNet, the private address nets, the max rejected, and so on.
214
        //
215
        // I think we probably won't ever want to do exit summarisation in dirauths for v6.
216
        // Having the relay do its own summary is fine.
217
        //
218
        // But, anyway here are some notes for how to support V6:
219
        //  - Expose the Net trait from tor-netdoc's summariser.
220
        //    Under some other name, presumably, and maybe it should be implemented
221
        //    for IpvXAddr rather IpvXNet.
222
        //  - In each match below, use those trait methods.
223
        //    To avoid missing one, maybe call a trait function on pat.addrs
224
        //    and match on the return value instead.
225
        //    Or maybe reuse the code in tor-netdoc's `Summariser::apply_rule`.
226
        //  - Make our own subtrait of Net, and
227
        //    make MAX_REJECTED and PRIVATE_NETWORKS trait constants in it
228
        //  - Make PortStateRunning.rejected big enough for v6
229
        //    (this might mean adding generics to PortState)
230
        //  - `grep -i ipv` this file to see what you missed
231
        //  - consider calculating v4 and v6 in parallel
232

            
233
1348
        match (rule_kind, pat.addrs) {
234
            // From the spec:
235
            //
236
            // "* Disregard items whose addrspec matches no IPv4 addresses"
237
            (_, IPP::Net(IpNet::V6(_))) => Ok(()),
238

            
239
126
            (RuleKind::Reject, IPP::Net(IpNet::V4(net))) => {
240
                // "* Disregard `reject`s whose addrspec is an IPv4 subnet
241
                //    completely contained within a private network (see below)."
242
                //
243
                // (As per the spec's suggested approximation, we treat partially-private
244
                // rejects as public, rather than counting the addresses precisely.)
245
126
                if PRIVATE_NETWORKS
246
126
                    .iter()
247
1129
                    .any(|private| private.contains(&net))
248
                {
249
74
                    return Ok(());
250
52
                }
251
                // "* For other `reject` lines, add the size of the subnet
252
                //    to the "rejected address count"."
253
100
                self.update_for_ports(ports, |mut state| {
254
                    // Avoid overflow; if we saturate, we'll treat it as rejected - fine.
255
74
                    state.rejected = state.rejected.saturating_add(host_count_saturating(net));
256
74
                    if state.rejected > MAX_REJECTED {
257
                        // * If the "rejected address count" exceeds the 2^25 limit,
258
                        //   stop and list the port as closed.
259
30
                        PS::Stopped(RuleKind::Reject)
260
                    } else {
261
44
                        PS::Running(state)
262
                    }
263
74
                })
264
            }
265

            
266
            (RuleKind::Reject, IPP::All) => {
267
                // This is an "other `reject` line" - but we didn't handle it above.
268
                // It necessarily blows the limit.
269
858
                self.update_for_ports(ports, |_: PortStateRunning| {
270
                    //
271
828
                    PS::Stopped(RuleKind::Reject)
272
828
                })
273
            }
274

            
275
            // "* For an `accept` item which matches all IPv4 addresses,
276
            //    stop and list the port as open."
277
            (RuleKind::Accept, IPP::All) => {
278
                // All-addresses patterns
279
1740
                self.update_for_ports(ports, |_: PortStateRunning| {
280
                    //
281
1160
                    PS::Stopped(RuleKind::Accept)
282
1160
                })
283
            }
284
2
            (RuleKind::Accept, IPP::Net(IpNet::V4(net))) => {
285
2
                if net.prefix_len() == 0 {
286
                    // IPv4-only patterns matching all IPv4 addresses
287
                    self.update_for_ports(ports, |_: PortStateRunning| {
288
                        PS::Stopped(RuleKind::Accept)
289
                    })
290
                } else {
291
                    // Ignore IPv4 accepts which don't accept every address.
292
2
                    Ok(())
293
                }
294
            }
295
        }
296
1348
    }
297
}
298

            
299
/// IPv4 port policy summarisation, for use by dirauths
300
///
301
/// <https://spec.torproject.org/dir-spec/computing-microdescriptors.html#item:p>
302
///
303
/// This is an approximate algorithm.
304
///
305
/// It is for use by directory authorities when processing routerdescs into
306
/// microdescs.
307
///
308
/// Should *not* be used by relays, or other entities processing reasonably-trusted
309
/// policy data.  Those should use [`AddrPolicy::summarise_precise`].
310
///
311
/// Only implemented for IPv4.
312
78
pub(crate) fn summarise_policy_v4_approximate(
313
78
    policy: &AddrPolicy,
314
78
    _method: &TrackedConsensusMethod,
315
78
) -> Result<PortPolicy, Bug> {
316
78
    let mut state = ResolutionState {
317
78
        port_map: RangeInclusiveMap::new(),
318
78
        total_resolved: 0,
319
78
    };
320

            
321
78
    state.port_map.insert(
322
        //
323
78
        1..=u16::MAX,
324
78
        PS::Running(PortStateRunning { rejected: 0 }),
325
    );
326

            
327
117
    let r = (|| {
328
1348
        for (rule_kind, pat) in policy.rules() {
329
1348
            state.apply_rule(rule_kind, &pat)?;
330
        }
331
        // Otherwise, on reaching the end of the exit policy items, list the port as open.
332
42
        state.update_for_ports(1..=u16::MAX, |_: PortStateRunning| {
333
42
            PS::Stopped(RuleKind::Accept)
334
42
        })
335
    })();
336

            
337
78
    match r {
338
78
        Err(EarlyTermination::EveryPortResolved) => {}
339
        Err(EarlyTermination::Bug(bug)) => return Err(bug),
340
        Ok(()) => return Err(internal!("not every port resolved ({state:?})")),
341
    }
342

            
343
78
    let allowed = state
344
78
        .port_map
345
78
        .into_iter()
346
1697
        .map(|(range, state)| {
347
1658
            Ok::<_, Bug>(match state {
348
858
                PS::Stopped(RuleKind::Reject) => None,
349
                PS::Stopped(RuleKind::Accept) => Some(
350
800
                    PortRange::from_range(range.clone())
351
800
                        .ok_or_else(|| internal!("malformed port range {range:?}"))?,
352
                ),
353
                PS::Running(wat) => {
354
                    Err(internal!("some port loop still running {range:?} {wat:?}"))?
355
                }
356
            })
357
1658
        })
358
78
        .flatten_ok()
359
117
        .process_results(|ranges| PortPolicy::from_ordered_allowed_ranges(ranges))?
360
78
        .map_err(into_internal!("ranges from rangemap out of order"))?;
361

            
362
78
    Ok(allowed)
363
78
}
364

            
365
#[cfg(test)]
366
mod test {
367
    // @@ begin test lint list maintained by maint/add_warning @@
368
    #![allow(clippy::bool_assert_comparison)]
369
    #![allow(clippy::clone_on_copy)]
370
    #![allow(clippy::dbg_macro)]
371
    #![allow(clippy::mixed_attributes_style)]
372
    #![allow(clippy::print_stderr)]
373
    #![allow(clippy::print_stdout)]
374
    #![allow(clippy::single_char_pattern)]
375
    #![allow(clippy::unwrap_used)]
376
    #![allow(clippy::unchecked_time_subtraction)]
377
    #![allow(clippy::useless_vec)]
378
    #![allow(clippy::needless_pass_by_value)]
379
    #![allow(clippy::string_slice)] // See arti#2571
380
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
381
    use super::*;
382
    use derive_deftly::Deftly;
383
    use tor_netdoc::parse_testcase_from_netdoc;
384
    use tor_netdoc::types::policy::PortSummaryThresholds;
385

            
386
    /// cut-down (IPv4 only) version of tor-netdoc's summary.rs version
387
    const REJECT_PRECISELY_ALLOWED_AMOUNT: &str = r"
388
                accept *:100
389
                reject 1.0.0.0/8:400-419
390
                reject 2.0.0.0/8:410-429
391
                reject 0.0.0.0/0:1-399
392
                reject 0.0.0.0/0:430-65535
393
    ";
394

            
395
    #[derive(Deftly)]
396
    #[derive_deftly(tor_netdoc::NetdocParseableFields)]
397
    struct IsPreciseTestCase {
398
        /// Input policy, `accept` and `reject` lines
399
        #[deftly(netdoc(flatten))]
400
        full: AddrPolicy,
401
    }
402

            
403
    fn method_for_testing() -> TrackedConsensusMethod {
404
        TrackedConsensusMethod::new(SupportedConsensusMethod::MAX)
405
    }
406

            
407
    /// Run one test case where the approx summary is, in fact, precise
408
    ///
409
    /// The expected output is calculated with [`AddrPolicy::summarise_precise`]
410
    /// (which uses a quite different implementation strategy to the code here.)
411
    fn chk_is_precise(input_doc: &str) {
412
        let case: IsPreciseTestCase = parse_testcase_from_netdoc(input_doc);
413

            
414
        let approx = summarise_policy_v4_approximate(
415
            //
416
            &case.full,
417
            &method_for_testing(),
418
        )
419
        .unwrap();
420

            
421
        let precise = case.full.summarise_precise(
422
            // if the thresholds change in a new consensus method, we'll probably need
423
            // to explicitly change the value here and maybe test both thresholds
424
            &PortSummaryThresholds::default(),
425
            PRIVATE_NETWORKS.iter().copied().map(IpNet::V4),
426
        );
427

            
428
        assert_eq!(approx, precise.v4);
429
    }
430

            
431
    #[test]
432
    fn basics() {
433
        chk_is_precise(r"p4 accept 1-65535");
434
        chk_is_precise(r"p4 reject 1-65535");
435
        chk_is_precise(r"p4 accept *:*");
436
        chk_is_precise(r"p4 reject *:25");
437
    }
438

            
439
    #[test]
440
    fn edge_cases() {
441
        chk_is_precise(REJECT_PRECISELY_ALLOWED_AMOUNT);
442

            
443
        // reject a private net, proving it is disregarded
444
        chk_is_precise(&format!(
445
            r"  {REJECT_PRECISELY_ALLOWED_AMOUNT}
446
                reject 100.64.1.0/24:* "
447
        ));
448

            
449
        // Reject one more address
450
        chk_is_precise(&format!(
451
            r"  {REJECT_PRECISELY_ALLOWED_AMOUNT}
452
                reject 4.0.0.0/32:415-425 "
453
        ));
454
    }
455

            
456
    #[derive(Deftly)]
457
    #[derive_deftly(tor_netdoc::NetdocParseableFields)]
458
    struct ImpreciseTestCase {
459
        /// Input policy, `accept` and `reject` lines
460
        #[deftly(netdoc(flatten))]
461
        full: AddrPolicy,
462

            
463
        /// Expected IPv4 summary
464
        p4: PortPolicy,
465
    }
466

            
467
    /// Run one test case where the approx summary is imprecise
468
    ///
469
    /// The expected output is `p4` in the test case.
470
    fn chk_imprecise(input_doc: &str) {
471
        let case: ImpreciseTestCase = parse_testcase_from_netdoc(input_doc);
472

            
473
        let approx = summarise_policy_v4_approximate(
474
            //
475
            &case.full,
476
            &method_for_testing(),
477
        )
478
        .unwrap();
479

            
480
        assert_eq!(approx, case.p4);
481

            
482
        let precise = case.full.summarise_precise(
483
            // if the thresholds change in a new consensus method, we'll probably need
484
            // to explicitly change the value here and maybe test both thresholds
485
            &PortSummaryThresholds::default(),
486
            PRIVATE_NETWORKS.iter().copied().map(IpNet::V4),
487
        );
488

            
489
        assert_ne!(approx, precise.v4);
490
    }
491

            
492
    #[test]
493
    fn approximations() {
494
        chk_imprecise(&format!(
495
            r"  accept 1.0.0.0/32:417 # ignored non-wildcard accept
496
                {REJECT_PRECISELY_ALLOWED_AMOUNT}
497
                reject 4.0.0.0/32:415-425
498

            
499
                p4 accept 100,400-414,420-429 " // 417 missing
500
        ));
501

            
502
        chk_imprecise(
503
            r"  reject 10.0.0.0/7:10 # reject 2^25 addresses, but they're half-private
504
                reject 4.0.0.0/32:10-20 # tip over the edge
505

            
506
                p4 reject 10 ",
507
        );
508
    }
509
}