Skip to main content

tor_proto/client/
rpc.rs

1//! RPC support for client tor-proto objects.
2
3use std::{collections::HashMap, net::SocketAddr, sync::Arc};
4
5use derive_deftly::Deftly;
6use tor_linkspec::{HasAddrs, HasRelayIds};
7use tor_llcrypto::pk;
8use tor_rpcbase::{self as rpc, SingleIdResponse};
9
10use crate::{
11    ClientTunnel,
12    client::stream::{ClientDataStreamCtrl, ClientStreamCtrl},
13};
14
15/// RPC method that returns the tunnel for a given object.
16///
17/// This is currently implemented for Data streams,
18/// but could in the future be implemented for other types.
19///
20/// # In the Arti RPC System
21///
22/// Return a tunnel associated with a given object.
23///
24/// (A tunnel is a collection of one or more circuits
25/// used to transmit data.)
26///
27/// Gives an error if the object is not associated with a tunnel,
28/// or if the tunnel was closed.
29///
30/// The returned ObjectId is a handle to a `ClientTunnel`.
31/// The caller should drop this ObjectId when they are done with the ClientTunnel.
32#[derive(Debug, serde::Deserialize, Deftly)]
33#[derive_deftly(rpc::DynMethod)]
34#[deftly(rpc(method_name = "arti:get_tunnel"))]
35#[non_exhaustive]
36pub struct GetTunnel {}
37
38impl rpc::RpcMethod for GetTunnel {
39    type Output = rpc::SingleIdResponse;
40    type Update = rpc::NoUpdates;
41}
42
43/// RPC method to describe the path for an object.
44///
45/// This is currently implemented for [`ClientTunnel`],
46/// but could in the future be implemented for other types.
47///
48/// # In the Arti RPC System
49///
50/// Describe the path(s) of a tunnel through the Tor network.
51///
52/// (Because of [Conflux], a tunnel can have multiple paths.
53/// This method describes the members of each one.)
54///
55/// If `include_deprecated_ids` is true,
56/// the output includes deprecated node identity types,
57/// including the RSA identity.
58/// Otherwise, only non-deprecated identities are included.
59///
60/// [Conflux]: https://spec.torproject.org/proposals/329-traffic-splitting.html
61#[derive(Debug, serde::Deserialize, Deftly)]
62#[derive_deftly(rpc::DynMethod)]
63#[deftly(rpc(method_name = "arti:describe_path"))]
64#[non_exhaustive]
65pub struct DescribePath {
66    /// If true, the output will include deprecated node identity types.
67    #[serde(default)]
68    include_deprecated_ids: bool,
69}
70
71impl rpc::RpcMethod for DescribePath {
72    type Output = PathDescription;
73    type Update = rpc::NoUpdates;
74}
75
76/// A RPC-level description for a single entry in a tunnel or circuit path.
77#[derive(serde::Serialize, Clone, Debug)]
78#[non_exhaustive]
79#[serde(rename_all = "snake_case")]
80pub enum PathEntry {
81    /// A hop not corresponding to a known relay.
82    ///
83    /// Typically, this is the join point for a rendezvous circuit for
84    /// communicating with or as an onion service.
85    VirtualHop {},
86
87    /// Return
88    KnownRelay {
89        /// A set of IDs for this Tor relay.
90        ///
91        /// Each ID represents a long-term public key used to identify the relay.
92        /// Deprecated ID types are not included, unless they were specifically requested.
93        ids: RelayIds,
94
95        /// A list of the relay's addresses.
96        addrs: Vec<SocketAddr>,
97    },
98}
99
100/// Serializable container of relay identities.
101///
102/// Differs from [`tor_linkspec::RelayIds`] in is serialize behavior;
103/// See <https://gitlab.torproject.org/tpo/core/arti/-/issues/2477>.
104///
105/// Every relay will have at least one ID.  On the current Tor network
106/// as of April 2026, every relay has an Ed25519 ID.
107/// (This may not always be the case in the future;
108/// for example, if we migrate to ML-DSA keys,
109/// we may eventually retire Ed25519 IDs.)
110#[derive(Clone, Debug, serde::Serialize)]
111pub struct RelayIds {
112    /// Copy of the ed25519 id from the underlying ChanTarget.
113    #[serde(rename = "ed25519", skip_serializing_if = "Option::is_none")]
114    ed_identity: Option<pk::ed25519::Ed25519Identity>,
115
116    /// Copy of the rsa id from the underlying ChanTarget.
117    ///
118    /// This is a deprecated ID type.
119    #[serde(rename = "rsa", skip_serializing_if = "Option::is_none")]
120    rsa_identity: Option<pk::rsa::RsaIdentity>,
121}
122
123/// A description of a tunnel's path.
124///
125/// Note that tunnels are potentially made of multiple circuits, even though
126/// Arti (as of April 2026) does not yet build Conflux tunnels.
127/// Therefore, users should make sure to handle multi-path tunnels here.
128#[derive(serde::Serialize, Clone, Debug)]
129pub struct PathDescription {
130    /// The entries in a given tunnel's path(s).
131    ///
132    /// Within each path, entries are ordered from first (closest to the client)
133    /// to last (farthest from the client).
134    ///
135    /// Since tunnels can have multiple paths, each path is identified with a string.
136    /// The actual content of these strings is not documented,
137    /// but the string identifying each tunnel is stable.
138    ///
139    /// (That is, if you query a tunnel's path description twice,
140    /// each path will have the same identifying string both times.
141    /// If you get a new identifying string,
142    /// it represents a path that was previously not part of the tunnel.)
143    path: HashMap<String, Vec<PathEntry>>,
144}
145
146impl PathEntry {
147    /// Construct a PathEntry from a PathEntry returned by a circuit.
148    fn from_client_entry(
149        detail: &crate::client::circuit::PathEntry,
150        command: &DescribePath,
151    ) -> Self {
152        let Some(owned_chan_target) = detail.as_chan_target() else {
153            return PathEntry::VirtualHop {};
154        };
155
156        let ids = tor_linkspec::RelayIds::from_relay_ids(owned_chan_target);
157        let ids = RelayIds {
158            ed_identity: ids.ed_identity().cloned(),
159            rsa_identity: if command.include_deprecated_ids {
160                ids.rsa_identity().cloned()
161            } else {
162                None
163            },
164        };
165        let addrs = owned_chan_target.addrs().collect();
166        PathEntry::KnownRelay { ids, addrs }
167    }
168}
169
170/// Helper: Return the [`ClientTunnel`] for a [`ClientDataStreamCtrl`],
171/// or an RPC error if the stream isn't attached to a tunnel.
172fn client_stream_tunnel(stream: &ClientDataStreamCtrl) -> Result<Arc<ClientTunnel>, rpc::RpcError> {
173    stream.tunnel().ok_or_else(|| {
174        rpc::RpcError::new(
175            "Stream was not attached to a tunnel".to_string(),
176            rpc::RpcErrorKind::RequestError,
177        )
178    })
179}
180
181/// Helper: Return a [`PathDescription`] for a [`ClientTunnel`]
182fn tunnel_path(tunnel: &ClientTunnel, method: &DescribePath) -> PathDescription {
183    let path = tunnel
184        .tagged_paths()
185        .into_iter()
186        .map(|(id, path)| {
187            let id = id.display_chan_circ().to_string();
188            let path = path
189                .iter()
190                .map(|hop| PathEntry::from_client_entry(hop, method))
191                .collect();
192            (id, path)
193        })
194        .collect();
195    PathDescription { path }
196}
197
198/// Implementation function: implements GetTunnel on ClientDataStreamCtrl.
199async fn client_data_stream_ctrl_get_tunnel(
200    target: Arc<ClientDataStreamCtrl>,
201    _method: Box<GetTunnel>,
202    ctx: Arc<dyn rpc::Context>,
203) -> Result<rpc::SingleIdResponse, rpc::RpcError> {
204    let tunnel: Arc<dyn rpc::Object> = client_stream_tunnel(&target)? as _;
205    let id = ctx.register_owned(tunnel);
206    Ok(SingleIdResponse::from(id))
207}
208
209/// Implementation function: implements DescribePath on a ClientTunnel.
210async fn client_tunnel_describe_path(
211    target: Arc<ClientTunnel>,
212    method: Box<DescribePath>,
213    _ctx: Arc<dyn rpc::Context>,
214) -> Result<PathDescription, rpc::RpcError> {
215    Ok(tunnel_path(&target, &method))
216}
217
218/// Implementation function: implements DescribePath on a ClientDataStreamCtrl.
219async fn client_data_stream_ctrl_describe_path(
220    target: Arc<ClientDataStreamCtrl>,
221    method: Box<DescribePath>,
222    _ctx: Arc<dyn rpc::Context>,
223) -> Result<PathDescription, rpc::RpcError> {
224    let tunnel = client_stream_tunnel(&target)?;
225    Ok(tunnel_path(&tunnel, &method))
226}
227
228rpc::static_rpc_invoke_fn! {
229    client_data_stream_ctrl_get_tunnel;
230    client_tunnel_describe_path;
231    client_data_stream_ctrl_describe_path;
232}