1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
//! Tools for determining what circuits to preemptively build.

use crate::{PreemptiveCircuitConfig, TargetCircUsage, TargetPort};
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Instant;
use tracing::warn;

/// Predicts what circuits might be used in future based on past activity, and suggests
/// circuits to preemptively build as a result.
pub(crate) struct PreemptiveCircuitPredictor {
    /// A map of every exit port we've observed being used (or `None` if we observed an exit being
    /// used to resolve DNS names instead of building a stream), to the last time we encountered
    /// such usage.
    // TODO(nickm): Let's have a mechanism for cleaning this out from time to time.
    usages: HashMap<Option<TargetPort>, Instant>,

    /// Configuration for this predictor.
    config: tor_config::MutCfg<PreemptiveCircuitConfig>,
}

impl PreemptiveCircuitPredictor {
    /// Create a new predictor, starting out with a set of ports we think are likely to be used.
    pub(crate) fn new(config: PreemptiveCircuitConfig) -> Self {
        let mut usages = HashMap::new();
        for port in &config.initial_predicted_ports {
            // TODO(nickm) should this be IPv6? Should we have a way to configure IPv6 initial ports?
            usages.insert(Some(TargetPort::ipv4(*port)), Instant::now());
        }

        // We want to build circuits for resolving DNS, too.
        usages.insert(None, Instant::now());

        Self {
            usages,
            config: config.into(),
        }
    }

    /// Return the configuration for this PreemptiveCircuitPredictor.
    pub(crate) fn config(&self) -> Arc<PreemptiveCircuitConfig> {
        self.config.get()
    }

    /// Replace the current configuration for this PreemptiveCircuitPredictor
    /// with `new_config`.
    pub(crate) fn set_config(&self, mut new_config: PreemptiveCircuitConfig) {
        self.config.map_and_replace(|cfg| {
            // Force this to stay the same, since it can't meaningfully be changed.
            new_config.initial_predicted_ports = cfg.initial_predicted_ports.clone();
            new_config
        });
    }

    /// Make some predictions for what circuits should be built.
    pub(crate) fn predict(&self) -> Vec<TargetCircUsage> {
        let config = self.config();
        let now = Instant::now();
        let circs = config.min_exit_circs_for_port;
        self.usages
            .iter()
            .filter(|(_, &time)| {
                time.checked_add(config.prediction_lifetime)
                    .map(|t| t > now)
                    .unwrap_or_else(|| {
                        // FIXME(eta): this is going to be a bit noisy if it triggers, but that's better
                        //             than panicking or silently doing the wrong thing?
                        warn!("failed to represent preemptive circuit prediction lifetime as an Instant");
                        false
                    })
            })
            .map(|(&port, _)| TargetCircUsage::Preemptive { port, circs })
            .collect()
    }

    /// Note the use of a new port at the provided `time`.
    ///
    /// # Limitations
    ///
    /// This function assumes that the `time` values it receives are
    /// monotonically increasing.
    pub(crate) fn note_usage(&mut self, port: Option<TargetPort>, time: Instant) {
        self.usages.insert(port, time);
    }
}

#[cfg(test)]
mod test {
    #![allow(clippy::unwrap_used)]
    use crate::{PreemptiveCircuitConfig, PreemptiveCircuitPredictor, TargetCircUsage, TargetPort};
    use std::time::{Duration, Instant};

    use crate::isolation::test::{assert_isoleq, IsolationTokenEq};

    #[test]
    fn predicts_starting_ports() {
        let mut cfg = PreemptiveCircuitConfig::builder();
        cfg.set_initial_predicted_ports(vec![]);
        cfg.prediction_lifetime(Duration::from_secs(2));
        let predictor = PreemptiveCircuitPredictor::new(cfg.build().unwrap());

        assert_isoleq!(
            predictor.predict(),
            vec![TargetCircUsage::Preemptive {
                port: None,
                circs: 2
            }]
        );

        let mut cfg = PreemptiveCircuitConfig::builder();
        cfg.set_initial_predicted_ports(vec![80]);
        cfg.prediction_lifetime(Duration::from_secs(2));
        let predictor = PreemptiveCircuitPredictor::new(cfg.build().unwrap());

        let results = predictor.predict();
        assert_eq!(results.len(), 2);
        assert!(results
            .iter()
            .any(|r| r.isol_eq(&TargetCircUsage::Preemptive {
                port: None,
                circs: 2
            })));
        assert!(results
            .iter()
            .any(|r| r.isol_eq(&TargetCircUsage::Preemptive {
                port: Some(TargetPort::ipv4(80)),
                circs: 2
            })));
    }

    #[test]
    fn predicts_used_ports() {
        let mut cfg = PreemptiveCircuitConfig::builder();
        cfg.set_initial_predicted_ports(vec![]);
        cfg.prediction_lifetime(Duration::from_secs(2));
        let mut predictor = PreemptiveCircuitPredictor::new(cfg.build().unwrap());

        assert_isoleq!(
            predictor.predict(),
            vec![TargetCircUsage::Preemptive {
                port: None,
                circs: 2
            }]
        );

        predictor.note_usage(Some(TargetPort::ipv4(1234)), Instant::now());

        let results = predictor.predict();
        assert_eq!(results.len(), 2);
        assert!(results
            .iter()
            .any(|r| r.isol_eq(&TargetCircUsage::Preemptive {
                port: None,
                circs: 2
            })));
        assert!(results
            .iter()
            .any(|r| r.isol_eq(&TargetCircUsage::Preemptive {
                port: Some(TargetPort::ipv4(1234)),
                circs: 2
            })));
    }

    #[test]
    fn does_not_predict_old_ports() {
        let mut cfg = PreemptiveCircuitConfig::builder();
        cfg.set_initial_predicted_ports(vec![]);
        cfg.prediction_lifetime(Duration::from_secs(2));
        let mut predictor = PreemptiveCircuitPredictor::new(cfg.build().unwrap());
        let more_than_an_hour_ago = Instant::now() - Duration::from_secs(60 * 60 + 1);

        predictor.note_usage(Some(TargetPort::ipv4(2345)), more_than_an_hour_ago);

        assert_isoleq!(
            predictor.predict(),
            vec![TargetCircUsage::Preemptive {
                port: None,
                circs: 2
            }]
        );
    }
}