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
//! Stream isolation for Tor circuits
//!
//! Stream isolation ensures different activities use different Tor circuits,
//! preventing correlation attacks that could link your actions together.
use std::sync::atomic::{AtomicU64, Ordering};
/// Global counter for generating unique isolation tokens
static ISOLATION_COUNTER: AtomicU64 = AtomicU64::new(0);
/// Stream isolation level
///
/// Determines how Tor circuits are shared between requests.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum IsolationLevel {
/// No isolation - all requests share circuits (fastest, least private)
#[default]
None,
/// Isolate by destination host (requests to different hosts use different circuits)
ByHost,
/// Isolate per request (each request gets a new circuit - slowest, most private)
PerRequest,
/// Custom isolation token (requests with same token share a circuit)
Token(IsolationToken),
}
/// Unique token for custom stream isolation
///
/// Requests with the same token will share a Tor circuit.
/// Different tokens guarantee different circuits.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct IsolationToken(u64);
impl IsolationToken {
/// Create a new unique isolation token
pub fn new() -> Self {
Self(ISOLATION_COUNTER.fetch_add(1, Ordering::Relaxed))
}
/// Create a token from a raw value (for testing)
pub fn from_raw(value: u64) -> Self {
Self(value)
}
/// Get the raw token value
pub fn as_raw(&self) -> u64 {
self.0
}
}
impl Default for IsolationToken {
fn default() -> Self {
Self::new()
}
}
/// A session with its own isolation token
///
/// All requests made through an isolated session share the same Tor circuit,
/// but use a different circuit from other sessions.
#[derive(Debug, Clone)]
pub struct IsolatedSession {
token: IsolationToken,
}
impl IsolatedSession {
/// Create a new isolated session
pub fn new() -> Self {
Self {
token: IsolationToken::new(),
}
}
/// Get the isolation token for this session
pub fn token(&self) -> IsolationToken {
self.token
}
}
impl Default for IsolatedSession {
fn default() -> Self {
Self::new()
}
}
/// Compute isolation token based on level and request info
pub fn compute_isolation(level: IsolationLevel, host: Option<&str>) -> Option<IsolationToken> {
match level {
IsolationLevel::None => None,
IsolationLevel::ByHost => {
// Hash the host to create a deterministic token
host.map(|h| {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
h.hash(&mut hasher);
IsolationToken::from_raw(hasher.finish())
})
}
IsolationLevel::PerRequest => Some(IsolationToken::new()),
IsolationLevel::Token(token) => Some(token),
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used)]
use super::*;
#[test]
fn test_token_uniqueness() {
let t1 = IsolationToken::new();
let t2 = IsolationToken::new();
assert_ne!(t1, t2);
}
#[test]
fn test_compute_isolation_none() {
let token = compute_isolation(IsolationLevel::None, Some("example.com"));
assert!(token.is_none());
}
#[test]
fn test_compute_isolation_by_host() {
let t1 = compute_isolation(IsolationLevel::ByHost, Some("example.com"));
let t2 = compute_isolation(IsolationLevel::ByHost, Some("example.com"));
let t3 = compute_isolation(IsolationLevel::ByHost, Some("other.com"));
assert_eq!(t1, t2); // Same host = same token
assert_ne!(t1, t3); // Different host = different token
}
#[test]
fn test_compute_isolation_per_request() {
let t1 = compute_isolation(IsolationLevel::PerRequest, Some("example.com"));
let t2 = compute_isolation(IsolationLevel::PerRequest, Some("example.com"));
assert_ne!(t1, t2); // Always different
}
}