1
//! Code for estimating good values for circuit timeouts.
2
//!
3
//! We need good circuit timeouts for two reasons: first, they help
4
//! user experience.  If user wait too long for their circuits, or if
5
//! they use exceptionally slow circuits, then Tor will feel really
6
//! slow.  Second, these timeouts are actually a security
7
//! property.
8
// TODO(nickm): explain why!
9

            
10
use std::time::Duration;
11

            
12
pub(crate) mod estimator;
13
pub(crate) mod pareto;
14
pub(crate) mod readonly;
15

            
16
pub(crate) use estimator::Estimator;
17

            
18
/// An object that calculates circuit timeout thresholds from the history
19
/// of circuit build times.
20
pub(crate) trait TimeoutEstimator {
21
    /// Record that a given circuit hop has completed.
22
    ///
23
    /// The `hop` number is a zero-indexed value for which hop just completed.
24
    ///
25
    /// The `delay` value is the amount of time after we first launched the
26
    /// circuit.
27
    ///
28
    /// If this is the last hop of the circuit, then `is_last` is true.
29
    fn note_hop_completed(&mut self, hop: u8, delay: Duration, is_last: bool);
30

            
31
    /// Record that a circuit failed to complete because it took too long.
32
    ///
33
    /// The `hop` number is a the number of hops that were successfully
34
    /// completed.
35
    ///
36
    /// The `delay` number is the amount of time after we first launched the
37
    /// circuit.
38
    fn note_circ_timeout(&mut self, hop: u8, delay: Duration);
39

            
40
    /// Return the current estimation for how long we should wait for a given
41
    /// [`Action`] to complete.
42
    ///
43
    /// This function should return a 2-tuple of `(timeout, abandon)`
44
    /// durations.  After `timeout` has elapsed since circuit launch,
45
    /// the circuit should no longer be used, but we should still keep
46
    /// building it in order see how long it takes.  After `abandon`
47
    /// has elapsed since circuit launch, the circuit should be
48
    /// abandoned completely.
49
    fn timeouts(&mut self, action: &Action) -> (Duration, Duration);
50

            
51
    /// Return true if we're currently trying to learn more timeouts
52
    /// by launching testing circuits.
53
    fn learning_timeouts(&self) -> bool;
54

            
55
    /// Replace the network parameters used by this estimator (if any)
56
    /// with ones derived from `params`.
57
    fn update_params(&mut self, params: &tor_netdir::params::NetParameters);
58

            
59
    /// Construct a new ParetoTimeoutState to represent the current state
60
    /// of this estimator, if it is possible to store the state to disk.
61
    ///
62
    /// TODO: change the type used for the state.
63
    fn build_state(&mut self) -> Option<pareto::ParetoTimeoutState>;
64
}
65

            
66
/// A possible action for which we can try to estimate a timeout.
67
#[non_exhaustive]
68
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
69
pub enum Action {
70
    /// Build a circuit of a given length.
71
    ///
72
    /// Note that you should not generally have to use
73
    /// this timeout outside of the circmgr code:
74
    /// The circmgr already computes and applies
75
    /// timeouts for creating and extending circuits.
76
    ///
77
    /// If you are using this timeout to estimate how long
78
    /// it will take somebody _else_ to build a circuit,
79
    /// remember that Tor clients will retry circuits when they fail,
80
    /// and so it may take longer than this for your peer to
81
    /// actually get a working circuit.
82
    BuildCircuit {
83
        /// The length of the circuit to construct.
84
        ///
85
        /// (A 0-hop circuit takes no time.)
86
        length: usize,
87
    },
88
    /// Extend a given circuit from one length to another.
89
    ExtendCircuit {
90
        /// The current length of the circuit.
91
        initial_length: usize,
92
        /// The new length of the circuit.
93
        ///
94
        /// (Should typically be greater than `initial_length`; otherwise we
95
        /// estimate a zero timeout.)
96
        final_length: usize,
97
    },
98
    /// Send a message to the last hop of a circuit and receive a response
99
    RoundTrip {
100
        /// The length of the circuit.
101
        length: usize,
102
    },
103
    /// Wait for a message to arrive to the other end of a circuit.
104
    ///
105
    /// (This is almost never the right operation to use unless you
106
    /// are doing something tricky.)
107
    OneWay {
108
        /// The length of the circuit.
109
        length: usize,
110
    },
111
}
112

            
113
impl Action {
114
    /// Compute a scaling factor for a given `Action`
115
    ///
116
    /// These values are arbitrary numbers such that if the correct
117
    /// timeout for an Action `a1` is `t`, then the correct timeout
118
    /// for an action `a2` is `t * a2.timeout_scale() /
119
    /// a1.timeout_scale()`.
120
    ///
121
    /// Callers should never rely on the actual values of this method:
122
    /// only on their ratios.
123
    ///
124
    /// This function can return garbage if the circuit length is larger
125
    /// than actually supported on the Tor network.
126
138
    fn timeout_scale(&self) -> usize {
127
        // Internally, we define "1" as the amount of time it takes a
128
        // message to transit from one hop to the next.
129
        // So sending a message to the end of a 3-hop circuit is "3",
130
        // and a round-trip with a 3-hop circuit is "6".
131

            
132
        /// An arbitrary value to use to prevent overflow.
133
        const MAX_LEN: usize = 64;
134

            
135
        /// An arbitrary value to use to prevent overflow.
136
        const MAX_ONEWAY_LEN: usize = MAX_LEN * 2;
137

            
138
        /// Return the scale value for building a `len`-hop circuit.
139
138
        fn build_scale(len: usize) -> usize {
140
138
            len * (len + 1) / 2
141
138
        }
142
        // This is based on an approximation from Tor's
143
        // `circuit_expire_building()` code.
144
        //
145
        // The general principle here is that when you're waiting for
146
        // a round-trip through a circuit through three relays
147
        // 'a--b--c', it takes three units of time.  Thus, building a
148
        // three hop circuit requires you to send a message through
149
        // "a", then through "a--b", then through "a--b--c", for a
150
        // total of 6.
151
        //
152
        // This is documented in path-spec.txt under "Calculating
153
        // timeouts thresholds for circuits of different lengths".
154
138
        match *self {
155
130
            Action::BuildCircuit { length } => {
156
                // We never down-scale our estimates for building a circuit
157
                // below a 3-hop length.
158
                //
159
                // TODO: This is undocumented.
160
130
                let length = length.clamp(3, MAX_LEN);
161
130
                build_scale(length) * 2
162
            }
163
            Action::ExtendCircuit {
164
4
                initial_length,
165
4
                final_length,
166
            } => {
167
4
                let initial_length = initial_length.clamp(0, MAX_LEN);
168
4
                let final_length = final_length.clamp(initial_length, MAX_LEN);
169
4
                (build_scale(final_length) - build_scale(initial_length)) * 2
170
            }
171
2
            Action::RoundTrip { length } => length.clamp(0, MAX_LEN) * 2,
172
2
            Action::OneWay { length } => length.clamp(0, MAX_ONEWAY_LEN),
173
        }
174
138
    }
175
}
176

            
177
/// A safe variant of [`Duration::mul_f64`] that never panics.
178
///
179
/// If the result would be outside the range of [`Duration`],
180
/// instead saturate to `0` or [`Duration::MAX`] as appropriate.
181
///
182
/// If the result would be NaN, we return an arbitrary duration.
183
90
fn mul_duration_f64_saturating(d: Duration, mul: f64) -> Duration {
184
    use std::cmp::Ordering;
185

            
186
90
    let secs = d.as_secs_f64() * mul;
187
90
    match Duration::try_from_secs_f64(secs) {
188
80
        Ok(product) => product,
189
        // TODO perhaps we should log a warning in this case.
190
        // Alternatively, we could make our timeout estimators fallible.
191
10
        Err(_) => match secs.partial_cmp(&0.0) {
192
4
            Some(Ordering::Greater) => Duration::MAX,
193
4
            Some(_) => Duration::new(0, 0),
194
            // The result is NaN, which means that the input `mul` was NaN.
195
            // We are allowed to return anything in this case, so we return
196
            // the original duration, on the theory that it is roughly
197
            // the right order of magnitude.
198
2
            None => d,
199
        },
200
    }
201
90
}
202

            
203
#[cfg(test)]
204
mod test {
205
    // @@ begin test lint list maintained by maint/add_warning @@
206
    #![allow(clippy::bool_assert_comparison)]
207
    #![allow(clippy::clone_on_copy)]
208
    #![allow(clippy::dbg_macro)]
209
    #![allow(clippy::mixed_attributes_style)]
210
    #![allow(clippy::print_stderr)]
211
    #![allow(clippy::print_stdout)]
212
    #![allow(clippy::single_char_pattern)]
213
    #![allow(clippy::unwrap_used)]
214
    #![allow(clippy::unchecked_time_subtraction)]
215
    #![allow(clippy::useless_vec)]
216
    #![allow(clippy::needless_pass_by_value)]
217
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
218
    use super::*;
219

            
220
    #[test]
221
    fn action_scale_values() {
222
        assert_eq!(Action::BuildCircuit { length: 1 }.timeout_scale(), 6 * 2);
223
        assert_eq!(Action::BuildCircuit { length: 2 }.timeout_scale(), 6 * 2);
224
        assert_eq!(Action::BuildCircuit { length: 3 }.timeout_scale(), 6 * 2);
225
        assert_eq!(Action::BuildCircuit { length: 4 }.timeout_scale(), 10 * 2);
226
        assert_eq!(Action::BuildCircuit { length: 5 }.timeout_scale(), 15 * 2);
227

            
228
        assert_eq!(
229
            Action::ExtendCircuit {
230
                initial_length: 3,
231
                final_length: 4
232
            }
233
            .timeout_scale(),
234
            4 * 2
235
        );
236
        assert_eq!(
237
            Action::ExtendCircuit {
238
                initial_length: 99,
239
                final_length: 4
240
            }
241
            .timeout_scale(),
242
            0
243
        );
244

            
245
        assert_eq!(Action::RoundTrip { length: 3 }.timeout_scale(), 3 * 2);
246
        assert_eq!(Action::OneWay { length: 3 }.timeout_scale(), 3);
247
    }
248

            
249
    #[test]
250
    fn test_mul_duration() {
251
        // This is wrong because of leap years, but we'll fake it.
252
        let mega_year = Duration::from_secs(86400 * 365 * 1000 * 1000);
253

            
254
        // Multiply by zero.
255
        let v = mul_duration_f64_saturating(mega_year, 0.0);
256
        assert!(v.is_zero());
257

            
258
        // Multiply by one.
259
        assert_eq!(mul_duration_f64_saturating(mega_year, 1.0), mega_year);
260

            
261
        // Divide by 1000.
262
        let v = mul_duration_f64_saturating(mega_year, 1.0 / 1000.0);
263
        let s = v.as_secs_f64();
264
        assert!((s - (mega_year.as_secs_f64() / 1000.0)).abs() < 0.1);
265

            
266
        // This would overflow if we were using mul_f64.
267
        let v = mul_duration_f64_saturating(mega_year, 1e9);
268
        assert!(v > mega_year * 1000);
269

            
270
        // This would underflow.
271
        let v = mul_duration_f64_saturating(mega_year, -1.0);
272
        assert_eq!(v, Duration::from_secs(0));
273

            
274
        // These are just silly.
275
        let v = mul_duration_f64_saturating(mega_year, f64::INFINITY);
276
        assert_eq!(v, Duration::MAX);
277
        let v = mul_duration_f64_saturating(mega_year, f64::NEG_INFINITY);
278
        assert_eq!(v, Duration::from_secs(0));
279
        let v = mul_duration_f64_saturating(mega_year, f64::NAN);
280
        assert_eq!(v, mega_year);
281
    }
282
}