tor_circmgr/
isolation.rs

1//! Types related to stream isolation
2use downcast_rs::{Downcast, impl_downcast};
3use dyn_clone::{DynClone, clone_trait_object};
4use std::sync::atomic::{AtomicU64, Ordering};
5
6/// A type that can make isolation decisions about streams it is attached to.
7///
8/// Types that implement `Isolation` contain properties about a stream that are
9/// used to make decisions about whether that stream can share the same circuit
10/// as other streams. You may pass in any type implementing `Isolation` when
11/// creating a stream via `TorClient::connect_with_prefs`, or constructing a
12/// circuit with [`CircMgr::get_or_launch_exit()`](crate::CircMgr::get_or_launch_exit).
13///
14/// You typically do not want to implement this trait directly.  Instead, most
15/// users should implement [`IsolationHelper`].
16pub trait Isolation:
17    seal::Sealed + Downcast + DynClone + std::fmt::Debug + Send + Sync + 'static
18{
19    /// Return true if this Isolation is compatible with another.
20    ///
21    /// Two streams may share a circuit if and only if they have compatible
22    /// `Isolation`s.
23    ///
24    /// # Requirements
25    ///
26    /// For correctness, this relation must be symmetrical and reflexive:
27    /// `self.compatible(other)` must equal `other.compatible(self)`, and
28    /// `self.compatible(self)` must be true.
29    ///
30    /// For correctness, this function must always give the same result as
31    /// `self.join(other).is_some()`.
32    ///
33    /// This relationship does **not** have to be transitive: it's possible that
34    /// stream A can share a circuit with either stream B or stream C, but not
35    /// with both.
36    fn compatible(&self, other: &dyn Isolation) -> bool;
37
38    /// Join two [`Isolation`] into the intersection of what each allows.
39    ///
40    /// A circuit's isolation is the `join` of the isolation values of all of
41    /// the streams that have _ever_ used that circuit.  A circuit's isolation
42    /// can never be `None`: streams that would cause it to be `None` can't be
43    /// attached to the circuit.
44    ///
45    /// When a stream is added to a circuit, `join` is used to calculate the
46    /// circuit's new isolation.
47    ///
48    /// # Requirements
49    ///
50    /// For correctness, this function must be commutative: `self.join(other)`
51    /// must equal `other.join(self)`.  Also, it must be idempotent:
52    /// `self.join(self)` must equal self.
53    //
54    // TODO: (This function probably should be associative too, but we haven't done
55    // all the math.)
56    fn join(&self, other: &dyn Isolation) -> Option<Box<dyn Isolation>>;
57
58    /// Return true if this `Isolation` object should be considered sufficiently strong
59    /// as to enable long-lived circuits.
60    ///
61    /// By default, once a circuit has been in use for long enough,
62    /// it is considered no longer usable for new circuits.
63    /// But if the circuit's isolation is sufficiently strong
64    /// (and this method returns true)
65    /// then a circuit will keep being used for new streams indefinitely.
66    ///
67    /// The default implementation of this method returns false.
68    fn enables_long_lived_circuits(&self) -> bool {
69        false
70    }
71}
72
73/// Seal preventing implementation of Isolation not relying on IsolationHelper
74mod seal {
75    /// Seal preventing implementation of Isolation not relying on IsolationHelper
76    pub trait Sealed {}
77    impl<T: super::IsolationHelper> Sealed for T {}
78}
79
80impl_downcast!(Isolation);
81clone_trait_object!(Isolation);
82impl<T: Isolation> From<T> for Box<dyn Isolation> {
83    fn from(isolation: T) -> Self {
84        Box::new(isolation)
85    }
86}
87
88impl<T: IsolationHelper + Clone + std::fmt::Debug + Send + Sync + 'static> Isolation for T {
89    fn compatible(&self, other: &dyn Isolation) -> bool {
90        if let Some(other) = other.as_any().downcast_ref() {
91            self.compatible_same_type(other)
92        } else {
93            false
94        }
95    }
96
97    fn join(&self, other: &dyn Isolation) -> Option<Box<dyn Isolation>> {
98        if let Some(other) = other.as_any().downcast_ref() {
99            self.join_same_type(other)
100                .map(|res| Box::new(res) as Box<dyn Isolation>)
101        } else {
102            None
103        }
104    }
105
106    fn enables_long_lived_circuits(&self) -> bool {
107        IsolationHelper::enables_long_lived_circuits(self)
108    }
109}
110
111/// Trait to help implement [`Isolation`].
112///
113/// You should generally implement this trait whenever you need to implement a
114/// new set of stream isolation rules: it takes care of down-casting and type
115/// checking for you.
116///
117/// When you implement this trait for some type T, isolation objects of that
118/// type will be incompatible (unable to share circuits) with objects of _any
119/// other type_.  (That's usually what you want; if you're defining a new type
120/// of Isolation rules, then you probably don't want streams using different
121/// rules to share circuits with yours.)
122pub trait IsolationHelper: Sized {
123    /// Returns whether self and other are compatible.
124    ///
125    /// Two streams may share a circuit if and only if they have compatible
126    /// `Isolation`s.
127    ///
128    /// (See [`Isolation::compatible`] for more information and requirements.)
129    fn compatible_same_type(&self, other: &Self) -> bool;
130
131    /// Join self and other into the intersection of what they allows.
132    ///
133    /// (See [`Isolation::join`] for more information and requirements.)
134    fn join_same_type(&self, other: &Self) -> Option<Self>;
135
136    /// Return true if this `Isolation` object should be considered sufficiently strong
137    /// as to permit long-lived circuits.
138    ///
139    /// (See [`Isolation::enables_long_lived_circuits`] for more information.)
140    fn enables_long_lived_circuits(&self) -> bool {
141        false
142    }
143}
144
145/// A token used to isolate unrelated streams on different circuits.
146///
147/// When two streams are associated with different isolation tokens, they
148/// can never share the same circuit.
149///
150/// Tokens created with [`IsolationToken::new`] are all different from
151/// one another, and different from tokens created with
152/// [`IsolationToken::no_isolation`]. However, tokens created with
153/// [`IsolationToken::no_isolation`] are all equal to one another.
154///
155/// # Examples
156///
157/// Creating distinct isolation tokens:
158///
159/// ```rust
160/// # use tor_circmgr::IsolationToken;
161/// let token_1 = IsolationToken::new();
162/// let token_2 = IsolationToken::new();
163///
164/// assert_ne!(token_1, token_2);
165///
166/// // Demonstrating the behaviour of no_isolation() tokens:
167/// assert_ne!(token_1, IsolationToken::no_isolation());
168/// assert_eq!(IsolationToken::no_isolation(), IsolationToken::no_isolation());
169/// ```
170///
171/// Using an isolation token to route streams differently over the Tor network:
172///
173/// ```ignore
174/// use arti_client::StreamPrefs;
175///
176/// let token_1 = IsolationToken::new();
177/// let token_2 = IsolationToken::new();
178///
179/// let mut prefs_1 = StreamPrefs::new();
180/// prefs_1.set_isolation(token_1);
181///
182/// let mut prefs_2 = StreamPrefs::new();
183/// prefs_2.set_isolation(token_2);
184///
185/// // These two connections will come from different source IP addresses.
186/// tor_client.connect(("example.com", 80), Some(prefs_1)).await?;
187/// tor_client.connect(("example.com", 80), Some(prefs_2)).await?;
188/// ```
189// # Semver note
190//
191// This type is re-exported by `arti-client`: any changes to it must be
192// reflected in `arti-client`'s version.
193#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
194pub struct IsolationToken(u64);
195
196#[allow(clippy::new_without_default)]
197impl IsolationToken {
198    /// Create a new IsolationToken, unequal to any other token this function
199    /// has created.
200    ///
201    /// # Panics
202    ///
203    /// Panics if we have already allocated 2^64 isolation tokens: in that
204    /// case, we have exhausted the space of possible tokens, and it is
205    /// no longer possible to ensure isolation.
206    pub fn new() -> Self {
207        /// Internal counter used to generate different tokens each time
208        static COUNTER: AtomicU64 = AtomicU64::new(1);
209        // Ordering::Relaxed is fine because we don't care about causality, we just want a
210        // different number each time
211        let token = COUNTER.fetch_add(1, Ordering::Relaxed);
212        assert!(token < u64::MAX);
213        IsolationToken(token)
214    }
215
216    /// Create a new IsolationToken equal to every other token created
217    /// with this function, but different from all tokens created with
218    /// `new`.
219    ///
220    /// This can be used when no isolation is wanted for some streams.
221    pub fn no_isolation() -> Self {
222        IsolationToken(0)
223    }
224}
225
226impl IsolationHelper for IsolationToken {
227    fn compatible_same_type(&self, other: &Self) -> bool {
228        self == other
229    }
230    fn join_same_type(&self, other: &Self) -> Option<Self> {
231        if self.compatible_same_type(other) {
232            Some(*self)
233        } else {
234            None
235        }
236    }
237
238    fn enables_long_lived_circuits(&self) -> bool {
239        false
240    }
241}
242
243/// Helper macro to implement IsolationHelper for tuple of IsolationHelper
244macro_rules! tuple_impls {
245    ($(
246        $Tuple:ident {
247            $(($idx:tt) -> $T:ident)+
248        }
249    )+) => {
250        $(
251            impl<$($T:IsolationHelper),+> IsolationHelper for ($($T,)+) {
252                fn compatible_same_type(&self, other: &Self) -> bool {
253                    $(self.$idx.compatible_same_type(&other.$idx))&&+
254                }
255
256                fn join_same_type(&self, other: &Self) -> Option<Self> {
257                    Some((
258                    $(self.$idx.join_same_type(&other.$idx)?,)+
259                    ))
260                }
261
262                fn enables_long_lived_circuits(&self) -> bool {
263                    $(self.$idx.enables_long_lived_circuits() || )+ false
264                }
265            }
266        )+
267    }
268}
269
270tuple_impls! {
271    Tuple1 {
272        (0) -> A
273    }
274    Tuple2 {
275        (0) -> A
276        (1) -> B
277    }
278    Tuple3 {
279        (0) -> A
280        (1) -> B
281        (2) -> C
282    }
283    Tuple4 {
284        (0) -> A
285        (1) -> B
286        (2) -> C
287        (3) -> D
288    }
289    Tuple5 {
290        (0) -> A
291        (1) -> B
292        (2) -> C
293        (3) -> D
294        (4) -> E
295    }
296    Tuple6 {
297        (0) -> A
298        (1) -> B
299        (2) -> C
300        (3) -> D
301        (4) -> E
302        (5) -> F
303    }
304    Tuple7 {
305        (0) -> A
306        (1) -> B
307        (2) -> C
308        (3) -> D
309        (4) -> E
310        (5) -> F
311        (6) -> G
312    }
313    Tuple8 {
314        (0) -> A
315        (1) -> B
316        (2) -> C
317        (3) -> D
318        (4) -> E
319        (5) -> F
320        (6) -> G
321        (7) -> H
322    }
323    Tuple9 {
324        (0) -> A
325        (1) -> B
326        (2) -> C
327        (3) -> D
328        (4) -> E
329        (5) -> F
330        (6) -> G
331        (7) -> H
332        (8) -> I
333    }
334    Tuple10 {
335        (0) -> A
336        (1) -> B
337        (2) -> C
338        (3) -> D
339        (4) -> E
340        (5) -> F
341        (6) -> G
342        (7) -> H
343        (8) -> I
344        (9) -> J
345    }
346    Tuple11 {
347        (0) -> A
348        (1) -> B
349        (2) -> C
350        (3) -> D
351        (4) -> E
352        (5) -> F
353        (6) -> G
354        (7) -> H
355        (8) -> I
356        (9) -> J
357        (10) -> K
358    }
359    Tuple12 {
360        (0) -> A
361        (1) -> B
362        (2) -> C
363        (3) -> D
364        (4) -> E
365        (5) -> F
366        (6) -> G
367        (7) -> H
368        (8) -> I
369        (9) -> J
370        (10) -> K
371        (11) -> L
372    }
373}
374
375/// A set of information about how a stream should be isolated.
376///
377/// If two streams are isolated from one another, they may not share
378/// a circuit.
379#[derive(Clone, Debug, derive_builder::Builder)]
380pub struct StreamIsolation {
381    /// Any isolation set on the stream.
382    #[builder(default = "Box::new(IsolationToken::no_isolation())")]
383    stream_isolation: Box<dyn Isolation>,
384    /// Any additional isolation token set on an object that "owns" this
385    /// stream.  This is typically owned by a `TorClient`.
386    #[builder(default = "IsolationToken::no_isolation()")]
387    owner_token: IsolationToken,
388}
389
390impl StreamIsolation {
391    /// Construct a new StreamIsolation with no isolation enabled.
392    pub fn no_isolation() -> Self {
393        StreamIsolationBuilder::new()
394            .build()
395            .expect("Bug constructing StreamIsolation")
396    }
397
398    /// Return a new StreamIsolationBuilder for constructing
399    /// StreamIsolation objects.
400    pub fn builder() -> StreamIsolationBuilder {
401        StreamIsolationBuilder::new()
402    }
403}
404
405impl IsolationHelper for StreamIsolation {
406    fn compatible_same_type(&self, other: &StreamIsolation) -> bool {
407        self.owner_token == other.owner_token
408            && self
409                .stream_isolation
410                .compatible(other.stream_isolation.as_ref())
411    }
412
413    fn join_same_type(&self, other: &StreamIsolation) -> Option<StreamIsolation> {
414        if self.owner_token != other.owner_token {
415            return None;
416        }
417        self.stream_isolation
418            .join(other.stream_isolation.as_ref())
419            .map(|stream_isolation| StreamIsolation {
420                stream_isolation,
421                owner_token: self.owner_token,
422            })
423    }
424
425    fn enables_long_lived_circuits(&self) -> bool {
426        self.stream_isolation.enables_long_lived_circuits()
427    }
428}
429
430impl StreamIsolationBuilder {
431    /// Construct a builder with no items set.
432    pub fn new() -> Self {
433        StreamIsolationBuilder::default()
434    }
435}
436
437#[cfg(test)]
438pub(crate) mod test {
439    #![allow(clippy::unwrap_used)]
440    use super::*;
441
442    /// Trait for testing use only. Much like PartialEq, but for type containing an dyn Isolation
443    /// which is known to be an IsolationToken.
444    pub(crate) trait IsolationTokenEq {
445        /// Compare two values, returning true if they are equals and all dyn Isolation they contain
446        /// are IsolationToken (which are equal too).
447        fn isol_eq(&self, other: &Self) -> bool;
448    }
449
450    macro_rules! assert_isoleq {
451        { $arg1:expr, $arg2:expr } => {
452            assert!($arg1.isol_eq(&$arg2))
453        }
454    }
455    pub(crate) use assert_isoleq;
456
457    impl IsolationTokenEq for IsolationToken {
458        fn isol_eq(&self, other: &Self) -> bool {
459            self == other
460        }
461    }
462
463    impl<T: IsolationTokenEq> IsolationTokenEq for Option<T> {
464        fn isol_eq(&self, other: &Self) -> bool {
465            match (self, other) {
466                (Some(this), Some(other)) => this.isol_eq(other),
467                (None, None) => true,
468                _ => false,
469            }
470        }
471    }
472
473    impl<T: IsolationTokenEq + std::fmt::Debug> IsolationTokenEq for Vec<T> {
474        fn isol_eq(&self, other: &Self) -> bool {
475            if self.len() != other.len() {
476                return false;
477            }
478            self.iter()
479                .zip(other.iter())
480                .all(|(this, other)| this.isol_eq(other))
481        }
482    }
483
484    impl IsolationTokenEq for dyn Isolation {
485        fn isol_eq(&self, other: &Self) -> bool {
486            let this = self.as_any().downcast_ref::<IsolationToken>();
487            let other = other.as_any().downcast_ref::<IsolationToken>();
488            match (this, other) {
489                (Some(this), Some(other)) => this == other,
490                _ => false,
491            }
492        }
493    }
494
495    impl IsolationTokenEq for StreamIsolation {
496        fn isol_eq(&self, other: &Self) -> bool {
497            self.stream_isolation
498                .isol_eq(other.stream_isolation.as_ref())
499                && self.owner_token == other.owner_token
500        }
501    }
502
503    #[derive(PartialEq, Clone, Copy, Debug, Eq)]
504    struct OtherIsolation(usize);
505
506    impl IsolationHelper for OtherIsolation {
507        fn compatible_same_type(&self, other: &Self) -> bool {
508            self == other
509        }
510        fn join_same_type(&self, other: &Self) -> Option<Self> {
511            if self.compatible_same_type(other) {
512                Some(*self)
513            } else {
514                None
515            }
516        }
517    }
518
519    #[test]
520    fn isolation_token() {
521        let token_1 = IsolationToken::new();
522        let token_2 = IsolationToken::new();
523
524        assert!(token_1.compatible_same_type(&token_1));
525        assert!(token_2.compatible_same_type(&token_2));
526        assert!(!token_1.compatible_same_type(&token_2));
527
528        assert_eq!(token_1.join_same_type(&token_1), Some(token_1));
529        assert_eq!(token_2.join_same_type(&token_2), Some(token_2));
530        assert_eq!(token_1.join_same_type(&token_2), None);
531    }
532
533    #[test]
534    fn isolation_trait() {
535        let token_1: Box<dyn Isolation> = Box::new(IsolationToken::new());
536        let token_2: Box<dyn Isolation> = Box::new(IsolationToken::new());
537        let other_1: Box<dyn Isolation> = Box::new(OtherIsolation(0));
538        let other_2: Box<dyn Isolation> = Box::new(OtherIsolation(1));
539
540        assert!(token_1.compatible(token_1.as_ref()));
541        assert!(token_2.compatible(token_2.as_ref()));
542        assert!(!token_1.compatible(token_2.as_ref()));
543
544        assert!(other_1.compatible(other_1.as_ref()));
545        assert!(other_2.compatible(other_2.as_ref()));
546        assert!(!other_1.compatible(other_2.as_ref()));
547
548        assert!(!token_1.compatible(other_1.as_ref()));
549        assert!(!other_1.compatible(token_1.as_ref()));
550
551        assert!(token_1.join(token_1.as_ref()).is_some());
552        assert!(token_1.join(token_2.as_ref()).is_none());
553
554        assert!(other_1.join(other_1.as_ref()).is_some());
555        assert!(other_1.join(other_2.as_ref()).is_none());
556
557        assert!(token_1.join(other_1.as_ref()).is_none());
558        assert!(other_1.join(token_1.as_ref()).is_none());
559    }
560
561    #[test]
562    fn isolation_tuple() {
563        let token_1 = IsolationToken::new();
564        let token_2 = IsolationToken::new();
565        let other_1 = OtherIsolation(0);
566        let other_2 = OtherIsolation(1);
567
568        let token_12: Box<dyn Isolation> = Box::new((token_1, token_2));
569        let token_21: Box<dyn Isolation> = Box::new((token_2, token_1));
570        let mix_11: Box<dyn Isolation> = Box::new((token_1, other_1));
571        let mix_12: Box<dyn Isolation> = Box::new((token_1, other_2));
572        let revmix_11: Box<dyn Isolation> = Box::new((other_1, token_1));
573
574        let join_token = token_12.join(token_12.as_ref()).unwrap();
575        assert!(join_token.compatible(token_12.as_ref()));
576        let join_mix = mix_12.join(mix_12.as_ref()).unwrap();
577        assert!(join_mix.compatible(mix_12.as_ref()));
578
579        let isol_list = [token_12, token_21, mix_11, mix_12, revmix_11];
580
581        for (i, isol1) in isol_list.iter().enumerate() {
582            for (j, isol2) in isol_list.iter().enumerate() {
583                assert_eq!(isol1.compatible(isol2.as_ref()), i == j);
584            }
585        }
586    }
587
588    #[test]
589    fn build_isolation() {
590        let no_isolation = StreamIsolation::no_isolation();
591        let no_isolation2 = StreamIsolation::builder()
592            .owner_token(IsolationToken::no_isolation())
593            .stream_isolation(Box::new(IsolationToken::no_isolation()))
594            .build()
595            .unwrap();
596        assert_eq!(no_isolation.owner_token, no_isolation2.owner_token);
597        assert_eq!(
598            no_isolation
599                .stream_isolation
600                .as_ref()
601                .as_any()
602                .downcast_ref::<IsolationToken>(),
603            no_isolation2
604                .stream_isolation
605                .as_ref()
606                .as_any()
607                .downcast_ref::<IsolationToken>()
608        );
609        assert!(no_isolation.compatible(&no_isolation2));
610
611        let tok = IsolationToken::new();
612        let some_isolation = StreamIsolation::builder().owner_token(tok).build().unwrap();
613        let some_isolation2 = StreamIsolation::builder()
614            .stream_isolation(Box::new(tok))
615            .build()
616            .unwrap();
617        assert!(!no_isolation.compatible(&some_isolation));
618        assert!(!no_isolation.compatible(&some_isolation2));
619        assert!(!some_isolation.compatible(&some_isolation2));
620        assert!(some_isolation.compatible(&some_isolation));
621    }
622}