//! Types related to stream isolation use downcast_rs::{impl_downcast, Downcast}; use dyn_clone::{clone_trait_object, DynClone}; use std::sync::atomic::{AtomicU64, Ordering}; /// A type that can make isolation decisions about streams it is attached to. /// /// Types that implement `Isolation` contain properties about a stream that are /// used to make decisions about whether that stream can share the same circuit /// as other streams. You may pass in any type implementing `Isolation` when /// creating a stream via `TorClient::connect_with_prefs`, or constructing a /// circuit with [`CircMgr::get_or_launch_exit()`](crate::CircMgr::get_or_launch_exit). /// /// You typically do not want to implement this trait directly. Instead, most /// users should implement [`IsolationHelper`]. pub trait Isolation: seal::Sealed + Downcast + DynClone + std::fmt::Debug + Send + Sync + 'static { /// Return true if this Isolation is compatible with another. /// /// Two streams may share a circuit if and only if they have compatible /// `Isolation`s. /// /// # Requirements /// /// For correctness, this relation must be symmetrical and reflexive: /// `self.compatible(other)` must equal `other.compatible(self)`, and /// `self.compatible(self)` must be true. /// /// For correctness, this function must always give the same result as /// `self.join(other).is_some()`. /// /// This relationship does **not** have to be transitive: it's possible that /// stream A can share a circuit with either stream B or stream C, but not /// with both. fn compatible(&self, other: &dyn Isolation) -> bool; /// Join two [`Isolation`] into the intersection of what each allows. /// /// A circuit's isolation is the `join` of the isolation values of all of /// the streams that have _ever_ used that circuit. A circuit's isolation /// can never be `None`: streams that would cause it to be `None` can't be /// attached to the circuit. /// /// When a stream is added to a circuit, `join` is used to calculate the /// circuit's new isolation. /// /// # Requirements /// /// For correctness, this function must be commutative: `self.join(other)` /// must equal `other.join(self)`. Also, it must be idempotent: /// `self.join(self)` must equal self. // // TODO: (This function probably should be associative too, but we haven't done // all the math.) fn join(&self, other: &dyn Isolation) -> Option>; } /// Seal preventing implementation of Isolation not relying on IsolationHelper mod seal { /// Seal preventing implementation of Isolation not relying on IsolationHelper pub trait Sealed {} impl Sealed for T {} } impl_downcast!(Isolation); clone_trait_object!(Isolation); impl From for Box { fn from(isolation: T) -> Self { Box::new(isolation) } } impl Isolation for T { fn compatible(&self, other: &dyn Isolation) -> bool { if let Some(other) = other.as_any().downcast_ref() { self.compatible_same_type(other) } else { false } } fn join(&self, other: &dyn Isolation) -> Option> { if let Some(other) = other.as_any().downcast_ref() { self.join_same_type(other) .map(|res| Box::new(res) as Box) } else { None } } } /// Trait to help implement [`Isolation`]. /// /// You should generally implement this trait whenever you need to implement a /// new set of stream isolation rules: it takes care of down-casting and type /// checking for you. /// /// When you implement this trait for some type T, isolation objects of that /// type will be incompatible (unable to share circuits) with objects of _any /// other type_. (That's usually what you want; if you're defining a new type /// of Isolation rules, then you probably don't want streams using different /// rules to share circuits with yours.) pub trait IsolationHelper: Sized { /// Returns whether self and other are compatible. /// /// Two streams may share a circuit if and only if they have compatible /// `Isolation`s. /// /// (See [`Isolation::compatible`] for more information and requirements.) fn compatible_same_type(&self, other: &Self) -> bool; /// Join self and other into the intersection of what they allows. /// /// (See [`Isolation::join`] for more information and requirements.) fn join_same_type(&self, other: &Self) -> Option; } /// A token used to isolate unrelated streams on different circuits. /// /// When two streams are associated with different isolation tokens, they /// can never share the same circuit. /// /// Tokens created with [`IsolationToken::new`] are all different from /// one another, and different from tokens created with /// [`IsolationToken::no_isolation`]. However, tokens created with /// [`IsolationToken::no_isolation`] are all equal to one another. /// /// # Examples /// /// Creating distinct isolation tokens: /// /// ```rust /// # use tor_circmgr::IsolationToken; /// let token_1 = IsolationToken::new(); /// let token_2 = IsolationToken::new(); /// /// assert_ne!(token_1, token_2); /// /// // Demonstrating the behaviour of no_isolation() tokens: /// assert_ne!(token_1, IsolationToken::no_isolation()); /// assert_eq!(IsolationToken::no_isolation(), IsolationToken::no_isolation()); /// ``` /// /// Using an isolation token to route streams differently over the Tor network: /// /// ```ignore /// use arti_client::StreamPrefs; /// /// let token_1 = IsolationToken::new(); /// let token_2 = IsolationToken::new(); /// /// let mut prefs_1 = StreamPrefs::new(); /// prefs_1.set_isolation(token_1); /// /// let mut prefs_2 = StreamPrefs::new(); /// prefs_2.set_isolation(token_2); /// /// // These two connections will come from different source IP addresses. /// tor_client.connect(("example.com", 80), Some(prefs_1)).await?; /// tor_client.connect(("example.com", 80), Some(prefs_2)).await?; /// ``` // # Semver note // // This type is re-exported by `arti-client`: any changes to it must be // reflected in `arti-client`'s version. #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct IsolationToken(u64); #[allow(clippy::new_without_default)] impl IsolationToken { /// Create a new IsolationToken, unequal to any other token this function /// has created. /// /// # Panics /// /// Panics if we have already allocated 2^64 isolation tokens: in that /// case, we have exhausted the space of possible tokens, and it is /// no longer possible to ensure isolation. pub fn new() -> Self { /// Internal counter used to generate different tokens each time static COUNTER: AtomicU64 = AtomicU64::new(1); // Ordering::Relaxed is fine because we don't care about causality, we just want a // different number each time let token = COUNTER.fetch_add(1, Ordering::Relaxed); assert!(token < u64::MAX); IsolationToken(token) } /// Create a new IsolationToken equal to every other token created /// with this function, but different from all tokens created with /// `new`. /// /// This can be used when no isolation is wanted for some streams. pub fn no_isolation() -> Self { IsolationToken(0) } } impl IsolationHelper for IsolationToken { fn compatible_same_type(&self, other: &Self) -> bool { self == other } fn join_same_type(&self, other: &Self) -> Option { if self.compatible_same_type(other) { Some(*self) } else { None } } } /// Helper macro to implement IsolationHelper for tuple of IsolationHelper macro_rules! tuple_impls { ($( $Tuple:ident { $(($idx:tt) -> $T:ident)+ } )+) => { $( impl<$($T:IsolationHelper),+> IsolationHelper for ($($T,)+) { fn compatible_same_type(&self, other: &Self) -> bool { $(self.$idx.compatible_same_type(&other.$idx))&&+ } fn join_same_type(&self, other: &Self) -> Option { Some(( $(self.$idx.join_same_type(&other.$idx)?,)+ )) } } )+ } } tuple_impls! { Tuple1 { (0) -> A } Tuple2 { (0) -> A (1) -> B } Tuple3 { (0) -> A (1) -> B (2) -> C } Tuple4 { (0) -> A (1) -> B (2) -> C (3) -> D } Tuple5 { (0) -> A (1) -> B (2) -> C (3) -> D (4) -> E } Tuple6 { (0) -> A (1) -> B (2) -> C (3) -> D (4) -> E (5) -> F } Tuple7 { (0) -> A (1) -> B (2) -> C (3) -> D (4) -> E (5) -> F (6) -> G } Tuple8 { (0) -> A (1) -> B (2) -> C (3) -> D (4) -> E (5) -> F (6) -> G (7) -> H } Tuple9 { (0) -> A (1) -> B (2) -> C (3) -> D (4) -> E (5) -> F (6) -> G (7) -> H (8) -> I } Tuple10 { (0) -> A (1) -> B (2) -> C (3) -> D (4) -> E (5) -> F (6) -> G (7) -> H (8) -> I (9) -> J } Tuple11 { (0) -> A (1) -> B (2) -> C (3) -> D (4) -> E (5) -> F (6) -> G (7) -> H (8) -> I (9) -> J (10) -> K } Tuple12 { (0) -> A (1) -> B (2) -> C (3) -> D (4) -> E (5) -> F (6) -> G (7) -> H (8) -> I (9) -> J (10) -> K (11) -> L } } /// A set of information about how a stream should be isolated. /// /// If two streams are isolated from one another, they may not share /// a circuit. #[derive(Clone, Debug, derive_builder::Builder)] pub struct StreamIsolation { /// Any isolation set on the stream. #[builder(default = "Box::new(IsolationToken::no_isolation())")] stream_isolation: Box, /// Any additional isolation token set on an object that "owns" this /// stream. This is typically owned by a `TorClient`. #[builder(default = "IsolationToken::no_isolation()")] owner_token: IsolationToken, } impl StreamIsolation { /// Construct a new StreamIsolation with no isolation enabled. pub fn no_isolation() -> Self { StreamIsolationBuilder::new() .build() .expect("Bug constructing StreamIsolation") } /// Return a new StreamIsolationBuilder for constructing /// StreamIsolation objects. pub fn builder() -> StreamIsolationBuilder { StreamIsolationBuilder::new() } } impl IsolationHelper for StreamIsolation { fn compatible_same_type(&self, other: &StreamIsolation) -> bool { self.owner_token == other.owner_token && self .stream_isolation .compatible(other.stream_isolation.as_ref()) } fn join_same_type(&self, other: &StreamIsolation) -> Option { if self.owner_token != other.owner_token { return None; } self.stream_isolation .join(other.stream_isolation.as_ref()) .map(|stream_isolation| StreamIsolation { stream_isolation, owner_token: self.owner_token, }) } } impl StreamIsolationBuilder { /// Construct a builder with no items set. pub fn new() -> Self { StreamIsolationBuilder::default() } } #[cfg(test)] pub(crate) mod test { #![allow(clippy::unwrap_used)] use super::*; /// Trait for testing use only. Much like PartialEq, but for type containing an dyn Isolation /// which is known to be an IsolationToken. pub(crate) trait IsolationTokenEq { /// Compare two values, returning true if they are equals and all dyn Isolation they contain /// are IsolationToken (which are equal too). fn isol_eq(&self, other: &Self) -> bool; } macro_rules! assert_isoleq { { $arg1:expr, $arg2:expr } => { assert!($arg1.isol_eq(&$arg2)) } } pub(crate) use assert_isoleq; impl IsolationTokenEq for IsolationToken { fn isol_eq(&self, other: &Self) -> bool { self == other } } impl IsolationTokenEq for Option { fn isol_eq(&self, other: &Self) -> bool { match (self, other) { (Some(this), Some(other)) => this.isol_eq(other), (None, None) => true, _ => false, } } } impl IsolationTokenEq for Vec { fn isol_eq(&self, other: &Self) -> bool { if self.len() != other.len() { return false; } self.iter() .zip(other.iter()) .all(|(this, other)| this.isol_eq(other)) } } impl IsolationTokenEq for dyn Isolation { fn isol_eq(&self, other: &Self) -> bool { let this = self.as_any().downcast_ref::(); let other = other.as_any().downcast_ref::(); match (this, other) { (Some(this), Some(other)) => this == other, _ => false, } } } impl IsolationTokenEq for StreamIsolation { fn isol_eq(&self, other: &Self) -> bool { self.stream_isolation .isol_eq(other.stream_isolation.as_ref()) && self.owner_token == other.owner_token } } #[derive(PartialEq, Clone, Copy, Debug, Eq)] struct OtherIsolation(usize); impl IsolationHelper for OtherIsolation { fn compatible_same_type(&self, other: &Self) -> bool { self == other } fn join_same_type(&self, other: &Self) -> Option { if self.compatible_same_type(other) { Some(*self) } else { None } } } #[test] fn isolation_token() { let token_1 = IsolationToken::new(); let token_2 = IsolationToken::new(); assert!(token_1.compatible_same_type(&token_1)); assert!(token_2.compatible_same_type(&token_2)); assert!(!token_1.compatible_same_type(&token_2)); assert_eq!(token_1.join_same_type(&token_1), Some(token_1)); assert_eq!(token_2.join_same_type(&token_2), Some(token_2)); assert_eq!(token_1.join_same_type(&token_2), None); } #[test] fn isolation_trait() { let token_1: Box = Box::new(IsolationToken::new()); let token_2: Box = Box::new(IsolationToken::new()); let other_1: Box = Box::new(OtherIsolation(0)); let other_2: Box = Box::new(OtherIsolation(1)); assert!(token_1.compatible(token_1.as_ref())); assert!(token_2.compatible(token_2.as_ref())); assert!(!token_1.compatible(token_2.as_ref())); assert!(other_1.compatible(other_1.as_ref())); assert!(other_2.compatible(other_2.as_ref())); assert!(!other_1.compatible(other_2.as_ref())); assert!(!token_1.compatible(other_1.as_ref())); assert!(!other_1.compatible(token_1.as_ref())); assert!(token_1.join(token_1.as_ref()).is_some()); assert!(token_1.join(token_2.as_ref()).is_none()); assert!(other_1.join(other_1.as_ref()).is_some()); assert!(other_1.join(other_2.as_ref()).is_none()); assert!(token_1.join(other_1.as_ref()).is_none()); assert!(other_1.join(token_1.as_ref()).is_none()); } #[test] fn isolation_tuple() { let token_1 = IsolationToken::new(); let token_2 = IsolationToken::new(); let other_1 = OtherIsolation(0); let other_2 = OtherIsolation(1); let token_12: Box = Box::new((token_1, token_2)); let token_21: Box = Box::new((token_2, token_1)); let mix_11: Box = Box::new((token_1, other_1)); let mix_12: Box = Box::new((token_1, other_2)); let revmix_11: Box = Box::new((other_1, token_1)); let join_token = token_12.join(token_12.as_ref()).unwrap(); assert!(join_token.compatible(token_12.as_ref())); let join_mix = mix_12.join(mix_12.as_ref()).unwrap(); assert!(join_mix.compatible(mix_12.as_ref())); let isol_list = [token_12, token_21, mix_11, mix_12, revmix_11]; for (i, isol1) in isol_list.iter().enumerate() { for (j, isol2) in isol_list.iter().enumerate() { assert_eq!(isol1.compatible(isol2.as_ref()), i == j); } } } #[test] fn build_isolation() { let no_isolation = StreamIsolation::no_isolation(); let no_isolation2 = StreamIsolation::builder() .owner_token(IsolationToken::no_isolation()) .stream_isolation(Box::new(IsolationToken::no_isolation())) .build() .unwrap(); assert_eq!(no_isolation.owner_token, no_isolation2.owner_token); assert_eq!( no_isolation .stream_isolation .as_ref() .as_any() .downcast_ref::(), no_isolation2 .stream_isolation .as_ref() .as_any() .downcast_ref::() ); assert!(no_isolation.compatible(&no_isolation2)); let tok = IsolationToken::new(); let some_isolation = StreamIsolation::builder().owner_token(tok).build().unwrap(); let some_isolation2 = StreamIsolation::builder() .stream_isolation(Box::new(tok)) .build() .unwrap(); assert!(!no_isolation.compatible(&some_isolation)); assert!(!no_isolation.compatible(&some_isolation2)); assert!(!some_isolation.compatible(&some_isolation2)); assert!(some_isolation.compatible(&some_isolation)); } }