//! Declare RPC functionality on for the `arti-client` crate. use derive_deftly::Deftly; use dyn_clone::DynClone; use futures::{SinkExt as _, StreamExt as _}; use serde::{Deserialize, Serialize}; use std::{net::IpAddr, sync::Arc}; use tor_proto::stream::DataStream; use tor_rpcbase as rpc; use tor_rtcompat::Runtime; use crate::{StreamPrefs, TorAddr, TorClient}; impl TorClient { /// Ensure that every RPC method is registered for this instantiation of TorClient. /// /// We can't use [`rpc::static_rpc_invoke_fn`] for these, since TorClient is /// parameterized. pub fn rpc_methods() -> Vec { rpc::invoker_ent_list![ get_client_status::, watch_client_status::, isolated_client::, @special client_connect_with_prefs::, @special client_resolve_with_prefs::, @special client_resolve_ptr_with_prefs::, ] } } /// Return current bootstrap and health information for a client. #[derive(Deftly, Debug, Serialize, Deserialize)] #[derive_deftly(rpc::DynMethod)] #[deftly(rpc(method_name = "arti:get_client_status"))] struct GetClientStatus {} impl rpc::RpcMethod for GetClientStatus { type Output = ClientStatusInfo; type Update = rpc::NoUpdates; } /// Run forever, delivering updates about a client's bootstrap and health information. /// /// (This method can return updates that have no visible changes.) #[derive(Deftly, Debug, Serialize, Deserialize)] #[derive_deftly(rpc::DynMethod)] #[deftly(rpc(method_name = "arti:watch_client_status"))] struct WatchClientStatus {} impl rpc::RpcMethod for WatchClientStatus { type Output = rpc::Nil; // TODO: Possibly there should be an rpc::Never for methods that don't return. type Update = ClientStatusInfo; } /// Reported bootstrap and health information for a client. /// /// Note that all `TorClient`s on a session share the same underlying bootstrap status: /// if you check the status for one, you don't need to check the others. #[derive(Serialize, Deserialize)] struct ClientStatusInfo { /// True if the client is ready for traffic. ready: bool, /// Approximate estimate of how close the client is to being ready for traffic. /// /// This value is a rough approximation; its exact implementation may change over /// arti versions. It is not guaranteed to be monotonic. fraction: f32, /// If present, a description of possible problem(s) that may be stopping /// the client from using the Tor network. blocked: Option, } impl From for ClientStatusInfo { fn from(s: crate::status::BootstrapStatus) -> Self { let ready = s.ready_for_traffic(); let fraction = s.as_frac(); let blocked = s.blocked().map(|b| b.to_string()); Self { ready, fraction, blocked, } } } // NOTE: These functions could be defined as methods on TorClient. // I'm defining them like this to make it more clear that they are never // invoked as client.method(), but only via the RPC system. // We can revisit this later if we want. // TODO RPC: Once we have one or two more get/watch combinations, // we should look into some facility for automatically declaring them, // so that their behavior stays uniform. // // See https://gitlab.torproject.org/tpo/core/arti/-/issues/1384#note_3023659 /// Invocable function to run [`GetClientStatus`] on a [`TorClient`]. async fn get_client_status( client: Arc>, _method: Box, _ctx: Arc, ) -> Result { Ok(client.bootstrap_status().into()) } /// Invocable function to run [`WatchClientStatus`] on a [`TorClient`]. async fn watch_client_status( client: Arc>, _method: Box, _ctx: Arc, mut updates: rpc::UpdateSink, ) -> Result { let mut events = client.bootstrap_events(); // Send the _current_ status, no matter what. // (We do this after constructing er) updates.send(client.bootstrap_status().into()).await?; // Send additional updates whenever the status changes. while let Some(status) = events.next().await { updates.send(status.into()).await?; } // This can only happen if the client exits. Ok(rpc::NIL) } /// Create a new isolated client instance. /// /// Returned ObjectID is a handle for a new `TorClient`, /// which is isolated from other `TorClients`: /// any streams created with the new `TorClient` will not share circuits /// with streams created with any other `TorClient`. #[derive(Deftly, Debug, Serialize, Deserialize)] #[derive_deftly(rpc::DynMethod)] #[deftly(rpc(method_name = "arti:new_isolated_client"))] #[non_exhaustive] pub struct IsolatedClient {} impl rpc::RpcMethod for IsolatedClient { type Output = rpc::SingleIdResponse; type Update = rpc::NoUpdates; } /// RPC method implementation: return a new isolated client based on a given client. async fn isolated_client( client: Arc>, _method: Box, ctx: Arc, ) -> Result { let new_client = Arc::new(client.isolated_client()); let client_id = ctx.register_owned(new_client); Ok(rpc::SingleIdResponse::from(client_id)) } /// Type-erased error returned by ClientConnectionTarget. // // TODO RPC: It would be handy if this implemented HasErrorHint, but HasErrorHint is sealed. // Perhaps we could go and solve our problem by implementing HasErrorHint on dyn StdError? pub trait ClientConnectionError: std::error::Error + tor_error::HasKind + DynClone + Send + Sync + seal::Sealed { } impl seal::Sealed for E where E: std::error::Error + tor_error::HasKind + DynClone + Send + Sync {} impl ClientConnectionError for E where E: std::error::Error + tor_error::HasKind + DynClone + Send + Sync + seal::Sealed { } impl std::error::Error for Box { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { self.as_ref().source() } } impl tor_error::HasKind for Box { fn kind(&self) -> tor_error::ErrorKind { self.as_ref().kind() } } dyn_clone::clone_trait_object!(ClientConnectionError); /// module to seal the ClientConnectionError trait. mod seal { /// hidden trait to seal the ClientConnectionError trait. #[allow(unreachable_pub)] pub trait Sealed {} } /// Type alias for a Result return by ClientConnectionTarget pub type ClientConnectionResult = Result>; /// RPC special method: make a connection to a chosen address and preferences. /// /// This method has no method name, and is not invoked by an RPC session /// directly. Instead, it is invoked in response to a SOCKS request. /// It receives its target from the SOCKS `DEST` field. /// The isolation information in its `SrreamPrefs`, if any, is taken from /// the SOCKS username/password. /// Other information in the `StreamPrefs` is inferred /// from the SOCKS port configuration in the usual way. /// /// When this method returns successfully, /// the proxy code sends a SOCKS reply indicating success, /// and links the returned `DataStream` with the application's incoming socket, /// copying data back and forth. /// /// If instead this method returns an error, /// the error is either used to generate a SOCKS error code, /// /// Note 1: in the future, this method will likely be used to integrate RPC data streams /// with other proxy types other than SOCKS. /// When this happens, we will specify how those proxy types /// will provide `target` and `prefs`. /// /// Note 2: This has to be a special method, because /// it needs to return a DataStream, which can't be serialized. /// /// > TODO RPC: The above documentation still isn't quite specific enough, /// > and a lot of it belongs in socks.rs where it could explain how a SOCKS request /// > is interpreted and converted into a ConnectWithPrefs call. /// > See /// > for discussion. #[derive(Deftly, Debug)] #[derive_deftly(rpc::DynMethod)] #[deftly(rpc(no_method_name))] #[allow(clippy::exhaustive_structs)] pub struct ConnectWithPrefs { /// The target address pub target: TorAddr, /// The stream preferences implied by the SOCKS connect request. pub prefs: StreamPrefs, } impl rpc::Method for ConnectWithPrefs { // TODO RPC: I am not sure that this is the error type we truly want. type Output = Result>; type Update = rpc::NoUpdates; } /// RPC special method: lookup an address with a chosen address and preferences. /// /// This method has no method name, and is not invoked by an RPC connection /// directly. Instead, it is invoked in response to a SOCKS request. // // TODO RPC: We _could_ give this a method name so that it can be invoked as an RPC method, and // maybe we should. First, however, we would need to make `StreamPrefs` an RPC-visible serializable // type, or replace it with an equivalent. #[derive(Deftly, Debug)] #[derive_deftly(rpc::DynMethod)] #[deftly(rpc(no_method_name))] #[allow(clippy::exhaustive_structs)] pub struct ResolveWithPrefs { /// The hostname to resolve. pub hostname: String, /// The stream preferences implied by the SOCKS resolve request. pub prefs: StreamPrefs, } impl rpc::Method for ResolveWithPrefs { // TODO RPC: I am not sure that this is the error type we truly want. type Output = Result, Box>; type Update = rpc::NoUpdates; } /// RPC special method: reverse-lookup an address with a chosen address and preferences. /// /// This method has no method name, and is not invoked by an RPC connection /// directly. Instead, it is invoked in response to a SOCKS request. // // TODO RPC: We _could_ give this a method name so that it can be invoked as an RPC method, and // maybe we should. First, however, we would need to make `StreamPrefs` an RPC-visible serializable // type, or replace it with an equivalent. #[derive(Deftly, Debug)] #[derive_deftly(rpc::DynMethod)] #[deftly(rpc(no_method_name))] #[allow(clippy::exhaustive_structs)] pub struct ResolvePtrWithPrefs { /// The address to resolve. pub addr: IpAddr, /// The stream preferences implied by the SOCKS resolve request. pub prefs: StreamPrefs, } impl rpc::Method for ResolvePtrWithPrefs { // TODO RPC: I am not sure that this is the error type we truly want. type Output = Result, Box>; type Update = rpc::NoUpdates; } /// RPC method implementation: start a connection on a `TorClient`. async fn client_connect_with_prefs( client: Arc>, method: Box, _ctx: Arc, ) -> Result> { TorClient::connect_with_prefs(client.as_ref(), &method.target, &method.prefs) .await .map_err(|e| Box::new(e) as _) } /// RPC method implementation: perform a remote DNS lookup using a `TorClient`. async fn client_resolve_with_prefs( client: Arc>, method: Box, _ctx: Arc, ) -> Result, Box> { TorClient::resolve_with_prefs(client.as_ref(), &method.hostname, &method.prefs) .await .map_err(|e| Box::new(e) as _) } /// RPC method implementation: perform a remote DNS reverse lookup using a `TorClient`. async fn client_resolve_ptr_with_prefs( client: Arc>, method: Box, _ctx: Arc, ) -> Result, Box> { TorClient::resolve_ptr_with_prefs(client.as_ref(), method.addr, &method.prefs) .await .map_err(|e| Box::new(e) as _) }