tor_circmgr/timeouts.rs
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
10use std::time::Duration;
11
12pub(crate) mod estimator;
13pub(crate) mod pareto;
14pub(crate) mod readonly;
15
16pub(crate) use estimator::Estimator;
17
18/// An object that calculates circuit timeout thresholds from the history
19/// of circuit build times.
20pub(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)]
69pub 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
113impl 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 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 fn build_scale(len: usize) -> usize {
140 len * (len + 1) / 2
141 }
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 match *self {
155 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 let length = length.clamp(3, MAX_LEN);
161 build_scale(length) * 2
162 }
163 Action::ExtendCircuit {
164 initial_length,
165 final_length,
166 } => {
167 let initial_length = initial_length.clamp(0, MAX_LEN);
168 let final_length = final_length.clamp(initial_length, MAX_LEN);
169 (build_scale(final_length) - build_scale(initial_length)) * 2
170 }
171 Action::RoundTrip { length } => length.clamp(0, MAX_LEN) * 2,
172 Action::OneWay { length } => length.clamp(0, MAX_ONEWAY_LEN),
173 }
174 }
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.
183fn mul_duration_f64_saturating(d: Duration, mul: f64) -> Duration {
184 use std::cmp::Ordering;
185
186 let secs = d.as_secs_f64() * mul;
187 match Duration::try_from_secs_f64(secs) {
188 Ok(product) => product,
189 // TODO perhaps we should log a warning in this case.
190 // Alternatively, we could make our timeout estimators fallible.
191 Err(_) => match secs.partial_cmp(&0.0) {
192 Some(Ordering::Greater) => Duration::MAX,
193 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 None => d,
199 },
200 }
201}
202
203#[cfg(test)]
204mod 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}