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}