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

            
15
use crate::{MdDigest, MdReceiver, PartialNetDir};
16
use std::iter;
17
use std::net::SocketAddr;
18
use std::time::{Duration, SystemTime};
19
#[cfg(feature = "geoip")]
20
use tor_geoip::GeoipDb;
21
use tor_netdoc::doc::microdesc::{Microdesc, MicrodescBuilder};
22
use tor_netdoc::doc::netstatus::{ConsensusBuilder, MdConsensus, MdConsensusRouterStatus};
23
use tor_netdoc::doc::netstatus::{Lifetime, RelayFlags, RelayWeight, RouterStatusBuilder};
24

            
25
pub use tor_netdoc::{BuildError, BuildResult};
26

            
27
/// A set of builder objects for a single node.
28
#[derive(Debug, Clone)]
29
#[non_exhaustive]
30
pub 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.
50
154240
pub fn simple_net_func(
51
154240
    _idx: usize,
52
154240
    _nb: &mut NodeBuilders,
53
154240
    _bld: &mut ConsensusBuilder<MdConsensusRouterStatus>,
54
154240
) {
55
154240
}
56

            
57
/// As [`construct_network()`], but return a [`PartialNetDir`].
58
3086
pub fn construct_netdir() -> PartialNetDir {
59
3086
    construct_custom_netdir(simple_net_func).expect("failed to build default testing netdir")
60
3086
}
61

            
62
/// As [`construct_custom_network()`], but return a [`PartialNetDir`],
63
/// and allow network parameter customisation.
64
3270
pub fn construct_custom_netdir_with_params<F, P, PK>(
65
3270
    func: F,
66
3270
    params: P,
67
3270
    lifetime: Option<Lifetime>,
68
3270
) -> BuildResult<PartialNetDir>
69
3270
where
70
3270
    F: FnMut(usize, &mut NodeBuilders, &mut ConsensusBuilder<MdConsensusRouterStatus>),
71
3270
    P: IntoIterator<Item = (PK, i32)>,
72
3270
    PK: Into<String>,
73
3270
{
74
3270
    construct_custom_netdir_with_params_inner(
75
3270
        func,
76
3270
        params,
77
3270
        lifetime,
78
3270
        #[cfg(feature = "geoip")]
79
3270
        None,
80
3270
    )
81
3270
}
82

            
83
/// Implementation of `construct_custom_netdir_with_params`, written this way to avoid
84
/// the GeoIP argument crossing a crate API boundary.
85
3272
fn construct_custom_netdir_with_params_inner<F, P, PK>(
86
3272
    func: F,
87
3272
    params: P,
88
3272
    lifetime: Option<Lifetime>,
89
3272
    #[cfg(feature = "geoip")] geoip_db: Option<&GeoipDb>,
90
3272
) -> BuildResult<PartialNetDir>
91
3272
where
92
3272
    F: FnMut(usize, &mut NodeBuilders, &mut ConsensusBuilder<MdConsensusRouterStatus>),
93
3272
    P: IntoIterator<Item = (PK, i32)>,
94
3272
    PK: Into<String>,
