tor_netdir/
testnet.rs

1//! Support for unit tests, in this crate and elsewhere.
2//!
3//! This module is only enabled when the `testing` feature is enabled.
4//!
5//! It is not covered by semver for the `tor-netdir` crate: see notes
6//! on [`construct_network`].
7//!
8//! # Panics
9//!
10//! These functions can panic on numerous possible internal failures:
11//! only use these functions for testing.
12
13#![allow(clippy::unwrap_used)]
14
15use crate::{MdDigest, MdReceiver, PartialNetDir};
16use std::iter;
17use std::net::SocketAddr;
18use std::time::{Duration, SystemTime};
19#[cfg(feature = "geoip")]
20use tor_geoip::GeoipDb;
21use tor_netdoc::doc::microdesc::{Microdesc, MicrodescBuilder};
22use tor_netdoc::doc::netstatus::{ConsensusBuilder, MdConsensus, MdConsensusRouterStatus};
23use tor_netdoc::doc::netstatus::{Lifetime, RelayFlags, RelayWeight, RouterStatusBuilder};
24
25pub use tor_netdoc::{BuildError, BuildResult};
26
27/// A set of builder objects for a single node.
28#[derive(Debug, Clone)]
29#[non_exhaustive]
30pub struct NodeBuilders {
31    /// Builds a routerstatus for a single node.
32    ///
33    /// Adjust fields in this builder to change the node's properties.
34    pub rs: RouterStatusBuilder<MdDigest>,
35
36    /// Builds a microdescriptor for a single node.
37    ///
38    /// Adjust fields in this builder in order to change the node's
39    /// properties.
40    pub md: MicrodescBuilder,
41
42    /// Set this value to `true` to omit the microdesc from the network.
43    pub omit_md: bool,
44
45    /// Set this value to `true` to omit the routerdesc from the network.
46    pub omit_rs: bool,
47}
48
49/// Helper: a customization function that does nothing.
50pub fn simple_net_func(
51    _idx: usize,
52    _nb: &mut NodeBuilders,
53    _bld: &mut ConsensusBuilder<MdConsensusRouterStatus>,
54) {
55}
56
57/// As [`construct_network()`], but return a [`PartialNetDir`].
58pub fn construct_netdir() -> PartialNetDir {
59    construct_custom_netdir(simple_net_func).expect("failed to build default testing netdir")
60}
61
62/// As [`construct_custom_network()`], but return a [`PartialNetDir`],
63/// and allow network parameter customisation.
64pub fn construct_custom_netdir_with_params<F, P, PK>(
65    func: F,
66    params: P,
67    lifetime: Option<Lifetime>,
68) -> BuildResult<PartialNetDir>
69where
70    F: FnMut(usize, &mut NodeBuilders, &mut ConsensusBuilder<MdConsensusRouterStatus>),
71    P: IntoIterator<Item = (PK, i32)>,
72    PK: Into<String>,
73{
74    construct_custom_netdir_with_params_inner(
75        func,
76        params,
77        lifetime,
78        #[cfg(feature = "geoip")]
79        None,
80    )
81}
82
83/// Implementation of `construct_custom_netdir_with_params`, written this way to avoid
84/// the GeoIP argument crossing a crate API boundary.
85fn construct_custom_netdir_with_params_inner<F, P, PK>(
86    func: F,
87    params: P,
88    lifetime: Option<Lifetime>,
89    #[cfg(feature = "geoip")] geoip_db: Option<&GeoipDb>,
90) -> BuildResult<PartialNetDir>
91where
92    F: FnMut(usize, &mut NodeBuilders, &mut ConsensusBuilder<MdConsensusRouterStatus>),
93    P: IntoIterator<Item = (PK, i32)>,
94    PK: Into<String>,
95{
96    let (consensus, microdescs) = construct_custom_network(func, lifetime)?;
97    #[cfg(feature = "geoip")]
98    let mut dir = if let Some(db) = geoip_db {
99        PartialNetDir::new_with_geoip(consensus, Some(&params.into_iter().collect()), db)
100    } else {
101        PartialNetDir::new(consensus, Some(&params.into_iter().collect()))
102    };
103    #[cfg(not(feature = "geoip"))]
104    let mut dir = PartialNetDir::new(consensus, Some(&params.into_iter().collect()));
105    for md in microdescs {
106        dir.add_microdesc(md);
107    }
108
109    Ok(dir)
110}
111
112/// As [`construct_custom_network()`], but return a [`PartialNetDir`].
113pub fn construct_custom_netdir<F>(func: F) -> BuildResult<PartialNetDir>
114where
115    F: FnMut(usize, &mut NodeBuilders, &mut ConsensusBuilder<MdConsensusRouterStatus>),
116{
117    construct_custom_netdir_with_params(func, iter::empty::<(&str, _)>(), None)
118}
119
120#[cfg(feature = "geoip")]
121/// As [`construct_custom_netdir()`], but with a `GeoipDb`.
122pub fn construct_custom_netdir_with_geoip<F>(func: F, db: &GeoipDb) -> BuildResult<PartialNetDir>
123where
124    F: FnMut(usize, &mut NodeBuilders, &mut ConsensusBuilder<MdConsensusRouterStatus>),
125{
126    construct_custom_netdir_with_params_inner(func, iter::empty::<(&str, _)>(), None, Some(db))
127}
128
129/// As [`construct_custom_network`], but do not require a
130/// customization function.
131pub fn construct_network() -> BuildResult<(MdConsensus, Vec<Microdesc>)> {
132    construct_custom_network(simple_net_func, None)
133}
134
135/// Build a fake network with enough information to enable some basic
136/// tests.
137///
138/// By default, the constructed network will contain 40 relays,
139/// numbered 0 through 39. They will have with RSA and Ed25519
140/// identity fingerprints set to 0x0000...00 through 0x2727...27.
141/// Each pair of relays is in a family with one another: 0x00..00 with
142/// 0x01..01, and so on.
143///
144/// All relays are marked as usable.  The first ten are marked with no
145/// additional flags.  The next ten are marked with the exit flag.
146/// The next ten are marked with the guard flag.  The last ten are
147/// marked with the exit _and_ guard flags.
148///
149/// TAP and Ntor onion keys are present, but unusable.
150///
151/// Odd-numbered exit relays are set to allow ports 80 and 443 on
152/// IPv4.  Even-numbered exit relays are set to allow ports 1-65535
153/// on IPv4.  No exit relays are marked to support IPv6.
154///
155/// Even-numbered relays support the `DirCache=2` protocol.
156///
157/// Every relay is given a measured weight based on its position
158/// within its group of ten.  The weights for the ten relays in each
159/// group are: 1000, 2000, 3000, ... 10000.  There is no additional
160/// flag-based bandwidth weighting.
161///
162/// The consensus is declared as using method 34, and as being valid for
163/// one day (in realtime) after the current `SystemTime`.
164///
165/// # Customization
166///
167/// Before each relay is added to the consensus or the network, it is
168/// passed through the provided filtering function.  This function
169/// receives as its arguments the current index (in range 0..40), a
170/// [`RouterStatusBuilder`], and a [`MicrodescBuilder`].  If it
171/// returns a `RouterStatusBuilder`, the corresponding router status
172/// is added to the consensus.  If it returns a `MicrodescBuilder`,
173/// the corresponding microdescriptor is added to the vector of
174/// microdescriptor.
175///
176/// # Notes for future expansion
177///
178/// _Resist the temptation to make unconditional changes to this
179/// function._ If the network generated by this function gets more and
180/// more complex, then it will become harder and harder over time to
181/// make it support new test cases and new behavior, and eventually
182/// we'll have to throw the whole thing away.  (We ran into this
183/// problem with Tor's unit tests.)
184///
185/// Instead, refactor this function so that it takes a
186/// description of what kind of network to build, and then builds it from
187/// that description.
188pub fn construct_custom_network<F>(
189    mut func: F,
190    lifetime: Option<Lifetime>,
191) -> BuildResult<(MdConsensus, Vec<Microdesc>)>
192where
193    F: FnMut(usize, &mut NodeBuilders, &mut ConsensusBuilder<MdConsensusRouterStatus>),
194{
195    let f = RelayFlags::RUNNING
196        | RelayFlags::VALID
197        | RelayFlags::V2DIR
198        | RelayFlags::FAST
199        | RelayFlags::STABLE;
200    // define 4 groups of flags
201    let flags = [
202        f | RelayFlags::HSDIR,
203        f | RelayFlags::EXIT,
204        f | RelayFlags::GUARD,
205        f | RelayFlags::EXIT | RelayFlags::GUARD,
206    ];
207
208    let lifetime = lifetime.map(Ok).unwrap_or_else(|| {
209        let now = SystemTime::now();
210        let one_day = Duration::new(86400, 0);
211
212        Lifetime::new(now, now + one_day / 2, now + one_day)
213    })?;
214
215    let mut bld = MdConsensus::builder();
216    bld.consensus_method(34)
217        .lifetime(lifetime)
218        .param("bwweightscale", 1)
219        .weights("".parse()?);
220
221    let mut microdescs = Vec::new();
222    for idx in 0..40_u8 {
223        // Each relay gets a couple of no-good onion keys.
224        // Its identity fingerprints are set to `idx`, repeating.
225        // They all get the same address.
226        let flags = flags[(idx / 10) as usize];
227        let policy = if flags.contains(RelayFlags::EXIT) {
228            if idx % 2 == 1 {
229                "accept 80,443"
230            } else {
231                "accept 1-65535"
232            }
233        } else {
234            "reject 1-65535"
235        };
236        // everybody is family with the adjacent relay.
237        let fam_id = [idx ^ 1; 20];
238        let family = hex::encode(fam_id);
239
240        let mut md_builder = Microdesc::builder();
241        md_builder
242            .ntor_key((*b"----nothing in dirmgr uses this-").into())
243            .ed25519_id([idx; 32].into())
244            .family(family.parse().unwrap())
245            .parse_ipv4_policy(policy)
246            .unwrap();
247        let protocols = if idx % 2 == 0 {
248            // even-numbered relays are dircaches.
249            "DirCache=2".parse().unwrap()
250        } else {
251            "".parse().unwrap()
252        };
253        let weight = RelayWeight::Measured(1000 * u32::from(idx % 10 + 1));
254        let mut rs_builder = bld.rs();
255        rs_builder
256            .identity([idx; 20].into())
257            .add_or_port(SocketAddr::from(([idx % 5, 0, 0, 3], 9001)))
258            .protos(protocols)
259            .set_flags(flags)
260            .weight(weight);
261
262        let mut node_builders = NodeBuilders {
263            rs: rs_builder,
264            md: md_builder,
265            omit_rs: false,
266            omit_md: false,
267        };
268
269        func(idx as usize, &mut node_builders, &mut bld);
270
271        let md = node_builders.md.testing_md()?;
272        let md_digest = *md.digest();
273        if !node_builders.omit_md {
274            microdescs.push(md);
275        }
276
277        if !node_builders.omit_rs {
278            node_builders
279                .rs
280                .doc_digest(md_digest)
281                .build_into(&mut bld)?;
282        }
283    }
284
285    let consensus = bld.testing_consensus()?;
286
287    Ok((consensus, microdescs))
288}
289
290#[cfg(test)]
291mod test {
292    // @@ begin test lint list maintained by maint/add_warning @@
293    #![allow(clippy::bool_assert_comparison)]
294    #![allow(clippy::clone_on_copy)]
295    #![allow(clippy::dbg_macro)]
296    #![allow(clippy::mixed_attributes_style)]
297    #![allow(clippy::print_stderr)]
298    #![allow(clippy::print_stdout)]
299    #![allow(clippy::single_char_pattern)]
300    #![allow(clippy::unwrap_used)]
301    #![allow(clippy::unchecked_duration_subtraction)]
302    #![allow(clippy::useless_vec)]
303    #![allow(clippy::needless_pass_by_value)]
304    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
305    use super::*;
306    #[test]
307    fn try_with_function() {
308        let mut val = 0_u32;
309        let _net = construct_custom_netdir(|_idx, _nb, _bld| {
310            val += 1;
311        });
312        assert_eq!(val, 40);
313    }
314}