Skip to main content

zcash_client_backend/
tor.rs

1//! Tor support for Zcash wallets.
2
3use std::{fmt, io, path::Path, sync::Arc, time::Duration};
4
5use arti_client::{TorClient, config::TorClientConfigBuilder};
6use tokio_rustls::rustls::crypto::{CryptoProvider, ring};
7use tor_rtcompat::PreferredRuntime;
8use tracing::debug;
9
10#[cfg(feature = "lightwalletd-tonic-tls-webpki-roots")]
11mod grpc;
12
13pub mod http;
14
15// Re-exported as this is currently the only `arti_client` type users would need to use
16// our minimal client API.
17pub use arti_client::DormantMode;
18
19/// Deadlines applied to network operations performed by a [`Client`].
20///
21/// Without these, a server that accepts a connection and then goes silent would leave the
22/// corresponding future pending forever. Tor circuits make this more likely than it is on
23/// the clearnet: a half-open circuit can persist for a long time without the peer's TCP
24/// stack ever delivering a reset.
25///
26/// Use [`Timeouts::default`] for values suitable for typical Tor circuit latency, or the
27/// `with_*` methods to tune them.
28#[derive(Clone, Copy, Debug)]
29pub struct Timeouts {
30    connect: Duration,
31    request: Duration,
32    response_body: Duration,
33    grpc_keepalive_interval: Duration,
34    grpc_keepalive_timeout: Duration,
35}
36
37impl Default for Timeouts {
38    fn default() -> Self {
39        Self {
40            connect: Duration::from_secs(30),
41            request: Duration::from_secs(60),
42            response_body: Duration::from_secs(60),
43            grpc_keepalive_interval: Duration::from_secs(30),
44            grpc_keepalive_timeout: Duration::from_secs(20),
45        }
46    }
47}
48
49impl Timeouts {
50    /// Sets the maximum time allowed to establish a usable connection to a server.
51    ///
52    /// This covers opening the Tor stream and, for HTTPS or gRPC-over-TLS endpoints,
53    /// completing the TLS handshake.
54    ///
55    /// The default is 30 seconds.
56    #[must_use]
57    pub fn with_connect(mut self, timeout: Duration) -> Self {
58        self.connect = timeout;
59        self
60    }
61
62    /// Sets the maximum time allowed to send a request and receive its response headers.
63    ///
64    /// This does not bound the time taken to receive a response body; see
65    /// [`Self::with_response_body`] for HTTP, and [`Self::with_grpc_keepalive`] for gRPC.
66    ///
67    /// The default is 60 seconds.
68    #[must_use]
69    pub fn with_request(mut self, timeout: Duration) -> Self {
70        self.request = timeout;
71        self
72    }
73
74    /// Sets the maximum time allowed to receive a complete HTTP response body.
75    ///
76    /// This is not applied to gRPC responses, because a gRPC response may be a
77    /// long-running stream (such as `GetBlockRange`) for which no fixed deadline is
78    /// appropriate. Use [`Self::with_grpc_keepalive`] to bound those instead.
79    ///
80    /// The default is 60 seconds.
81    #[must_use]
82    pub fn with_response_body(mut self, timeout: Duration) -> Self {
83        self.response_body = timeout;
84        self
85    }
86
87    /// Sets the HTTP/2 keep-alive parameters used for gRPC connections.
88    ///
89    /// A ping is sent every `interval` while a request is in flight, and the connection is
90    /// closed if no acknowledgement is received within `timeout`. This is what detects a
91    /// gRPC peer that stalls partway through streaming a response, which no request
92    /// deadline can bound without also capping legitimately long streams.
93    ///
94    /// The defaults are a 30 second interval and a 20 second timeout.
95    #[must_use]
96    pub fn with_grpc_keepalive(mut self, interval: Duration, timeout: Duration) -> Self {
97        self.grpc_keepalive_interval = interval;
98        self.grpc_keepalive_timeout = timeout;
99        self
100    }
101}
102
103/// A Tor client that exposes capabilities designed for Zcash wallets.
104#[derive(Clone)]
105pub struct Client {
106    inner: Arc<TorClient<PreferredRuntime>>,
107    timeouts: Timeouts,
108}
109
110impl Client {
111    /// Creates and bootstraps a Tor client with default [`Timeouts`].
112    ///
113    /// The client's persistent data and cache are both stored in the given directory.
114    /// Preserving the contents of this directory will speed up subsequent calls to
115    /// `Client::create`.
116    ///
117    /// If the `with_permissions` closure does not make any changes (e.g. is
118    /// passed as `|_| {}`), the default from [`arti_client`] will be used.
119    /// This default will enable permissions checks unless the
120    /// `ARTI_FS_DISABLE_PERMISSION_CHECKS` env variable is set.
121    ///
122    /// Tor requires a process-level `rustls` [`CryptoProvider`]. If the application has
123    /// not installed one, this installs the `ring` provider as the process default.
124    ///
125    /// Returns an error if `tor_dir` does not exist, or if bootstrapping fails.
126    pub async fn create(
127        tor_dir: &Path,
128        with_permissions: impl FnOnce(&mut fs_mistrust::MistrustBuilder),
129    ) -> Result<Self, Error> {
130        Self::create_with_timeouts(tor_dir, with_permissions, Timeouts::default()).await
131    }
132
133    /// Creates and bootstraps a Tor client that applies the given [`Timeouts`] to its
134    /// network operations.
135    ///
136    /// This is otherwise identical to [`Client::create`]; see its documentation for
137    /// details of the other arguments.
138    pub async fn create_with_timeouts(
139        tor_dir: &Path,
140        with_permissions: impl FnOnce(&mut fs_mistrust::MistrustBuilder),
141        timeouts: Timeouts,
142    ) -> Result<Self, Error> {
143        if CryptoProvider::get_default().is_none() {
144            // An error means that another thread installed a provider first. That
145            // provider stays in place.
146            let _provider_already_installed = ring::default_provider().install_default();
147        }
148
149        let runtime = PreferredRuntime::current()?;
150
151        if !tokio::fs::try_exists(tor_dir).await? {
152            return Err(Error::MissingTorDirectory);
153        }
154
155        let mut config_builder = TorClientConfigBuilder::from_directories(
156            tor_dir.join("arti-data"),
157            tor_dir.join("arti-cache"),
158        );
159
160        with_permissions(config_builder.storage().permissions());
161
162        let config = config_builder
163            .build()
164            .expect("all required fields initialized");
165
166        let client_builder = TorClient::with_runtime(runtime).config(config);
167
168        debug!("Bootstrapping Tor");
169        let inner = client_builder.create_bootstrapped().await?;
170        debug!("Tor bootstrapped");
171
172        Ok(Self { inner, timeouts })
173    }
174
175    /// Ensures the Tor client is bootstrapped.
176    ///
177    /// This should be called first inside every public method that makes network requests
178    /// using the Tor client.
179    ///
180    /// `Client` ensures it cannot be constructed in an un-bootstrapped state, but Tor
181    /// clients can become less bootstrapped over time (for example if it loses its
182    /// internet connectivity, or if its directory information expires before it's able to
183    /// replace it).
184    async fn ensure_bootstrapped(&self) -> Result<(), Error> {
185        if !self.inner.bootstrap_status().ready_for_traffic() {
186            debug!("Re-bootstrapping Tor");
187            self.inner.bootstrap().await?;
188            debug!("Tor re-bootstrapped");
189        }
190        Ok(())
191    }
192
193    /// Returns a new isolated `tor::Client` handle.
194    ///
195    /// The two `tor::Client`s will share internal state and configuration, but their
196    /// streams will never share circuits with one another.
197    ///
198    /// Use this method when you want separate parts of your program to each have a
199    /// `tor::Client` handle, but where you don't want their activities to be linkable to
200    /// one another over the Tor network.
201    ///
202    /// Calling this method is usually preferable to creating a completely separate
203    /// `tor::Client` instance, since it can share its internals with the existing
204    /// `tor::Client`.
205    ///
206    /// (Connections made with clones of the returned `tor::Client` may share circuits
207    /// with each other.)
208    ///
209    /// The returned handle uses the same [`Timeouts`] as this one.
210    #[must_use]
211    pub fn isolated_client(&self) -> Self {
212        Self {
213            inner: self.inner.isolated_client(),
214            timeouts: self.timeouts,
215        }
216    }
217
218    /// Changes the client's current dormant mode, putting background tasks to sleep or
219    /// waking them up as appropriate.
220    ///
221    /// This can be used to conserve CPU usage if you aren’t planning on using the client
222    /// for a while, especially on mobile platforms.
223    ///
224    /// See the [`DormantMode`] documentation for more details.
225    pub fn set_dormant(&self, mode: DormantMode) {
226        self.inner.set_dormant(mode);
227    }
228}
229
230/// Errors that can occur while creating or using a Tor [`Client`].
231#[derive(Debug)]
232#[non_exhaustive]
233pub enum Error {
234    /// The directory passed to [`Client::create`] does not exist.
235    MissingTorDirectory,
236    #[cfg(feature = "lightwalletd-tonic-tls-webpki-roots")]
237    /// An error occurred while using gRPC-over-Tor.
238    Grpc(self::grpc::GrpcError),
239    /// An error occurred while using HTTP-over-Tor.
240    Http(self::http::HttpError),
241    /// An IO error occurred while interacting with the filesystem.
242    Io(io::Error),
243    /// A Tor-specific error.
244    Tor(arti_client::Error),
245}
246
247impl fmt::Display for Error {
248    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
249        match self {
250            Error::MissingTorDirectory => write!(f, "Tor directory is missing"),
251            #[cfg(feature = "lightwalletd-tonic-tls-webpki-roots")]
252            Error::Grpc(e) => write!(f, "gRPC-over-Tor error: {e}"),
253            Error::Http(e) => write!(f, "HTTP-over-Tor error: {e}"),
254            Error::Io(e) => write!(f, "IO error: {e}"),
255            Error::Tor(e) => write!(f, "Tor error: {e}"),
256        }
257    }
258}
259
260impl std::error::Error for Error {
261    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
262        match self {
263            Error::MissingTorDirectory => None,
264            #[cfg(feature = "lightwalletd-tonic-tls-webpki-roots")]
265            Error::Grpc(e) => Some(e),
266            Error::Http(e) => Some(e),
267            Error::Io(e) => Some(e),
268            Error::Tor(e) => Some(e),
269        }
270    }
271}
272
273#[cfg(feature = "lightwalletd-tonic-tls-webpki-roots")]
274impl From<self::grpc::GrpcError> for Error {
275    fn from(e: self::grpc::GrpcError) -> Self {
276        Error::Grpc(e)
277    }
278}
279
280impl From<self::http::HttpError> for Error {
281    fn from(e: self::http::HttpError) -> Self {
282        Error::Http(e)
283    }
284}
285
286impl From<io::Error> for Error {
287    fn from(e: io::Error) -> Self {
288        Error::Io(e)
289    }
290}
291
292impl From<arti_client::Error> for Error {
293    fn from(e: arti_client::Error) -> Self {
294        Error::Tor(e)
295    }
296}