95
3272
{
96
3272
    let (consensus, microdescs) = construct_custom_network(func, lifetime)?;
97
    #[cfg(feature = "geoip")]
98
3272
    let mut dir = if let Some(db) = geoip_db {
99
2
        PartialNetDir::new_with_geoip(consensus, Some(&params.into_iter().collect()), db)
100
    } else {
101
3270
        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
134144
    for md in microdescs {
106
130872
        dir.add_microdesc(md);
107
130872
    }
108

            
109
3272
    Ok(dir)
110
3272
}
111

            
112
/// As [`construct_custom_network()`], but return a [`PartialNetDir`].
113
3190
pub fn construct_custom_netdir<F>(func: F) -> BuildResult<PartialNetDir>
114
3190
where
115
3190
    F: FnMut(usize, &mut NodeBuilders, &mut ConsensusBuilder<MdConsensusRouterStatus>),
116
3190
{
117
3190
    construct_custom_netdir_with_params(func, iter::empty::<(&str, _)>(), None)
118
3190
}
119

            
120
#[cfg(feature = "geoip")]
121
/// As [`construct_custom_netdir()`], but with a `GeoipDb`.
122
2
pub fn construct_custom_netdir_with_geoip<F>(func: F, db: &GeoipDb) -> BuildResult<PartialNetDir>
123
2
where
124
2
    F: FnMut(usize, &mut NodeBuilders, &mut ConsensusBuilder<MdConsensusRouterStatus>),
125
2
{
126
2
    construct_custom_netdir_with_params_inner(func, iter::empty::<(&str, _)>(), None, Some(db))
127
2
}
128

            
129
/// As [`construct_custom_network`], but do not require a
130
/// customization function.
131
730
pub fn construct_network() -> BuildResult<(MdConsensus, Vec<Microdesc>)> {
132
730
    construct_custom_network(simple_net_func, None)
133
730
}
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.
188
4004
pub fn construct_custom_network<F>(
189
4004
    mut func: F,
190
4004
    lifetime: Option<Lifetime>,
191
4004
) -> BuildResult<(MdConsensus, Vec<Microdesc>)>
192
4004
where
193
4004
    F: FnMut(usize, &mut NodeBuilders, &mut ConsensusBuilder<MdConsensusRouterStatus>),
194
4004
{
195
4004
    let f = RelayFlags::RUNNING
196
4004
        | RelayFlags::VALID
197
4004
        | RelayFlags::V2DIR
198
4004
        | RelayFlags::FAST
199
4004
        | RelayFlags::STABLE;
200
4004
    // define 4 groups of flags
201
4004
    let flags = [
202
4004
        f | RelayFlags::HSDIR,
203
4004
        f | RelayFlags::EXIT,
204
4004
        f | RelayFlags::GUARD,
205
4004
        f | RelayFlags::EXIT | RelayFlags::GUARD,
206
4004
    ];
207

            
208
4004
    let lifetime = lifetime.map(Ok).unwrap_or_else(|| {
209
3964
        let now = SystemTime::now();
210
3964
        let one_day = Duration::new(86400, 0);
211
3964

            
212
3964
        Lifetime::new(now, now + one_day / 2, now + one_day)
213
4004
    })?;
214

            
215
4004
    let mut bld = MdConsensus::builder();
216
4004
    bld.consensus_method(34)
217
4004
        .lifetime(lifetime)
218
4004
        .param("bwweightscale", 1)
219
4004
        .weights("".parse()?);
220

            
221
4004
    let mut microdescs = Vec::new();
222
164164
    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
160160
        let flags = flags[(idx / 10) as usize];
227
160160
        let policy = if flags.contains(RelayFlags::EXIT) {
228
80080
            if idx % 2 == 1 {
229
40040
                "accept 80,443"
230
            } else {
231
40040
                "accept 1-65535"
232
            }
233
        } else {
234
80080
            "reject 1-65535"
235
        };
236
        // everybody is family with the adjacent relay.
237
160160
        let fam_id = [idx ^ 1; 20];
238
160160
        let family = hex::encode(fam_id);
239
160160

            
240
160160
        let mut md_builder = Microdesc::builder();
241
160160
        md_builder
242
160160
            .ntor_key((*b"----nothing in dirmgr uses this-").into())
243
160160
            .ed25519_id([idx; 32].into())
244
160160
            .family(family.parse().unwrap())
245
160160
            .parse_ipv4_policy(policy)
246
160160
            .unwrap();
247
160160
        let protocols = if idx % 2 == 0 {
248
            // even-numbered relays are dircaches.
249
80080
            "DirCache=2".parse().unwrap()
250
        } else {
251
80080
            "".parse().unwrap()
252
        };
253
160160
        let weight = RelayWeight::Measured(1000 * u32::from(idx % 10 + 1));
254
160160
        let mut rs_builder = bld.rs();
255
160160
        rs_builder
256
160160
            .identity([idx; 20].into())
257
160160
            .add_or_port(SocketAddr::from(([idx % 5, 0, 0, 3], 9001)))
258
160160
            .protos(protocols)
259
160160
            .set_flags(flags)
260
160160
            .weight(weight);
261
160160

            
262
160160
        let mut node_builders = NodeBuilders {
263
160160
            rs: rs_builder,
264
160160
            md: md_builder,
265
160160
            omit_rs: false,
266
160160
            omit_md: false,
267
160160
        };
268
160160

            
269
160160
        func(idx as usize, &mut node_builders, &mut bld);
270

            
271
160160
        let md = node_builders.md.testing_md()?;
272
160160
        let md_digest = *md.digest();
273
160160
        if !node_builders.omit_md {
274
160152
            microdescs.push(md);
275
160152
        }
276

            
277
160160
        if !node_builders.omit_rs {
278
159462
            node_builders
279
159462
                .rs
280
159462
                .doc_digest(md_digest)
281
159462
                .build_into(&mut bld)?;
282
698
        }
283
    }
284

            
285
4004
    let consensus = bld.testing_consensus()?;
286

            
287
4004
    Ok((consensus, microdescs))
288
4004
}
289

            
290
#[cfg(test)]
291
mod 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
}