//! Parsing implementation for networkstatus documents. //! //! In Tor, a networkstatus documents describes a complete view of the //! relays in the network: how many there are, how to contact them, //! and so forth. //! //! A networkstatus document can either be a "votes" -- an authority's //! view of the network, used as input to the voting process -- or a //! "consensus" -- a combined view of the network based on multiple //! authorities' votes, and signed by multiple authorities. //! //! A consensus document can itself come in two different flavors: a //! "ns"-flavored consensus has references to router descriptors, and //! a "microdesc"-flavored consensus has references to //! microdescriptors. //! //! To keep an up-to-date view of the network, clients download //! microdescriptor-flavored consensuses periodically, and then //! download whatever microdescriptors the consensus lists that the //! client doesn't already have. //! //! For full information about the network status format, see //! [dir-spec.txt](https://spec.torproject.org/dir-spec). //! //! # Limitations //! //! NOTE: The consensus format has changes time, using a //! "consensus-method" mechanism. This module is does not yet handle all //! all historical consensus-methods. //! //! NOTE: This module _does_ parse some fields that are not in current //! use, like relay nicknames, and the "published" times on //! microdescriptors. We should probably decide whether we actually //! want to do this. //! //! TODO: This module doesn't implement vote parsing at all yet. //! //! TODO: This module doesn't implement ns-flavored consensuses. //! //! TODO: More testing is needed! //! //! TODO: There should be accessor functions for most of the fields here. //! As with the other tor-netdoc types, I'm deferring those till I know what //! they should be. mod rs; #[cfg(feature = "build_docs")] mod build; use crate::doc::authcert::{AuthCert, AuthCertKeyIds}; use crate::parse::keyword::Keyword; use crate::parse::parser::{Section, SectionRules}; use crate::parse::tokenize::{Item, ItemResult, NetDocReader}; use crate::types::misc::*; use crate::util::private::Sealed; use crate::{Error, ParseErrorKind as EK, Pos, Result}; use std::collections::{HashMap, HashSet}; use std::{net, result, time}; use tor_error::internal; use tor_protover::Protocols; use bitflags::bitflags; use digest::Digest; use once_cell::sync::Lazy; use tor_checkable::{timed::TimerangeBound, ExternallySigned}; use tor_llcrypto as ll; use tor_llcrypto::pk::rsa::RsaIdentity; use serde::{Deserialize, Deserializer}; #[cfg(feature = "build_docs")] pub use build::ConsensusBuilder; #[cfg(feature = "build_docs")] pub use rs::build::RouterStatusBuilder; pub use rs::MdConsensusRouterStatus; #[cfg(feature = "ns_consensus")] pub use rs::NsConsensusRouterStatus; /// The lifetime of a networkstatus document. /// /// In a consensus, this type describes when the consensus may safely /// be used. In a vote, this type describes the proposed lifetime for a /// consensus. #[derive(Clone, Debug)] pub struct Lifetime { /// Time at which the document becomes valid valid_after: time::SystemTime, /// Time after which there is expected to be a better version /// of this consensus fresh_until: time::SystemTime, /// Time after which this consensus is expired. /// /// (In practice, Tor clients will keep using documents for a while /// after this expiration time, if no better one can be found.) valid_until: time::SystemTime, } impl Lifetime { /// Construct a new Lifetime. pub fn new( valid_after: time::SystemTime, fresh_until: time::SystemTime, valid_until: time::SystemTime, ) -> Result { if valid_after < fresh_until && fresh_until < valid_until { Ok(Lifetime { valid_after, fresh_until, valid_until, }) } else { Err(EK::InvalidLifetime.err()) } } /// Return time when this consensus first becomes valid. /// /// (You might see a consensus a little while before this time, /// since voting tries to finish up before the.) pub fn valid_after(&self) -> time::SystemTime { self.valid_after } /// Return time when this consensus is no longer fresh. /// /// You can use the consensus after this time, but there is (or is /// supposed to be) a better one by this point. pub fn fresh_until(&self) -> time::SystemTime { self.fresh_until } /// Return the time when this consensus is no longer valid. /// /// You should try to get a better consensus after this time, /// though it's okay to keep using this one if no more recent one /// can be found. pub fn valid_until(&self) -> time::SystemTime { self.valid_until } /// Return true if this consensus is officially valid at the provided time. pub fn valid_at(&self, when: time::SystemTime) -> bool { self.valid_after <= when && when <= self.valid_until } } /// A set of named network parameters. /// /// These are used to describe current settings for the Tor network, /// current weighting parameters for path selection, and so on. They're /// encoded with a space-separated K=V format. /// /// A `NetParams` is part of the validated directory manager configuration, /// where it is built (in the builder-pattern sense) from a transparent HashMap. #[derive(Debug, Clone, Default, Eq, PartialEq)] pub struct NetParams { /// Map from keys to values. params: HashMap, } impl NetParams { /// Create a new empty list of NetParams. #[allow(unused)] pub fn new() -> Self { NetParams { params: HashMap::new(), } } /// Retrieve a given network parameter, if it is present. pub fn get>(&self, v: A) -> Option<&T> { self.params.get(v.as_ref()) } /// Return an iterator over all key value pairs in an arbitrary order. pub fn iter(&self) -> impl Iterator { self.params.iter() } /// Set or replace the value of a network parameter. pub fn set(&mut self, k: String, v: T) { self.params.insert(k, v); } } impl, T> FromIterator<(K, T)> for NetParams { fn from_iter>(i: I) -> Self { NetParams { params: i.into_iter().map(|(k, v)| (k.into(), v)).collect(), } } } impl<'de, T> Deserialize<'de> for NetParams where T: Deserialize<'de>, { fn deserialize(deserializer: D) -> std::result::Result where D: Deserializer<'de>, { let params = HashMap::deserialize(deserializer)?; Ok(NetParams { params }) } } /// A list of subprotocol versions that implementors should/must provide. #[allow(dead_code)] #[derive(Debug, Clone, Default)] pub struct ProtoStatus { /// Set of protocols that are recommended; if we're missing a protocol /// in this list we should warn the user. recommended: Protocols, /// Set of protocols that are required; if we're missing a protocol /// in this list we should refuse to start. required: Protocols, } /// A recognized 'flavor' of consensus document. #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)] #[non_exhaustive] pub enum ConsensusFlavor { /// A "microdesc"-flavored consensus. This is the one that /// clients and relays use today. Microdesc, /// A "networkstatus"-flavored consensus. It's used for /// historical and network-health purposes. Instead of listing /// microdescriptor digests, it lists digests of full relay /// descriptors. Ns, } impl ConsensusFlavor { /// Return the name of this consensus flavor. pub fn name(&self) -> &'static str { match self { ConsensusFlavor::Ns => "ns", ConsensusFlavor::Microdesc => "microdesc", } } /// Try to find the flavor whose name is `name`. /// /// For historical reasons, an unnamed flavor indicates an "Ns" /// document. pub fn from_opt_name(name: Option<&str>) -> Result { match name { Some("microdesc") => Ok(ConsensusFlavor::Microdesc), Some("ns") | None => Ok(ConsensusFlavor::Ns), Some(other) => { Err(EK::BadDocumentType.with_msg(format!("unrecognized flavor {:?}", other))) } } } } /// The signature of a single directory authority on a networkstatus document. #[allow(dead_code)] #[cfg_attr( feature = "dangerous-expose-struct-fields", visible::StructFields(pub), non_exhaustive )] #[derive(Debug, Clone)] pub struct Signature { /// The name of the digest algorithm used to make the signature. /// /// Currently sha1 and sh256 are recognized. Here we only support /// sha256. #[cfg_attr(docsrs, doc(cfg(feature = "dangerous-expose-struct-fields")))] digestname: String, /// Fingerprints of the keys for the authority that made /// this signature. #[cfg_attr(docsrs, doc(cfg(feature = "dangerous-expose-struct-fields")))] key_ids: AuthCertKeyIds, /// The signature itself. #[cfg_attr(docsrs, doc(cfg(feature = "dangerous-expose-struct-fields")))] signature: Vec, } /// A collection of signatures that can be checked on a networkstatus document #[allow(dead_code)] #[cfg_attr( feature = "dangerous-expose-struct-fields", visible::StructFields(pub), non_exhaustive )] #[derive(Debug, Clone)] pub struct SignatureGroup { /// The sha256 of the document itself #[cfg_attr(docsrs, doc(cfg(feature = "dangerous-expose-struct-fields")))] sha256: Option<[u8; 32]>, /// The sha1 of the document itself #[cfg_attr(docsrs, doc(cfg(feature = "dangerous-expose-struct-fields")))] sha1: Option<[u8; 20]>, /// The signatures listed on the document. #[cfg_attr(docsrs, doc(cfg(feature = "dangerous-expose-struct-fields")))] signatures: Vec, } /// A shared random value produced by the directory authorities. #[derive( Debug, Clone, Copy, Eq, PartialEq, derive_more::From, derive_more::Into, derive_more::AsRef, )] // TODO hs: Use CtBytes for this. I don't think it actually matters, but it // seems like a good idea. pub struct SharedRandVal([u8; 32]); /// A shared-random value produced by the directory authorities, /// along with meta-information about that value. #[allow(dead_code)] // TODO hs: This should have real accessors, not this 'visible/visibility' hack. #[cfg_attr( feature = "dangerous-expose-struct-fields", visible::StructFields(pub), visibility::make(pub), non_exhaustive )] #[derive(Debug, Clone)] pub struct SharedRandStatus { /// How many authorities revealed shares that contributed to this value. #[cfg_attr(docsrs, doc(cfg(feature = "dangerous-expose-struct-fields")))] n_reveals: u8, /// The current random value. /// /// The properties of the secure shared-random system guarantee /// that this value isn't predictable before it first becomes /// live, and that a hostile party could not have forced it to /// have any more than a small number of possible random values. #[cfg_attr(docsrs, doc(cfg(feature = "dangerous-expose-struct-fields")))] value: SharedRandVal, /// The time when this SharedRandVal becomes (or became) the latest. /// /// (This is added per proposal 342, assuming that gets accepted.) #[cfg_attr(docsrs, doc(cfg(feature = "dangerous-expose-struct-fields")))] timestamp: Option, } /// Parts of the networkstatus header that are present in every networkstatus. /// /// NOTE: this type is separate from the header parts that are only in /// votes or only in consensuses, even though we don't implement votes yet. #[allow(dead_code)] #[cfg_attr( feature = "dangerous-expose-struct-fields", visible::StructFields(pub), visibility::make(pub), non_exhaustive )] #[derive(Debug, Clone)] struct CommonHeader { /// What kind of consensus document is this? Absent in votes and /// in ns-flavored consensuses. #[cfg_attr(docsrs, doc(cfg(feature = "dangerous-expose-struct-fields")))] flavor: ConsensusFlavor, /// Over what time is this consensus valid? (For votes, this is /// the time over which the voted-upon consensus should be valid.) #[cfg_attr(docsrs, doc(cfg(feature = "dangerous-expose-struct-fields")))] lifetime: Lifetime, /// List of recommended Tor client versions. #[cfg_attr(docsrs, doc(cfg(feature = "dangerous-expose-struct-fields")))] client_versions: Vec, /// List of recommended Tor relay versions. #[cfg_attr(docsrs, doc(cfg(feature = "dangerous-expose-struct-fields")))] relay_versions: Vec, /// Lists of recommended and required subprotocol versions for clients #[cfg_attr(docsrs, doc(cfg(feature = "dangerous-expose-struct-fields")))] client_protos: ProtoStatus, /// Lists of recommended and required subprotocol versions for relays #[cfg_attr(docsrs, doc(cfg(feature = "dangerous-expose-struct-fields")))] relay_protos: ProtoStatus, /// Declared parameters for tunable settings about how to the /// network should operator. Some of these adjust timeouts and /// whatnot; some features things on and off. #[cfg_attr(docsrs, doc(cfg(feature = "dangerous-expose-struct-fields")))] params: NetParams, /// How long in seconds should voters wait for votes and /// signatures (respectively) to propagate? #[cfg_attr(docsrs, doc(cfg(feature = "dangerous-expose-struct-fields")))] voting_delay: Option<(u32, u32)>, } /// The header of a consensus networkstatus. #[allow(dead_code)] #[cfg_attr( feature = "dangerous-expose-struct-fields", visible::StructFields(pub), visibility::make(pub), non_exhaustive )] #[derive(Debug, Clone)] struct ConsensusHeader { /// Header fields common to votes and consensuses #[cfg_attr(docsrs, doc(cfg(feature = "dangerous-expose-struct-fields")))] hdr: CommonHeader, /// What "method" was used to produce this consensus? (A /// consensus method is a version number used by authorities to /// upgrade the consensus algorithm.) #[cfg_attr(docsrs, doc(cfg(feature = "dangerous-expose-struct-fields")))] consensus_method: u32, /// Global shared-random value for the previous shared-random period. #[cfg_attr(docsrs, doc(cfg(feature = "dangerous-expose-struct-fields")))] shared_rand_prev: Option, /// Global shared-random value for the current shared-random period. #[cfg_attr(docsrs, doc(cfg(feature = "dangerous-expose-struct-fields")))] shared_rand_cur: Option, } /// Description of an authority's identity and address. /// /// (Corresponds to a dir-source line.) #[allow(dead_code)] #[cfg_attr( feature = "dangerous-expose-struct-fields", visible::StructFields(pub), visibility::make(pub), non_exhaustive )] #[derive(Debug, Clone)] struct DirSource { /// human-readable nickname for this authority. #[cfg_attr(docsrs, doc(cfg(feature = "dangerous-expose-struct-fields")))] nickname: String, /// Fingerprint for the _authority_ identity key of this /// authority. /// /// This is the same key as the one that signs the authority's /// certificates. #[cfg_attr(docsrs, doc(cfg(feature = "dangerous-expose-struct-fields")))] identity: RsaIdentity, /// IP address for the authority #[cfg_attr(docsrs, doc(cfg(feature = "dangerous-expose-struct-fields")))] ip: net::IpAddr, /// HTTP directory port for this authority #[cfg_attr(docsrs, doc(cfg(feature = "dangerous-expose-struct-fields")))] dir_port: u16, /// OR port for this authority. #[cfg_attr(docsrs, doc(cfg(feature = "dangerous-expose-struct-fields")))] or_port: u16, } bitflags! { /// A set of recognized directory flags on a single relay. /// /// These flags come from a consensus directory document, and are /// used to describe what the authorities believe about the relay. /// If the document contained any flags that we _didn't_ recognize, /// they are not listed in this type. /// /// The bit values used to represent the flags have no meaning. pub struct RelayFlags: u16 { /// Is this a directory authority? const AUTHORITY = (1<<0); /// Is this relay marked as a bad exit? /// /// Bad exits can be used as intermediate relays, but not to /// deliver traffic. const BAD_EXIT = (1<<1); /// Is this relay marked as an exit for weighting purposes? const EXIT = (1<<2); /// Is this relay considered "fast" above a certain threshold? const FAST = (1<<3); /// Is this relay suitable for use as a guard relay? /// /// Clients choose their their initial relays from among the set /// of Guard relays. const GUARD = (1<<4); /// Does this relay participate on the onion service directory /// ring? const HSDIR = (1<<5); /// If set, there is no consensus for the ed25519 key for this relay. const NO_ED_CONSENSUS = (1<<6); /// Is this relay considered "stable" enough for long-lived circuits? const STABLE = (1<<7); /// Set if the authorities are requesting a fresh descriptor for /// this relay. const STALE_DESC = (1<<8); /// Set if this relay is currently running. /// /// This flag can appear in votes, but in consensuses, every relay /// is assumed to be running. const RUNNING = (1<<9); /// Set if this relay is considered "valid" -- allowed to be on /// the network. /// /// This flag can appear in votes, but in consensuses, every relay /// is assumed to be valid. const VALID = (1<<10); /// Set if this relay supports a currently recognized version of the /// directory protocol. const V2DIR = (1<<11); } } /// Recognized weight fields on a single relay in a consensus #[non_exhaustive] #[derive(Debug, Clone, Copy)] pub enum RelayWeight { /// An unmeasured weight for a relay. Unmeasured(u32), /// An measured weight for a relay. Measured(u32), } impl RelayWeight { /// Return true if this weight is the result of a successful measurement pub fn is_measured(&self) -> bool { matches!(self, RelayWeight::Measured(_)) } /// Return true if this weight is nonzero pub fn is_nonzero(&self) -> bool { !matches!(self, RelayWeight::Unmeasured(0) | RelayWeight::Measured(0)) } } /// All information about a single authority, as represented in a consensus #[allow(dead_code)] #[cfg_attr( feature = "dangerous-expose-struct-fields", visible::StructFields(pub), visibility::make(pub), non_exhaustive )] #[derive(Debug, Clone)] struct ConsensusVoterInfo { /// Contents of the dirsource line about an authority #[cfg_attr(docsrs, doc(cfg(feature = "dangerous-expose-struct-fields")))] dir_source: DirSource, /// Human-readable contact information about the authority #[cfg_attr(docsrs, doc(cfg(feature = "dangerous-expose-struct-fields")))] contact: String, /// Digest of the vote that the authority cast to contribute to /// this consensus. #[cfg_attr(docsrs, doc(cfg(feature = "dangerous-expose-struct-fields")))] vote_digest: Vec, } /// The signed footer of a consensus netstatus. #[allow(dead_code)] #[cfg_attr( feature = "dangerous-expose-struct-fields", visible::StructFields(pub), visibility::make(pub), non_exhaustive )] #[derive(Debug, Clone)] struct Footer { /// Weights to be applied to certain classes of relays when choosing /// for different roles. /// /// For example, we want to avoid choosing exits for non-exit /// roles when overall the proportion of exits is small. #[cfg_attr(docsrs, doc(cfg(feature = "dangerous-expose-struct-fields")))] weights: NetParams, } /// Trait to parse a single relay as listed in a consensus document. /// /// TODO(nickm): I'd rather not have this trait be public, but I haven't yet /// figured out how to make it private. pub trait ParseRouterStatus: Sized + Sealed { /// Parse this object from a `Section` object containing its /// elements. fn from_section(sec: &Section<'_, NetstatusKwd>) -> Result; /// Return the networkstatus consensus flavor in which this /// routerstatus appears. fn flavor() -> ConsensusFlavor; } /// Represents a single relay as listed in a consensus document. /// /// Not implementable outside of the `tor-netdoc` crate. pub trait RouterStatus: Sealed { /// A digest of the document that's identified by this RouterStatus. type DocumentDigest: Clone; /// Return RSA identity for the relay described by this RouterStatus fn rsa_identity(&self) -> &RsaIdentity; /// Return the digest of the document identified by this /// routerstatus. fn doc_digest(&self) -> &Self::DocumentDigest; } /// A single microdescriptor consensus netstatus /// /// TODO: This should possibly turn into a parameterized type, to represent /// votes and ns consensuses. #[allow(dead_code)] #[cfg_attr( feature = "dangerous-expose-struct-fields", visible::StructFields(pub), non_exhaustive )] #[derive(Debug, Clone)] pub struct Consensus { /// Part of the header shared by all consensus types. #[cfg_attr(docsrs, doc(cfg(feature = "dangerous-expose-struct-fields")))] header: ConsensusHeader, /// List of voters whose votes contributed to this consensus. #[cfg_attr(docsrs, doc(cfg(feature = "dangerous-expose-struct-fields")))] voters: Vec, /// A list of routerstatus entries for the relays on the network, /// with one entry per relay. #[cfg_attr(docsrs, doc(cfg(feature = "dangerous-expose-struct-fields")))] relays: Vec, /// Footer for the consensus object. #[cfg_attr(docsrs, doc(cfg(feature = "dangerous-expose-struct-fields")))] footer: Footer, } /// A consensus document that lists relays along with their /// microdescriptor documents. pub type MdConsensus = Consensus; /// An MdConsensus that has been parsed and checked for timeliness, /// but not for signatures. pub type UnvalidatedMdConsensus = UnvalidatedConsensus; /// An MdConsensus that has been parsed but not checked for signatures /// and timeliness. pub type UncheckedMdConsensus = UncheckedConsensus; #[cfg(feature = "ns_consensus")] /// A consensus document that lists relays along with their /// router descriptor documents. pub type NsConsensus = Consensus; #[cfg(feature = "ns_consensus")] /// An NsConsensus that has been parsed and checked for timeliness, /// but not for signatures. pub type UnvalidatedNsConsensus = UnvalidatedConsensus; #[cfg(feature = "ns_consensus")] /// An NsConsensus that has been parsed but not checked for signatures /// and timeliness. pub type UncheckedNsConsensus = UncheckedConsensus; impl Consensus { /// Return the Lifetime for this consensus. pub fn lifetime(&self) -> &Lifetime { &self.header.hdr.lifetime } /// Return a slice of all the routerstatus entries in this consensus. pub fn relays(&self) -> &[RS] { &self.relays[..] } /// Return a mapping from keywords to integers representing how /// to weight different kinds of relays in different path positions. pub fn bandwidth_weights(&self) -> &NetParams { &self.footer.weights } /// Return the map of network parameters that this consensus advertises. pub fn params(&self) -> &NetParams { &self.header.hdr.params } /// Return the latest shared random value, if the consensus /// contains one. pub fn shared_rand_cur(&self) -> Option<&SharedRandStatus> { self.header.shared_rand_cur.as_ref() } /// Return the previous shared random value, if the consensus /// contains one. pub fn shared_rand_prev(&self) -> Option<&SharedRandStatus> { self.header.shared_rand_prev.as_ref() } } decl_keyword! { /// Keywords that can be used in votes and consensuses. // TODO: This is public because otherwise we can't use it in the // ParseRouterStatus crate. But I'd rather find a way to make it // private. #[non_exhaustive] #[allow(missing_docs)] pub NetstatusKwd { // Header "network-status-version" => NETWORK_STATUS_VERSION, "vote-status" => VOTE_STATUS, "consensus-methods" => CONSENSUS_METHODS, "consensus-method" => CONSENSUS_METHOD, "published" => PUBLISHED, "valid-after" => VALID_AFTER, "fresh-until" => FRESH_UNTIL, "valid-until" => VALID_UNTIL, "voting-delay" => VOTING_DELAY, "client-versions" => CLIENT_VERSIONS, "server-versions" => SERVER_VERSIONS, "known-flags" => KNOWN_FLAGS, "flag-thresholds" => FLAG_THRESHOLDS, "recommended-client-protocols" => RECOMMENDED_CLIENT_PROTOCOLS, "required-client-protocols" => REQUIRED_CLIENT_PROTOCOLS, "recommended-relay-protocols" => RECOMMENDED_RELAY_PROTOCOLS, "required-relay-protocols" => REQUIRED_RELAY_PROTOCOLS, "params" => PARAMS, "bandwidth-file-headers" => BANDWIDTH_FILE_HEADERS, "bandwidth-file-digest" => BANDWIDTH_FILE_DIGEST, // "package" is now ignored. // header in consensus, voter section in vote? "shared-rand-previous-value" => SHARED_RAND_PREVIOUS_VALUE, "shared-rand-current-value" => SHARED_RAND_CURRENT_VALUE, // Voter section (both) "dir-source" => DIR_SOURCE, "contact" => CONTACT, // voter section (vote, but not consensus) "legacy-dir-key" => LEGACY_DIR_KEY, "shared-rand-participate" => SHARED_RAND_PARTICIPATE, "shared-rand-commit" => SHARED_RAND_COMMIT, // voter section (consensus, but not vote) "vote-digest" => VOTE_DIGEST, // voter cert beginning (but only the beginning) "dir-key-certificate-version" => DIR_KEY_CERTIFICATE_VERSION, // routerstatus "r" => RS_R, "a" => RS_A, "s" => RS_S, "v" => RS_V, "pr" => RS_PR, "w" => RS_W, "p" => RS_P, "m" => RS_M, "id" => RS_ID, // footer "directory-footer" => DIRECTORY_FOOTER, "bandwidth-weights" => BANDWIDTH_WEIGHTS, "directory-signature" => DIRECTORY_SIGNATURE, } } /// Shared parts of rules for all kinds of netstatus headers static NS_HEADER_RULES_COMMON_: Lazy> = Lazy::new(|| { use NetstatusKwd::*; let mut rules = SectionRules::new(); rules.add(NETWORK_STATUS_VERSION.rule().required().args(1..=2)); rules.add(VOTE_STATUS.rule().required().args(1..)); rules.add(VALID_AFTER.rule().required()); rules.add(FRESH_UNTIL.rule().required()); rules.add(VALID_UNTIL.rule().required()); rules.add(VOTING_DELAY.rule().args(2..)); rules.add(CLIENT_VERSIONS.rule()); rules.add(SERVER_VERSIONS.rule()); rules.add(KNOWN_FLAGS.rule().required()); rules.add(RECOMMENDED_CLIENT_PROTOCOLS.rule().args(1..)); rules.add(RECOMMENDED_RELAY_PROTOCOLS.rule().args(1..)); rules.add(REQUIRED_CLIENT_PROTOCOLS.rule().args(1..)); rules.add(REQUIRED_RELAY_PROTOCOLS.rule().args(1..)); rules.add(PARAMS.rule()); rules }); /// Rules for parsing the header of a consensus. static NS_HEADER_RULES_CONSENSUS: Lazy> = Lazy::new(|| { use NetstatusKwd::*; let mut rules = NS_HEADER_RULES_COMMON_.clone(); rules.add(CONSENSUS_METHOD.rule().args(1..=1)); rules.add(SHARED_RAND_PREVIOUS_VALUE.rule().args(2..)); rules.add(SHARED_RAND_CURRENT_VALUE.rule().args(2..)); rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional()); rules }); /* /// Rules for parsing the header of a vote. static NS_HEADER_RULES_VOTE: SectionRules = { use NetstatusKwd::*; let mut rules = NS_HEADER_RULES_COMMON_.clone(); rules.add(CONSENSUS_METHODS.rule().args(1..)); rules.add(FLAG_THRESHOLDS.rule()); rules.add(BANDWIDTH_FILE_HEADERS.rule()); rules.add(BANDWIDTH_FILE_DIGEST.rule().args(1..)); rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional()); rules }; /// Rules for parsing a single voter's information in a vote. static NS_VOTERINFO_RULES_VOTE: SectionRules = { use NetstatusKwd::*; let mut rules = SectionRules::new(); rules.add(DIR_SOURCE.rule().required().args(6..)); rules.add(CONTACT.rule().required()); rules.add(LEGACY_DIR_KEY.rule().args(1..)); rules.add(SHARED_RAND_PARTICIPATE.rule().no_args()); rules.add(SHARED_RAND_COMMIT.rule().may_repeat().args(4..)); rules.add(SHARED_RAND_PREVIOUS_VALUE.rule().args(2..)); rules.add(SHARED_RAND_CURRENT_VALUE.rule().args(2..)); // then comes an entire cert: When we implement vote parsing, // we should use the authcert code for handling that. rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional()); rules }; */ /// Rules for parsing a single voter's information in a consensus static NS_VOTERINFO_RULES_CONSENSUS: Lazy> = Lazy::new(|| { use NetstatusKwd::*; let mut rules = SectionRules::new(); rules.add(DIR_SOURCE.rule().required().args(6..)); rules.add(CONTACT.rule().required()); rules.add(VOTE_DIGEST.rule().required()); rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional()); rules }); /// Shared rules for parsing a single routerstatus static NS_ROUTERSTATUS_RULES_COMMON_: Lazy> = Lazy::new(|| { use NetstatusKwd::*; let mut rules = SectionRules::new(); rules.add(RS_A.rule().may_repeat().args(1..)); rules.add(RS_S.rule().required()); rules.add(RS_V.rule()); rules.add(RS_PR.rule().required()); rules.add(RS_W.rule()); rules.add(RS_P.rule().args(2..)); rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional()); rules }); /// Rules for parsing a single routerstatus in an NS consensus static NS_ROUTERSTATUS_RULES_NSCON: Lazy> = Lazy::new(|| { use NetstatusKwd::*; let mut rules = NS_ROUTERSTATUS_RULES_COMMON_.clone(); rules.add(RS_R.rule().required().args(8..)); rules }); /* /// Rules for parsing a single routerstatus in a vote static NS_ROUTERSTATUS_RULES_VOTE: SectionRules = { use NetstatusKwd::*; let mut rules = NS_ROUTERSTATUS_RULES_COMMON_.clone(); rules.add(RS_R.rule().required().args(8..)); rules.add(RS_M.rule().may_repeat().args(2..)); rules.add(RS_ID.rule().may_repeat().args(2..)); // may-repeat? rules }; */ /// Rules for parsing a single routerstatus in a microdesc consensus static NS_ROUTERSTATUS_RULES_MDCON: Lazy> = Lazy::new(|| { use NetstatusKwd::*; let mut rules = NS_ROUTERSTATUS_RULES_COMMON_.clone(); rules.add(RS_R.rule().required().args(6..)); rules.add(RS_M.rule().required().args(1..)); rules }); /// Rules for parsing consensus fields from a footer. static NS_FOOTER_RULES: Lazy> = Lazy::new(|| { use NetstatusKwd::*; let mut rules = SectionRules::new(); rules.add(DIRECTORY_FOOTER.rule().required().no_args()); // consensus only rules.add(BANDWIDTH_WEIGHTS.rule()); rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional()); rules }); impl ProtoStatus { /// Construct a ProtoStatus from two chosen keywords in a section. fn from_section( sec: &Section<'_, NetstatusKwd>, recommend_token: NetstatusKwd, required_token: NetstatusKwd, ) -> Result { /// Helper: extract a Protocols entry from an item's arguments. fn parse(t: Option<&Item<'_, NetstatusKwd>>) -> Result { if let Some(item) = t { item.args_as_str() .parse::() .map_err(|e| EK::BadArgument.at_pos(item.pos()).with_source(e)) } else { Ok(Protocols::new()) } } let recommended = parse(sec.get(recommend_token))?; let required = parse(sec.get(required_token))?; Ok(ProtoStatus { recommended, required, }) } } impl std::str::FromStr for NetParams where T: std::str::FromStr, T::Err: std::error::Error, { type Err = Error; fn from_str(s: &str) -> Result { /// Helper: parse a single K=V pair. fn parse_pair(p: &str) -> Result<(String, U)> where U: std::str::FromStr, U::Err: std::error::Error, { let parts: Vec<_> = p.splitn(2, '=').collect(); if parts.len() != 2 { return Err(EK::BadArgument .at_pos(Pos::at(p)) .with_msg("Missing = in key=value list")); } let num = parts[1].parse::().map_err(|e| { EK::BadArgument .at_pos(Pos::at(parts[1])) .with_msg(e.to_string()) })?; Ok((parts[0].to_string(), num)) } let params = s .split(' ') .filter(|p| !p.is_empty()) .map(parse_pair) .collect::>>()?; Ok(NetParams { params }) } } impl CommonHeader { /// Extract the CommonHeader members from a single header section. fn from_section(sec: &Section<'_, NetstatusKwd>) -> Result { use NetstatusKwd::*; { // this unwrap is safe because if there is not at least one // token in the section, the section is unparsable. #[allow(clippy::unwrap_used)] let first = sec.first_item().unwrap(); if first.kwd() != NETWORK_STATUS_VERSION { return Err(EK::UnexpectedToken .with_msg(first.kwd().to_str()) .at_pos(first.pos())); } } let ver_item = sec.required(NETWORK_STATUS_VERSION)?; let version: u32 = ver_item.parse_arg(0)?; if version != 3 { return Err(EK::BadDocumentVersion.with_msg(version.to_string())); } let flavor = ConsensusFlavor::from_opt_name(ver_item.arg(1))?; let valid_after = sec .required(VALID_AFTER)? .args_as_str() .parse::()? .into(); let fresh_until = sec .required(FRESH_UNTIL)? .args_as_str() .parse::()? .into(); let valid_until = sec .required(VALID_UNTIL)? .args_as_str() .parse::()? .into(); let lifetime = Lifetime::new(valid_after, fresh_until, valid_until)?; let client_versions = sec .maybe(CLIENT_VERSIONS) .args_as_str() .unwrap_or("") .split(',') .map(str::to_string) .collect(); let relay_versions = sec .maybe(SERVER_VERSIONS) .args_as_str() .unwrap_or("") .split(',') .map(str::to_string) .collect(); let client_protos = ProtoStatus::from_section( sec, RECOMMENDED_CLIENT_PROTOCOLS, REQUIRED_CLIENT_PROTOCOLS, )?; let relay_protos = ProtoStatus::from_section(sec, RECOMMENDED_RELAY_PROTOCOLS, REQUIRED_RELAY_PROTOCOLS)?; let params = sec.maybe(PARAMS).args_as_str().unwrap_or("").parse()?; let voting_delay = if let Some(tok) = sec.get(VOTING_DELAY) { let n1 = tok.parse_arg(0)?; let n2 = tok.parse_arg(1)?; Some((n1, n2)) } else { None }; Ok(CommonHeader { flavor, lifetime, client_versions, relay_versions, client_protos, relay_protos, params, voting_delay, }) } } impl SharedRandStatus { /// Parse a current or previous shared rand value from a given /// SharedRandPreviousValue or SharedRandCurrentValue. fn from_item(item: &Item<'_, NetstatusKwd>) -> Result { match item.kwd() { NetstatusKwd::SHARED_RAND_PREVIOUS_VALUE | NetstatusKwd::SHARED_RAND_CURRENT_VALUE => {} _ => { return Err(Error::from(internal!( "wrong keyword {:?} on shared-random value", item.kwd() )) .at_pos(item.pos())) } } let n_reveals: u8 = item.parse_arg(0)?; let val: B64 = item.parse_arg(1)?; let value = SharedRandVal(val.into_array()?); // Added in proposal 342 let timestamp = item .parse_optional_arg::(2)? .map(Into::into); Ok(SharedRandStatus { n_reveals, value, timestamp, }) } /// Return the actual shared random value. pub fn value(&self) -> &SharedRandVal { &self.value } /// Return the timestamp (if any) associated with this `SharedRandValue`. pub fn timestamp(&self) -> Option { self.timestamp } } impl ConsensusHeader { /// Parse the ConsensusHeader members from a provided section. fn from_section(sec: &Section<'_, NetstatusKwd>) -> Result { use NetstatusKwd::*; let status: &str = sec.required(VOTE_STATUS)?.arg(0).unwrap_or(""); if status != "consensus" { return Err(EK::BadDocumentType.err()); } // We're ignoring KNOWN_FLAGS in the consensus. let hdr = CommonHeader::from_section(sec)?; let consensus_method: u32 = sec.required(CONSENSUS_METHOD)?.parse_arg(0)?; let shared_rand_prev = sec .get(SHARED_RAND_PREVIOUS_VALUE) .map(SharedRandStatus::from_item) .transpose()?; let shared_rand_cur = sec .get(SHARED_RAND_CURRENT_VALUE) .map(SharedRandStatus::from_item) .transpose()?; Ok(ConsensusHeader { hdr, consensus_method, shared_rand_prev, shared_rand_cur, }) } } impl DirSource { /// Parse a "dir-source" item fn from_item(item: &Item<'_, NetstatusKwd>) -> Result { if item.kwd() != NetstatusKwd::DIR_SOURCE { return Err( Error::from(internal!("Bad keyword {:?} on dir-source", item.kwd())) .at_pos(item.pos()), ); } let nickname = item.required_arg(0)?.to_string(); let identity = item.parse_arg::(1)?.into(); let ip = item.parse_arg(3)?; let dir_port = item.parse_arg(4)?; let or_port = item.parse_arg(5)?; Ok(DirSource { nickname, identity, ip, dir_port, or_port, }) } } impl ConsensusVoterInfo { /// Parse a single ConsensusVoterInfo from a voter info section. fn from_section(sec: &Section<'_, NetstatusKwd>) -> Result { use NetstatusKwd::*; // this unwrap should be safe because if there is not at least one // token in the section, the section is unparsable. #[allow(clippy::unwrap_used)] let first = sec.first_item().unwrap(); if first.kwd() != DIR_SOURCE { return Err(Error::from(internal!( "Wrong keyword {:?} at start of voter info", first.kwd() )) .at_pos(first.pos())); } let dir_source = DirSource::from_item(sec.required(DIR_SOURCE)?)?; let contact = sec.required(CONTACT)?.args_as_str().to_string(); let vote_digest = sec.required(VOTE_DIGEST)?.parse_arg::(0)?.into(); Ok(ConsensusVoterInfo { dir_source, contact, vote_digest, }) } } impl std::str::FromStr for RelayFlags { type Err = std::convert::Infallible; fn from_str(s: &str) -> std::result::Result { Ok(match s { "Authority" => RelayFlags::AUTHORITY, "BadExit" => RelayFlags::BAD_EXIT, "Exit" => RelayFlags::EXIT, "Fast" => RelayFlags::FAST, "Guard" => RelayFlags::GUARD, "HSDir" => RelayFlags::HSDIR, "NoEdConsensus" => RelayFlags::NO_ED_CONSENSUS, "Stable" => RelayFlags::STABLE, "StaleDesc" => RelayFlags::STALE_DESC, "Running" => RelayFlags::RUNNING, "Valid" => RelayFlags::VALID, "V2Dir" => RelayFlags::V2DIR, _ => RelayFlags::empty(), }) } } impl RelayFlags { /// Parse a relay-flags entry from an "s" line. fn from_item(item: &Item<'_, NetstatusKwd>) -> Result { if item.kwd() != NetstatusKwd::RS_S { return Err( Error::from(internal!("Wrong keyword {:?} for S line", item.kwd())) .at_pos(item.pos()), ); } // These flags are implicit. let mut flags: RelayFlags = RelayFlags::RUNNING | RelayFlags::VALID; let mut prev: Option<&str> = None; for s in item.args() { if let Some(p) = prev { if p >= s { // Arguments out of order. return Err(EK::BadArgument .at_pos(item.pos()) .with_msg("Flags out of order")); } } match s.parse() { Ok(fl) => { flags |= fl; prev = Some(s); } Err(_e) => { return Err(EK::BadArgument .at_pos(item.pos()) .with_msg("failed to parse flag")) } }; } Ok(flags) } } impl Default for RelayWeight { fn default() -> RelayWeight { RelayWeight::Unmeasured(0) } } impl RelayWeight { /// Parse a routerweight from a "w" line. fn from_item(item: &Item<'_, NetstatusKwd>) -> Result { if item.kwd() != NetstatusKwd::RS_W { return Err( Error::from(internal!("Wrong keyword {:?} on W line", item.kwd())) .at_pos(item.pos()), ); } let params: NetParams = item.args_as_str().parse()?; let bw = params.params.get("Bandwidth"); let unmeas = params.params.get("Unmeasured"); let bw = match bw { None => return Ok(RelayWeight::Unmeasured(0)), Some(b) => *b, }; match unmeas { None | Some(0) => Ok(RelayWeight::Measured(bw)), Some(1) => Ok(RelayWeight::Unmeasured(bw)), _ => Err(EK::BadArgument .at_pos(item.pos()) .with_msg("unmeasured value")), } } } impl Footer { /// Parse a directory footer from a footer section. fn from_section(sec: &Section<'_, NetstatusKwd>) -> Result