tor_interface/
mock_tor_client.rs

1// standard
2use std::collections::BTreeMap;
3use std::net::{SocketAddr, TcpListener, TcpStream};
4use std::sync::{atomic, Arc, Mutex};
5
6// internal crates
7use crate::tor_crypto::*;
8use crate::tor_provider;
9use crate::tor_provider::*;
10
11
12/// [`MockTorClient`]-specific error type
13#[derive(thiserror::Error, Debug)]
14pub enum Error {
15    #[error("client not bootstrapped")]
16    ClientNotBootstrapped(),
17
18    #[error("client already bootstrapped")]
19    ClientAlreadyBootstrapped(),
20
21    #[error("onion service not found: {}", .0)]
22    OnionServiceNotFound(OnionAddr),
23
24    #[error("onion service not published: {}", .0)]
25    OnionServiceNotPublished(OnionAddr),
26
27    #[error("onion service requires onion auth")]
28    OnionServiceRequiresOnionAuth(),
29
30    #[error("provided onion auth key invalid")]
31    OnionServiceAuthInvalid(),
32
33    #[error("unable to bind TCP listener")]
34    TcpListenerBindFailed(#[source] std::io::Error),
35
36    #[error("unable to get TCP listener's local adress")]
37    TcpListenerLocalAddrFailed(#[source] std::io::Error),
38
39    #[error("unable to connect to {}", .0)]
40    ConnectFailed(TargetAddr),
41
42    #[error("not implemented")]
43    NotImplemented(),
44}
45
46impl From<Error> for crate::tor_provider::Error {
47    fn from(error: Error) -> Self {
48        crate::tor_provider::Error::Generic(error.to_string())
49    }
50}
51
52struct MockTorNetwork {
53    onion_services: Option<BTreeMap<OnionAddr, (Vec<X25519PublicKey>, SocketAddr)>>,
54}
55
56impl MockTorNetwork {
57    const fn new() -> MockTorNetwork {
58        MockTorNetwork {
59            onion_services: None,
60        }
61    }
62
63    fn connect_to_onion(
64        &mut self,
65        service_id: &V3OnionServiceId,
66        virt_port: u16,
67        client_auth: Option<&X25519PublicKey>,
68    ) -> Result<OnionStream, Error> {
69        let onion_addr = OnionAddr::V3(OnionAddrV3::new(service_id.clone(), virt_port));
70
71        match &mut self.onion_services {
72            Some(onion_services) => {
73                if let Some((client_auth_keys, socket_addr)) = onion_services.get(&onion_addr) {
74                    match (client_auth_keys.len(), client_auth) {
75                        (0, None) => (),
76                        (_, None) => return Err(Error::OnionServiceRequiresOnionAuth()),
77                        (0, Some(_)) => return Err(Error::OnionServiceAuthInvalid()),
78                        (_, Some(client_auth)) => {
79                            if !client_auth_keys.contains(client_auth) {
80                                return Err(Error::OnionServiceAuthInvalid());
81                            }
82                        }
83                    }
84
85                    if let Ok(stream) = TcpStream::connect(socket_addr) {
86                        Ok(OnionStream {
87                            stream,
88                            local_addr: None,
89                            peer_addr: Some(TargetAddr::OnionService(onion_addr)),
90                        })
91                    } else {
92                        Err(Error::OnionServiceNotFound(onion_addr))
93                    }
94                } else {
95                    Err(Error::OnionServiceNotPublished(onion_addr))
96                }
97            }
98            None => Err(Error::OnionServiceNotPublished(onion_addr)),
99        }
100    }
101
102    fn start_onion(
103        &mut self,
104        service_id: V3OnionServiceId,
105        virt_port: u16,
106        client_auth_keys: Vec<X25519PublicKey>,
107        address: SocketAddr,
108    ) {
109        let onion_addr = OnionAddr::V3(OnionAddrV3::new(service_id, virt_port));
110        match &mut self.onion_services {
111            Some(onion_services) => {
112                onion_services.insert(onion_addr, (client_auth_keys, address));
113            }
114            None => {
115                let mut onion_services = BTreeMap::new();
116                onion_services.insert(onion_addr, (client_auth_keys, address));
117                self.onion_services = Some(onion_services);
118            }
119        }
120    }
121
122    fn stop_onion(&mut self, onion_addr: &OnionAddr) {
123        if let Some(onion_services) = &mut self.onion_services {
124            onion_services.remove(onion_addr);
125        }
126    }
127}
128
129static MOCK_TOR_NETWORK: Mutex<MockTorNetwork> = Mutex::new(MockTorNetwork::new());
130
131/// A mock `TorProvider` implementation for testing.
132///
133/// `MockTorClient` implements the [`TorProvider`] trait. It creates a fake, in-process Tor Network using local socekts and listeners. No actual traffic ever leaves the local host.
134///
135/// Mock onion-services can be created, connected to, and communiccated with. Connecting to clearnet targets always succeeds by connecting to single local endpoint, but will never send any traffic to connecting clients.
136pub struct MockTorClient {
137    events: Vec<TorEvent>,
138    bootstrapped: bool,
139    client_auth_keys: BTreeMap<V3OnionServiceId, X25519PublicKey>,
140    onion_services: Vec<(OnionAddr, Arc<atomic::AtomicBool>)>,
141    loopback: TcpListener,
142}
143
144impl MockTorClient {
145    /// Construt a new `MockTorClient`.
146    pub fn new() -> MockTorClient {
147        let mut events: Vec<TorEvent> = Default::default();
148        let line = "[notice] MockTorClient running".to_string();
149        events.push(TorEvent::LogReceived { line });
150
151        let socket_addr = SocketAddr::from(([127, 0, 0, 1], 0u16));
152        let listener = TcpListener::bind(socket_addr).expect("tcplistener bind failed");
153
154        MockTorClient {
155            events,
156            bootstrapped: false,
157            client_auth_keys: Default::default(),
158            onion_services: Default::default(),
159            loopback: listener,
160        }
161    }
162}
163
164impl Default for MockTorClient {
165    fn default() -> Self {
166        Self::new()
167    }
168}
169
170impl TorProvider for MockTorClient {
171    fn update(&mut self) -> Result<Vec<TorEvent>, tor_provider::Error> {
172        match MOCK_TOR_NETWORK.lock() {
173            Ok(mut mock_tor_network) => {
174                let mut i = 0;
175                while i < self.onion_services.len() {
176                    // remove onion services with no active listeners
177                    if !self.onion_services[i].1.load(atomic::Ordering::Relaxed) {
178                        let entry = self.onion_services.swap_remove(i);
179                        let onion_addr = entry.0;
180                        mock_tor_network.stop_onion(&onion_addr);
181                    } else {
182                        i += 1;
183                    }
184                }
185            }
186            Err(_) => unreachable!("another thread panicked while holding mock tor network's lock"),
187        }
188
189        Ok(std::mem::take(&mut self.events))
190    }
191
192    fn bootstrap(&mut self) -> Result<(), tor_provider::Error> {
193        if self.bootstrapped {
194            Err(Error::ClientAlreadyBootstrapped())?
195        } else {
196            self.events.push(TorEvent::BootstrapStatus {
197                progress: 0u32,
198                tag: "start".to_string(),
199                summary: "bootstrapping started".to_string(),
200            });
201            self.events.push(TorEvent::BootstrapStatus {
202                progress: 50u32,
203                tag: "middle".to_string(),
204                summary: "bootstrapping continues".to_string(),
205            });
206            self.events.push(TorEvent::BootstrapStatus {
207                progress: 100u32,
208                tag: "finished".to_string(),
209                summary: "bootstrapping completed".to_string(),
210            });
211            self.events.push(TorEvent::BootstrapComplete);
212            self.bootstrapped = true;
213            Ok(())
214        }
215    }
216
217    fn add_client_auth(
218        &mut self,
219        service_id: &V3OnionServiceId,
220        client_auth: &X25519PrivateKey,
221    ) -> Result<(), tor_provider::Error> {
222        let client_auth_public = X25519PublicKey::from_private_key(client_auth);
223        if let Some(key) = self.client_auth_keys.get_mut(service_id) {
224            *key = client_auth_public;
225        } else {
226            self.client_auth_keys
227                .insert(service_id.clone(), client_auth_public);
228        }
229        Ok(())
230    }
231
232    fn remove_client_auth(
233        &mut self,
234        service_id: &V3OnionServiceId,
235    ) -> Result<(), tor_provider::Error> {
236        self.client_auth_keys.remove(service_id);
237        Ok(())
238    }
239
240    fn connect(
241        &mut self,
242        target: TargetAddr,
243        _circuit: Option<CircuitToken>,
244    ) -> Result<OnionStream, tor_provider::Error> {
245        let (service_id, virt_port) = match target {
246            TargetAddr::OnionService(OnionAddr::V3(OnionAddrV3 {
247                service_id,
248                virt_port,
249            })) => (service_id, virt_port),
250            target_address => {
251                if let Ok(stream) = TcpStream::connect(
252                    self.loopback
253                        .local_addr()
254                        .expect("loopback local_addr failed"),
255                ) {
256                    return Ok(OnionStream {
257                        stream,
258                        local_addr: None,
259                        peer_addr: Some(target_address),
260                    });
261                } else {
262                    return Err(Error::ConnectFailed(target_address).into());
263                }
264            }
265        };
266        let client_auth = self.client_auth_keys.get(&service_id);
267
268        match MOCK_TOR_NETWORK.lock() {
269            Ok(mut mock_tor_network) => {
270                Ok(mock_tor_network.connect_to_onion(&service_id, virt_port, client_auth)?)
271            }
272            Err(_) => unreachable!("another thread panicked while holding mock tor network's lock"),
273        }
274    }
275
276    fn listener(
277        &mut self,
278        private_key: &Ed25519PrivateKey,
279        virt_port: u16,
280        authorized_clients: Option<&[X25519PublicKey]>,
281    ) -> Result<OnionListener, tor_provider::Error> {
282        // convert inputs to relevant types
283        let service_id = V3OnionServiceId::from_private_key(private_key);
284        let onion_addr = OnionAddr::V3(OnionAddrV3::new(service_id.clone(), virt_port));
285        let authorized_clients: Vec<X25519PublicKey> = match authorized_clients {
286            Some(keys) => keys.into(),
287            None => Default::default(),
288        };
289
290        // try to bind to a local address, let OS pick our port
291        let socket_addr = SocketAddr::from(([127, 0, 0, 1], 0u16));
292        let listener = TcpListener::bind(socket_addr).map_err(Error::TcpListenerBindFailed)?;
293        let socket_addr = listener
294            .local_addr()
295            .map_err(Error::TcpListenerLocalAddrFailed)?;
296
297        // register the onion service with the mock tor network
298        match MOCK_TOR_NETWORK.lock() {
299            Ok(mut mock_tor_network) => mock_tor_network.start_onion(
300                service_id.clone(),
301                virt_port,
302                authorized_clients,
303                socket_addr,
304            ),
305            Err(_) => unreachable!("another thread panicked while holding mock tor network's lock"),
306        }
307
308        // init flag for signaling when listener goes out of scope so we can tear down onion service
309        let is_active = Arc::new(atomic::AtomicBool::new(true));
310        self.onion_services
311            .push((onion_addr.clone(), Arc::clone(&is_active)));
312
313        // onion service published event
314        self.events
315            .push(TorEvent::OnionServicePublished { service_id });
316
317
318        Ok(OnionListener::new(listener, onion_addr, is_active, |is_active| {
319            is_active.store(false, atomic::Ordering::Relaxed);
320        }))
321    }
322
323    fn generate_token(&mut self) -> CircuitToken {
324        0usize
325    }
326
327    fn release_token(&mut self, _token: CircuitToken) {}
328}
329
330impl Drop for MockTorClient {
331    fn drop(&mut self) {
332        // remove all our onion services
333        match MOCK_TOR_NETWORK.lock() {
334            Ok(mut mock_tor_network) => {
335                for entry in self.onion_services.iter() {
336                    let onion_addr = &entry.0;
337                    mock_tor_network.stop_onion(onion_addr);
338                }
339            }
340            Err(_) => unreachable!("another thread panicked while holding mock tor network's lock"),
341        }
342    }
343}