tor_protover/
lib.rs

1#![cfg_attr(docsrs, feature(doc_auto_cfg, doc_cfg))]
2#![doc = include_str!("../README.md")]
3// @@ begin lint list maintained by maint/add_warning @@
4#![allow(renamed_and_removed_lints)] // @@REMOVE_WHEN(ci_arti_stable)
5#![allow(unknown_lints)] // @@REMOVE_WHEN(ci_arti_nightly)
6#![warn(missing_docs)]
7#![warn(noop_method_call)]
8#![warn(unreachable_pub)]
9#![warn(clippy::all)]
10#![deny(clippy::await_holding_lock)]
11#![deny(clippy::cargo_common_metadata)]
12#![deny(clippy::cast_lossless)]
13#![deny(clippy::checked_conversions)]
14#![warn(clippy::cognitive_complexity)]
15#![deny(clippy::debug_assert_with_mut_call)]
16#![deny(clippy::exhaustive_enums)]
17#![deny(clippy::exhaustive_structs)]
18#![deny(clippy::expl_impl_clone_on_copy)]
19#![deny(clippy::fallible_impl_from)]
20#![deny(clippy::implicit_clone)]
21#![deny(clippy::large_stack_arrays)]
22#![warn(clippy::manual_ok_or)]
23#![deny(clippy::missing_docs_in_private_items)]
24#![warn(clippy::needless_borrow)]
25#![warn(clippy::needless_pass_by_value)]
26#![warn(clippy::option_option)]
27#![deny(clippy::print_stderr)]
28#![deny(clippy::print_stdout)]
29#![warn(clippy::rc_buffer)]
30#![deny(clippy::ref_option_ref)]
31#![warn(clippy::semicolon_if_nothing_returned)]
32#![warn(clippy::trait_duplication_in_bounds)]
33#![deny(clippy::unchecked_duration_subtraction)]
34#![deny(clippy::unnecessary_wraps)]
35#![warn(clippy::unseparated_literal_suffix)]
36#![deny(clippy::unwrap_used)]
37#![deny(clippy::mod_module_files)]
38#![allow(clippy::let_unit_value)] // This can reasonably be done for explicitness
39#![allow(clippy::uninlined_format_args)]
40#![allow(clippy::significant_drop_in_scrutinee)] // arti/-/merge_requests/588/#note_2812945
41#![allow(clippy::result_large_err)] // temporary workaround for arti#587
42#![allow(clippy::needless_raw_string_hashes)] // complained-about code is fine, often best
43#![allow(clippy::needless_lifetimes)] // See arti#1765
44//! <!-- @@ end lint list maintained by maint/add_warning @@ -->
45
46#![allow(non_upper_case_globals)]
47#![allow(clippy::upper_case_acronyms)]
48
49use caret::caret_int;
50
51use thiserror::Error;
52
53pub mod named;
54
55caret_int! {
56    /// A recognized subprotocol.
57    ///
58    /// These names are kept in sync with the names used in consensus
59    /// documents; the values are kept in sync with the values in the
60    /// cbor document format in the walking onions proposal.
61    ///
62    /// For the full semantics of each subprotocol, see tor-spec.txt.
63    #[derive(Hash,Ord,PartialOrd)]
64    pub struct ProtoKind(u16) {
65        /// Initiating and receiving channels, and getting cells on them.
66        Link = 0,
67        /// Different kinds of authenticate cells
68        LinkAuth = 1,
69        /// CREATE cells, CREATED cells, and the encryption that they
70        /// create.
71        Relay = 2,
72        /// Serving and fetching network directory documents.
73        DirCache = 3,
74        /// Serving onion service descriptors
75        HSDir = 4,
76        /// Providing an onion service introduction point
77        HSIntro = 5,
78        /// Providing an onion service rendezvous point
79        HSRend = 6,
80        /// Describing a relay's functionality using router descriptors.
81        Desc = 7,
82        /// Describing a relay's functionality using microdescriptors.
83        MicroDesc = 8,
84        /// Describing the network as a consensus directory document.
85        Cons = 9,
86        /// Sending and accepting circuit-level padding
87        Padding = 10,
88        /// Improved means of flow control on circuits.
89        FlowCtrl = 11,
90        /// Multi-path circuit support.
91        Conflux = 12,
92    }
93}
94
95/// How many recognized protocols are there?
96const N_RECOGNIZED: usize = 13;
97
98/// A specific, named subversion of a protocol.
99#[derive(Eq, PartialEq, Copy, Clone, Debug)]
100pub struct NamedSubver {
101    /// The protocol in question
102    kind: ProtoKind,
103    /// The version of the protocol
104    version: u8,
105}
106
107impl NamedSubver {
108    /// Create a new NamedSubver.
109    const fn new(kind: ProtoKind, version: u8) -> Self {
110        Self { kind, version }
111    }
112}
113
114/// Representation for a known or unknown protocol.
115#[derive(Eq, PartialEq, Clone, Debug, Hash, Ord, PartialOrd)]
116enum Protocol {
117    /// A known protocol; represented by one of ProtoKind.
118    Proto(ProtoKind),
119    /// An unknown protocol; represented by its name.
120    Unrecognized(String),
121}
122
123impl Protocol {
124    /// Return true iff `s` is the name of a protocol we do not recognize.
125    fn is_unrecognized(&self, s: &str) -> bool {
126        match self {
127            Protocol::Unrecognized(s2) => s2 == s,
128            _ => false,
129        }
130    }
131    /// Return a string representation of this protocol.
132    fn to_str(&self) -> &str {
133        match self {
134            Protocol::Proto(k) => k.to_str().unwrap_or("<bug>"),
135            Protocol::Unrecognized(s) => s,
136        }
137    }
138}
139
140/// Representation of a set of versions supported by a protocol.
141///
142/// For now, we only use this type for unrecognized protocols.
143#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
144struct SubprotocolEntry {
145    /// Which protocol's versions does this describe?
146    proto: Protocol,
147    /// A bit-vector defining which versions are supported.  If bit
148    /// `(1<<i)` is set, then protocol version `i` is supported.
149    supported: u64,
150}
151
152/// A set of supported or required subprotocol versions.
153///
154/// This type supports both recognized subprotocols (listed in ProtoKind),
155/// and unrecognized subprotocols (stored by name).
156///
157/// To construct an instance, use the FromStr trait:
158/// ```
159/// use tor_protover::Protocols;
160/// let p: Result<Protocols,_> = "Link=1-3 LinkAuth=2-3 Relay=1-2".parse();
161/// ```
162#[derive(Debug, Clone, Default, Eq, PartialEq, Hash)]
163pub struct Protocols {
164    /// A mapping from protocols' integer encodings to bit-vectors.
165    recognized: [u64; N_RECOGNIZED],
166    /// A vector of unrecognized protocol versions.
167    unrecognized: Vec<SubprotocolEntry>,
168}
169
170impl Protocols {
171    /// Return a new empty set of protocol versions.
172    pub fn new() -> Self {
173        Protocols::default()
174    }
175    /// Helper: return true iff this protocol set contains the
176    /// version `ver` of the protocol represented by the integer `proto`.
177    fn supports_recognized_ver(&self, proto: usize, ver: u8) -> bool {
178        if ver > 63 {
179            return false;
180        }
181        if proto >= self.recognized.len() {
182            return false;
183        }
184        (self.recognized[proto] & (1 << ver)) != 0
185    }
186    /// Helper: return true iff this protocol set contains version
187    /// `ver` of the unrecognized protocol represented by the string
188    /// `proto`.
189    ///
190    /// Requires that `proto` is not the name of a recognized protocol.
191    fn supports_unrecognized_ver(&self, proto: &str, ver: u8) -> bool {
192        if ver > 63 {
193            return false;
194        }
195        let ent = self
196            .unrecognized
197            .iter()
198            .find(|ent| ent.proto.is_unrecognized(proto));
199        match ent {
200            Some(e) => (e.supported & (1 << ver)) != 0,
201            None => false,
202        }
203    }
204    // TODO: Combine these next two functions into one by using a trait.
205    /// Check whether a known protocol version is supported.
206    ///
207    /// ```
208    /// use tor_protover::*;
209    /// let protos: Protocols = "Link=1-3 HSDir=2,4-5".parse().unwrap();
210    ///
211    /// assert!(protos.supports_known_subver(ProtoKind::Link, 2));
212    /// assert!(protos.supports_known_subver(ProtoKind::HSDir, 4));
213    /// assert!(! protos.supports_known_subver(ProtoKind::HSDir, 3));
214    /// assert!(! protos.supports_known_subver(ProtoKind::LinkAuth, 3));
215    /// ```
216    pub fn supports_known_subver(&self, proto: ProtoKind, ver: u8) -> bool {
217        self.supports_recognized_ver(proto.get() as usize, ver)
218    }
219    /// Check whether a protocol version identified by a string is supported.
220    ///
221    /// ```
222    /// use tor_protover::*;
223    /// let protos: Protocols = "Link=1-3 Foobar=7".parse().unwrap();
224    ///
225    /// assert!(protos.supports_subver("Link", 2));
226    /// assert!(protos.supports_subver("Foobar", 7));
227    /// assert!(! protos.supports_subver("Link", 5));
228    /// assert!(! protos.supports_subver("Foobar", 6));
229    /// assert!(! protos.supports_subver("Wombat", 3));
230    /// ```
231    pub fn supports_subver(&self, proto: &str, ver: u8) -> bool {
232        match ProtoKind::from_name(proto) {
233            Some(p) => self.supports_recognized_ver(p.get() as usize, ver),
234            None => self.supports_unrecognized_ver(proto, ver),
235        }
236    }
237
238    /// Check whether a protocol version is supported.
239    ///
240    /// ```
241    /// use tor_protover::*;
242    /// let protos: Protocols = "Link=1-5 Desc=2-4".parse().unwrap();
243    /// assert!(protos.supports_named_subver(named::DESC_FAMILY_IDS)); // Desc=5
244    /// assert!(! protos.supports_named_subver(named::CONFLUX_BASE)); // Conflux=1
245    /// ```
246    pub fn supports_named_subver(&self, protover: NamedSubver) -> bool {
247        self.supports_known_subver(protover.kind, protover.version)
248    }
249
250    /// Parsing helper: Try to add a new entry `ent` to this set of protocols.
251    ///
252    /// Uses `foundmask`, a bit mask saying which recognized protocols
253    /// we've already found entries for.  Returns an error if `ent` is
254    /// for a protocol we've already added.
255    fn add(&mut self, foundmask: &mut u64, ent: SubprotocolEntry) -> Result<(), ParseError> {
256        match ent.proto {
257            Protocol::Proto(k) => {
258                let idx = k.get() as usize;
259                let bit = 1 << u64::from(k.get());
260                if (*foundmask & bit) != 0 {
261                    return Err(ParseError::Duplicate);
262                }
263                *foundmask |= bit;
264                self.recognized[idx] = ent.supported;
265            }
266            Protocol::Unrecognized(ref s) => {
267                if self
268                    .unrecognized
269                    .iter()
270                    .any(|ent| ent.proto.is_unrecognized(s))
271                {
272                    return Err(ParseError::Duplicate);
273                }
274                self.unrecognized.push(ent);
275            }
276        }
277        Ok(())
278    }
279}
280
281/// An error representing a failure to parse a set of protocol versions.
282#[derive(Error, Debug, PartialEq, Eq, Clone)]
283#[non_exhaustive]
284pub enum ParseError {
285    /// A protocol version was not in the range 0..=63.
286    #[error("Protocol version out of range")]
287    OutOfRange,
288    /// Some subprotocol or protocol version appeared more than once.
289    #[error("Duplicate protocol entry")]
290    Duplicate,
291    /// The list of protocol versions was malformed in some other way.
292    #[error("Malformed protocol entry")]
293    Malformed,
294}
295
296/// Helper: return a new u64 in which bits `lo` through `hi` inclusive
297/// are set to 1, and all the other bits are set to 0.
298///
299/// In other words, `bitrange(a,b)` is how we represent the range of
300/// versions `a-b` in a protocol version bitmask.
301///
302/// ```ignore
303/// # use tor_protover::bitrange;
304/// assert_eq!(bitrange(0, 5), 0b111111);
305/// assert_eq!(bitrange(2, 5), 0b111100);
306/// assert_eq!(bitrange(2, 7), 0b11111100);
307/// ```
308fn bitrange(lo: u64, hi: u64) -> u64 {
309    assert!(lo <= hi && lo <= 63 && hi <= 63);
310    let mut mask = !0;
311    mask <<= 63 - hi;
312    mask >>= 63 - hi + lo;
313    mask <<= lo;
314    mask
315}
316
317/// Helper: return true if the provided string is a valid "integer"
318/// in the form accepted by the protover spec.  This is stricter than
319/// rust's integer parsing format.
320fn is_good_number(n: &str) -> bool {
321    n.chars().all(|ch| ch.is_ascii_digit()) && !n.starts_with('0')
322}
323
324/// A single SubprotocolEntry is parsed from a string of the format
325/// Name=Versions, where Versions is a comma-separated list of
326/// integers or ranges of integers.
327impl std::str::FromStr for SubprotocolEntry {
328    type Err = ParseError;
329
330    fn from_str(s: &str) -> Result<Self, ParseError> {
331        // split the string on the =.
332        let (name, versions) = {
333            let eq_idx = s.find('=').ok_or(ParseError::Malformed)?;
334            (&s[..eq_idx], &s[eq_idx + 1..])
335        };
336        // Look up the protocol by name.
337        let proto = match ProtoKind::from_name(name) {
338            Some(p) => Protocol::Proto(p),
339            None => Protocol::Unrecognized(name.to_string()),
340        };
341        if versions.is_empty() {
342            // We need to handle this case specially, since otherwise
343            // it would be treated below as a single empty value, which
344            // would be rejected.
345            return Ok(SubprotocolEntry {
346                proto,
347                supported: 0,
348            });
349        }
350        // Construct a bitmask based on the comma-separated versions.
351        let mut supported = 0_u64;
352        for ent in versions.split(',') {
353            // Find and parse lo and hi for a single range of versions.
354            // (If this is not a range, but rather a single version v,
355            // treat it as if it were a range v-v.)
356            let (lo_s, hi_s) = {
357                match ent.find('-') {
358                    Some(pos) => (&ent[..pos], &ent[pos + 1..]),
359                    None => (ent, ent),
360                }
361            };
362            if !is_good_number(lo_s) {
363                return Err(ParseError::Malformed);
364            }
365            if !is_good_number(hi_s) {
366                return Err(ParseError::Malformed);
367            }
368            let lo: u64 = lo_s.parse().map_err(|_| ParseError::Malformed)?;
369            let hi: u64 = hi_s.parse().map_err(|_| ParseError::Malformed)?;
370            // Make sure that lo and hi are in-bounds and consistent.
371            if lo > 63 || hi > 63 {
372                return Err(ParseError::OutOfRange);
373            }
374            if lo > hi {
375                return Err(ParseError::Malformed);
376            }
377            let mask = bitrange(lo, hi);
378            // Make sure that no version is included twice.
379            if (supported & mask) != 0 {
380                return Err(ParseError::Duplicate);
381            }
382            // Add the appropriate bits to the mask.
383            supported |= mask;
384        }
385        Ok(SubprotocolEntry { proto, supported })
386    }
387}
388
389/// A Protocols set can be parsed from a string according to the
390/// format used in Tor consensus documents.
391///
392/// A protocols set is represented by a space-separated list of
393/// entries.  Each entry is of the form `Name=Versions`, where `Name`
394/// is the name of a protocol, and `Versions` is a comma-separated
395/// list of version numbers and version ranges.  Each version range is
396/// a pair of integers separated by `-`.
397///
398/// No protocol name may be listed twice.  No version may be listed
399/// twice for a single protocol.  All versions must be in range 0
400/// through 63 inclusive.
401impl std::str::FromStr for Protocols {
402    type Err = ParseError;
403
404    fn from_str(s: &str) -> Result<Self, ParseError> {
405        let mut result = Protocols::new();
406        let mut foundmask = 0_u64;
407        for ent in s.split(' ') {
408            if ent.is_empty() {
409                continue;
410            }
411
412            let s: SubprotocolEntry = ent.parse()?;
413            result.add(&mut foundmask, s)?;
414        }
415        result.unrecognized.sort();
416        Ok(result)
417    }
418}
419
420/// Given a bitmask, return a list of the bits set in the mask, as a
421/// String in the format expected by Tor consensus documents.
422///
423/// This implementation constructs ranges greedily.  For example, the
424/// bitmask `0b0111011` will be represented as `0-1,3-5`, and not
425/// `0,1,3,4,5` or `0,1,3-5`.
426///
427/// ```ignore
428/// # use tor_protover::dumpmask;
429/// assert_eq!(dumpmask(0b111111), "0-5");
430/// assert_eq!(dumpmask(0b111100), "2-5");
431/// assert_eq!(dumpmask(0b11111100), "2-7");
432/// ```
433fn dumpmask(mut mask: u64) -> String {
434    /// Helper: push a range (which may be a singleton) onto `v`.
435    fn append(v: &mut Vec<String>, lo: u32, hi: u32) {
436        if lo == hi {
437            v.push(lo.to_string());
438        } else {
439            v.push(format!("{}-{}", lo, hi));
440        }
441    }
442    // We'll be building up our result here, then joining it with
443    // commas.
444    let mut result = Vec::new();
445    // This implementation is a little tricky, but it should be more
446    // efficient than a raw search.  Basically, we're using the
447    // function u64::trailing_zeros to count how large each range of
448    // 1s or 0s is, and then shifting by that amount.
449
450    // How many bits have we already shifted `mask`?
451    let mut shift = 0;
452    while mask != 0 {
453        let zeros = mask.trailing_zeros();
454        mask >>= zeros;
455        shift += zeros;
456        let ones = mask.trailing_ones();
457        append(&mut result, shift, shift + ones - 1);
458        shift += ones;
459        if ones == 64 {
460            // We have to do this check to avoid overflow when formatting
461            // the range `0-63`.
462            break;
463        }
464        mask >>= ones;
465    }
466    result.join(",")
467}
468
469/// The Display trait formats a protocol set in the format expected by Tor
470/// consensus documents.
471///
472/// ```
473/// use tor_protover::*;
474/// let protos: Protocols = "Link=1,2,3 Foobar=7 Relay=2".parse().unwrap();
475/// assert_eq!(format!("{}", protos),
476///            "Foobar=7 Link=1-3 Relay=2");
477/// ```
478impl std::fmt::Display for Protocols {
479    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
480        let mut entries = Vec::new();
481        for (idx, mask) in self.recognized.iter().enumerate() {
482            if *mask != 0 {
483                let pk: ProtoKind = (idx as u16).into();
484                entries.push(format!("{}={}", pk, dumpmask(*mask)));
485            }
486        }
487        for ent in &self.unrecognized {
488            if ent.supported != 0 {
489                entries.push(format!(
490                    "{}={}",
491                    ent.proto.to_str(),
492                    dumpmask(ent.supported)
493                ));
494            }
495        }
496        // This sort is required.
497        entries.sort();
498        write!(f, "{}", entries.join(" "))
499    }
500}
501
502#[cfg(test)]
503mod test {
504    // @@ begin test lint list maintained by maint/add_warning @@
505    #![allow(clippy::bool_assert_comparison)]
506    #![allow(clippy::clone_on_copy)]
507    #![allow(clippy::dbg_macro)]
508    #![allow(clippy::mixed_attributes_style)]
509    #![allow(clippy::print_stderr)]
510    #![allow(clippy::print_stdout)]
511    #![allow(clippy::single_char_pattern)]
512    #![allow(clippy::unwrap_used)]
513    #![allow(clippy::unchecked_duration_subtraction)]
514    #![allow(clippy::useless_vec)]
515    #![allow(clippy::needless_pass_by_value)]
516    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
517    use super::*;
518
519    #[test]
520    fn test_bitrange() {
521        assert_eq!(0b1, bitrange(0, 0));
522        assert_eq!(0b10, bitrange(1, 1));
523        assert_eq!(0b11, bitrange(0, 1));
524        assert_eq!(0b1111110000000, bitrange(7, 12));
525        assert_eq!(!0, bitrange(0, 63));
526    }
527
528    #[test]
529    fn test_dumpmask() {
530        assert_eq!("", dumpmask(0));
531        assert_eq!("0-5", dumpmask(0b111111));
532        assert_eq!("4-5", dumpmask(0b110000));
533        assert_eq!("1,4-5", dumpmask(0b110010));
534        assert_eq!("0-63", dumpmask(!0));
535    }
536
537    #[test]
538    fn test_canonical() -> Result<(), ParseError> {
539        fn t(orig: &str, canonical: &str) -> Result<(), ParseError> {
540            let protos: Protocols = orig.parse()?;
541            let enc = format!("{}", protos);
542            assert_eq!(enc, canonical);
543            Ok(())
544        }
545
546        t("", "")?;
547        t(" ", "")?;
548        t("Link=5,6,7,9 Relay=4-7,2", "Link=5-7,9 Relay=2,4-7")?;
549        t("FlowCtrl= Padding=8,7 Desc=1-5,6-8", "Desc=1-8 Padding=7-8")?;
550        t("Zelda=7 Gannon=3,6 Link=4", "Gannon=3,6 Link=4 Zelda=7")?;
551
552        Ok(())
553    }
554
555    #[test]
556    fn test_invalid() {
557        fn t(s: &str) -> ParseError {
558            let protos: Result<Protocols, ParseError> = s.parse();
559            assert!(protos.is_err());
560            protos.err().unwrap()
561        }
562
563        assert_eq!(t("Link=1-100"), ParseError::OutOfRange);
564        assert_eq!(t("Zelda=100"), ParseError::OutOfRange);
565        assert_eq!(t("Link=100-200"), ParseError::OutOfRange);
566
567        assert_eq!(t("Link=1,1"), ParseError::Duplicate);
568        assert_eq!(t("Link=1 Link=1"), ParseError::Duplicate);
569        assert_eq!(t("Link=1 Link=3"), ParseError::Duplicate);
570        assert_eq!(t("Zelda=1 Zelda=3"), ParseError::Duplicate);
571
572        assert_eq!(t("Link=Zelda"), ParseError::Malformed);
573        assert_eq!(t("Link=6-2"), ParseError::Malformed);
574        assert_eq!(t("Link=6-"), ParseError::Malformed);
575        assert_eq!(t("Link=6-,2"), ParseError::Malformed);
576        assert_eq!(t("Link=1,,2"), ParseError::Malformed);
577        assert_eq!(t("Link=6-frog"), ParseError::Malformed);
578        assert_eq!(t("Link=gannon-9"), ParseError::Malformed);
579        assert_eq!(t("Link Zelda"), ParseError::Malformed);
580
581        assert_eq!(t("Link=01"), ParseError::Malformed);
582        assert_eq!(t("Link=waffle"), ParseError::Malformed);
583        assert_eq!(t("Link=1_1"), ParseError::Malformed);
584    }
585
586    #[test]
587    fn test_supports() -> Result<(), ParseError> {
588        let p: Protocols = "Link=4,5-7 Padding=2 Lonk=1-3,5".parse()?;
589
590        assert!(p.supports_known_subver(ProtoKind::Padding, 2));
591        assert!(!p.supports_known_subver(ProtoKind::Padding, 1));
592        assert!(p.supports_known_subver(ProtoKind::Link, 6));
593        assert!(!p.supports_known_subver(ProtoKind::Link, 255));
594        assert!(!p.supports_known_subver(ProtoKind::Cons, 1));
595        assert!(!p.supports_known_subver(ProtoKind::Cons, 0));
596        assert!(p.supports_subver("Link", 6));
597        assert!(!p.supports_subver("link", 6));
598        assert!(!p.supports_subver("Cons", 0));
599        assert!(p.supports_subver("Lonk", 3));
600        assert!(!p.supports_subver("Lonk", 4));
601        assert!(!p.supports_subver("lonk", 3));
602        assert!(!p.supports_subver("Lonk", 64));
603
604        Ok(())
605    }
606}