arti_client/
rpc.rs

1//! Declare RPC functionality on for the `arti-client` crate.
2
3use derive_deftly::Deftly;
4use dyn_clone::DynClone;
5use futures::{SinkExt as _, StreamExt as _};
6use serde::{Deserialize, Serialize};
7use std::{net::IpAddr, sync::Arc};
8use tor_proto::stream::DataStream;
9
10use tor_rpcbase as rpc;
11use tor_rtcompat::Runtime;
12
13use crate::{StreamPrefs, TorAddr, TorClient};
14
15impl<R: Runtime> TorClient<R> {
16    /// Ensure that every RPC method is registered for this instantiation of TorClient.
17    ///
18    /// We can't use [`rpc::static_rpc_invoke_fn`] for these, since TorClient is
19    /// parameterized.
20    pub fn rpc_methods() -> Vec<rpc::dispatch::InvokerEnt> {
21        rpc::invoker_ent_list![
22            get_client_status::<R>,
23            watch_client_status::<R>,
24            isolated_client::<R>,
25            @special client_connect_with_prefs::<R>,
26            @special client_resolve_with_prefs::<R>,
27            @special client_resolve_ptr_with_prefs::<R>,
28        ]
29    }
30}
31
32/// Return current bootstrap and health information for a client.
33#[derive(Deftly, Debug, Serialize, Deserialize)]
34#[derive_deftly(rpc::DynMethod)]
35#[deftly(rpc(method_name = "arti:get_client_status"))]
36struct GetClientStatus {}
37
38impl rpc::RpcMethod for GetClientStatus {
39    type Output = ClientStatusInfo;
40    type Update = rpc::NoUpdates;
41}
42
43/// Run forever, delivering updates about a client's bootstrap and health information.
44///
45/// (This method can return updates that have no visible changes.)
46#[derive(Deftly, Debug, Serialize, Deserialize)]
47#[derive_deftly(rpc::DynMethod)]
48#[deftly(rpc(method_name = "arti:watch_client_status"))]
49struct WatchClientStatus {}
50
51impl rpc::RpcMethod for WatchClientStatus {
52    type Output = rpc::Nil; // TODO: Possibly there should be an rpc::Never for methods that don't return.
53    type Update = ClientStatusInfo;
54}
55
56/// Reported bootstrap and health information for a client.
57///
58/// Note that all `TorClient`s on a session share the same underlying bootstrap status:
59/// if you check the status for one, you don't need to check the others.
60#[derive(Serialize, Deserialize)]
61struct ClientStatusInfo {
62    /// True if the client is ready for traffic.
63    ready: bool,
64    /// Approximate estimate of how close the client is to being ready for traffic.
65    ///
66    /// This value is a rough approximation; its exact implementation may change over
67    /// arti versions.  It is not guaranteed to be monotonic.
68    fraction: f32,
69    /// If present, a description of possible problem(s) that may be stopping
70    /// the client from using the Tor network.
71    blocked: Option<String>,
72}
73
74impl From<crate::status::BootstrapStatus> for ClientStatusInfo {
75    fn from(s: crate::status::BootstrapStatus) -> Self {
76        let ready = s.ready_for_traffic();
77        let fraction = s.as_frac();
78        let blocked = s.blocked().map(|b| b.to_string());
79        Self {
80            ready,
81            fraction,
82            blocked,
83        }
84    }
85}
86
87// NOTE: These functions could be defined as methods on TorClient<R>.
88// I'm defining them like this to make it more clear that they are never
89// invoked as client.method(), but only via the RPC system.
90// We can revisit this later if we want.
91
92// TODO RPC: Once we have one or two more get/watch combinations,
93// we should look into some facility for automatically declaring them,
94// so that their behavior stays uniform.
95//
96// See https://gitlab.torproject.org/tpo/core/arti/-/issues/1384#note_3023659
97
98/// Invocable function to run [`GetClientStatus`] on a [`TorClient`].
99async fn get_client_status<R: Runtime>(
100    client: Arc<TorClient<R>>,
101    _method: Box<GetClientStatus>,
102    _ctx: Arc<dyn rpc::Context>,
103) -> Result<ClientStatusInfo, rpc::RpcError> {
104    Ok(client.bootstrap_status().into())
105}
106
107/// Invocable function to run [`WatchClientStatus`] on a [`TorClient`].
108async fn watch_client_status<R: Runtime>(
109    client: Arc<TorClient<R>>,
110    _method: Box<WatchClientStatus>,
111    _ctx: Arc<dyn rpc::Context>,
112    mut updates: rpc::UpdateSink<ClientStatusInfo>,
113) -> Result<rpc::Nil, rpc::RpcError> {
114    let mut events = client.bootstrap_events();
115
116    // Send the _current_ status, no matter what.
117    // (We do this after constructing er)
118    updates.send(client.bootstrap_status().into()).await?;
119
120    // Send additional updates whenever the status changes.
121    while let Some(status) = events.next().await {
122        updates.send(status.into()).await?;
123    }
124
125    // This can only happen if the client exits.
126    Ok(rpc::NIL)
127}
128
129/// Create a new isolated client instance.
130///
131/// Returned ObjectID is a handle for a new `TorClient`,
132/// which is isolated from other `TorClients`:
133/// any streams created with the new `TorClient` will not share circuits
134/// with streams created with any other `TorClient`.
135#[derive(Deftly, Debug, Serialize, Deserialize)]
136#[derive_deftly(rpc::DynMethod)]
137#[deftly(rpc(method_name = "arti:new_isolated_client"))]
138#[non_exhaustive]
139pub struct IsolatedClient {}
140
141impl rpc::RpcMethod for IsolatedClient {
142    type Output = rpc::SingleIdResponse;
143    type Update = rpc::NoUpdates;
144}
145
146/// RPC method implementation: return a new isolated client based on a given client.
147async fn isolated_client<R: Runtime>(
148    client: Arc<TorClient<R>>,
149    _method: Box<IsolatedClient>,
150    ctx: Arc<dyn rpc::Context>,
151) -> Result<rpc::SingleIdResponse, rpc::RpcError> {
152    let new_client = Arc::new(client.isolated_client());
153    let client_id = ctx.register_owned(new_client);
154    Ok(rpc::SingleIdResponse::from(client_id))
155}
156
157/// Type-erased error returned by ClientConnectionTarget.
158//
159// TODO RPC: It would be handy if this implemented HasErrorHint, but HasErrorHint is sealed.
160// Perhaps we could go and solve our problem by implementing HasErrorHint on dyn StdError?
161pub trait ClientConnectionError:
162    std::error::Error + tor_error::HasKind + DynClone + Send + Sync + seal::Sealed
163{
164}
165impl<E> seal::Sealed for E where E: std::error::Error + tor_error::HasKind + DynClone + Send + Sync {}
166impl<E> ClientConnectionError for E where
167    E: std::error::Error + tor_error::HasKind + DynClone + Send + Sync + seal::Sealed
168{
169}
170impl std::error::Error for Box<dyn ClientConnectionError> {
171    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
172        self.as_ref().source()
173    }
174}
175impl tor_error::HasKind for Box<dyn ClientConnectionError> {
176    fn kind(&self) -> tor_error::ErrorKind {
177        self.as_ref().kind()
178    }
179}
180dyn_clone::clone_trait_object!(ClientConnectionError);
181
182/// module to seal the ClientConnectionError trait.
183mod seal {
184    /// hidden trait to seal the ClientConnectionError trait.
185    #[allow(unreachable_pub)]
186    pub trait Sealed {}
187}
188
189/// Type alias for a Result return by ClientConnectionTarget
190pub type ClientConnectionResult<T> = Result<T, Box<dyn ClientConnectionError>>;
191
192/// RPC special method: make a connection to a chosen address and preferences.
193///
194/// This method has no method name, and is not invoked by an RPC session
195/// directly.  Instead, it is invoked in response to a SOCKS request.
196/// It receives its target from the SOCKS `DEST` field.
197/// The isolation information in its `SrreamPrefs`, if any, is taken from
198/// the SOCKS username/password.
199/// Other information in the `StreamPrefs` is inferred
200/// from the SOCKS port configuration in the usual way.
201///
202/// When this method returns successfully,
203/// the proxy code sends a SOCKS reply indicating success,
204/// and links the returned `DataStream` with the application's incoming socket,
205/// copying data back and forth.
206///
207/// If instead this method returns an error,
208/// the error is either used to generate a SOCKS error code,
209///
210/// Note 1: in the future, this method will likely be used to integrate RPC data streams
211/// with other proxy types other than SOCKS.
212/// When this happens, we will specify how those proxy types
213/// will provide `target` and `prefs`.
214///
215/// Note 2: This has to be a special method, because
216/// it needs to return a DataStream, which can't be serialized.
217///
218/// > TODO RPC: The above documentation still isn't quite specific enough,
219/// > and a lot of it belongs in socks.rs where it could explain how a SOCKS request
220/// > is interpreted and converted into a ConnectWithPrefs call.
221/// > See <https://gitlab.torproject.org/tpo/core/arti/-/merge_requests/2373#note_3071833>
222/// > for discussion.
223#[derive(Deftly, Debug)]
224#[derive_deftly(rpc::DynMethod)]
225#[deftly(rpc(no_method_name))]
226#[allow(clippy::exhaustive_structs)]
227pub struct ConnectWithPrefs {
228    /// The target address
229    pub target: TorAddr,
230    /// The stream preferences implied by the SOCKS connect request.
231    pub prefs: StreamPrefs,
232}
233impl rpc::Method for ConnectWithPrefs {
234    // TODO RPC: I am not sure that this is the error type we truly want.
235    type Output = Result<DataStream, Box<dyn ClientConnectionError>>;
236    type Update = rpc::NoUpdates;
237}
238
239/// RPC special method: lookup an address with a chosen address and preferences.
240///
241/// This method has no method name, and is not invoked by an RPC connection
242/// directly.  Instead, it is invoked in response to a SOCKS request.
243//
244// TODO RPC: We _could_ give this a method name so that it can be invoked as an RPC method, and
245// maybe we should.  First, however, we would need to make `StreamPrefs` an RPC-visible serializable
246// type, or replace it with an equivalent.
247#[derive(Deftly, Debug)]
248#[derive_deftly(rpc::DynMethod)]
249#[deftly(rpc(no_method_name))]
250#[allow(clippy::exhaustive_structs)]
251pub struct ResolveWithPrefs {
252    /// The hostname to resolve.
253    pub hostname: String,
254    /// The stream preferences implied by the SOCKS resolve request.
255    pub prefs: StreamPrefs,
256}
257impl rpc::Method for ResolveWithPrefs {
258    // TODO RPC: I am not sure that this is the error type we truly want.
259    type Output = Result<Vec<IpAddr>, Box<dyn ClientConnectionError>>;
260    type Update = rpc::NoUpdates;
261}
262
263/// RPC special method: reverse-lookup an address with a chosen address and preferences.
264///
265/// This method has no method name, and is not invoked by an RPC connection
266/// directly.  Instead, it is invoked in response to a SOCKS request.
267//
268// TODO RPC: We _could_ give this a method name so that it can be invoked as an RPC method, and
269// maybe we should.  First, however, we would need to make `StreamPrefs` an RPC-visible serializable
270// type, or replace it with an equivalent.
271#[derive(Deftly, Debug)]
272#[derive_deftly(rpc::DynMethod)]
273#[deftly(rpc(no_method_name))]
274#[allow(clippy::exhaustive_structs)]
275pub struct ResolvePtrWithPrefs {
276    /// The address to resolve.
277    pub addr: IpAddr,
278    /// The stream preferences implied by the SOCKS resolve request.
279    pub prefs: StreamPrefs,
280}
281impl rpc::Method for ResolvePtrWithPrefs {
282    // TODO RPC: I am not sure that this is the error type we truly want.
283    type Output = Result<Vec<String>, Box<dyn ClientConnectionError>>;
284    type Update = rpc::NoUpdates;
285}
286
287/// RPC method implementation: start a connection on a `TorClient`.
288async fn client_connect_with_prefs<R: Runtime>(
289    client: Arc<TorClient<R>>,
290    method: Box<ConnectWithPrefs>,
291    _ctx: Arc<dyn rpc::Context>,
292) -> Result<DataStream, Box<dyn ClientConnectionError>> {
293    TorClient::connect_with_prefs(client.as_ref(), &method.target, &method.prefs)
294        .await
295        .map_err(|e| Box::new(e) as _)
296}
297
298/// RPC method implementation: perform a remote DNS lookup using a `TorClient`.
299async fn client_resolve_with_prefs<R: Runtime>(
300    client: Arc<TorClient<R>>,
301    method: Box<ResolveWithPrefs>,
302    _ctx: Arc<dyn rpc::Context>,
303) -> Result<Vec<IpAddr>, Box<dyn ClientConnectionError>> {
304    TorClient::resolve_with_prefs(client.as_ref(), &method.hostname, &method.prefs)
305        .await
306        .map_err(|e| Box::new(e) as _)
307}
308
309/// RPC method implementation: perform a remote DNS reverse lookup using a `TorClient`.
310async fn client_resolve_ptr_with_prefs<R: Runtime>(
311    client: Arc<TorClient<R>>,
312    method: Box<ResolvePtrWithPrefs>,
313    _ctx: Arc<dyn rpc::Context>,
314) -> Result<Vec<String>, Box<dyn ClientConnectionError>> {
315    TorClient::resolve_ptr_with_prefs(client.as_ref(), method.addr, &method.prefs)
316        .await
317        .map_err(|e| Box::new(e) as _)
318}