arti_rpcserver/mgr.rs
1//! Top-level `RpcMgr` to launch sessions.
2
3use std::sync::{Arc, Mutex, RwLock, Weak};
4
5use rand::Rng;
6use rpc::InvalidRpcIdentifier;
7use tor_rpcbase as rpc;
8use tracing::warn;
9
10use crate::{
11 RpcAuthentication,
12 connection::{Connection, ConnectionId},
13 globalid::{GlobalId, MacKey},
14};
15
16/// Alias to force use of RandomState, regardless of features enabled in `weak_tables`.
17///
18/// See <https://github.com/tov/weak-table-rs/issues/23> for discussion.
19type WeakValueHashMap<K, V> = weak_table::WeakValueHashMap<K, V, std::hash::RandomState>;
20
21/// Shared state, configuration, and data for all RPC sessions.
22///
23/// An RpcMgr knows how to listen for incoming RPC connections, and launch sessions based on them.
24pub struct RpcMgr {
25 /// A key that we use to ensure that identifiers are unforgeable.
26 ///
27 /// When giving out a global (non-session-bound) identifier, we use this key
28 /// to authenticate the identifier when it's given back to us.
29 ///
30 /// We make copies of this key when constructing a session.
31 global_id_mac_key: MacKey,
32
33 /// Our reference to the dispatch table used to look up the functions that
34 /// implement each object on each.
35 ///
36 /// Shared with each [`Connection`].
37 ///
38 /// **NOTE: observe the [Lock hierarchy](crate::mgr::Inner#lock-hierarchy)**
39 dispatch_table: Arc<RwLock<rpc::DispatchTable>>,
40
41 /// Lock-protected view of the manager's state.
42 ///
43 /// **NOTE: observe the [Lock hierarchy](crate::mgr::Inner#lock-hierarchy)**
44 ///
45 /// This mutex is at an _inner_ level
46 /// compared to the
47 /// per-Connection locks.
48 /// You must not take any per-connection lock if you
49 /// hold this lock.
50 /// Code that holds this lock must be checked
51 /// to make sure that it doesn't then acquire any `Connection` lock.
52 inner: Mutex<Inner>,
53}
54
55/// The [`RpcMgr`]'s state. This is kept inside a lock for interior mutability.
56///
57/// # Lock hierarchy
58///
59/// This system has, relevantly to the RPC code, three locks.
60/// In order from outermost (acquire earlier) to innermost (acquire later):
61///
62/// 1. [`Connection`]`.inner`
63/// 2. [`RpcMgr`]`.inner`
64/// 3. `RwLock<rpc::DispatchTable>`
65/// (found in [`RpcMgr`]`.dispatch_table` *and* [`Connection`]`.dispatch_table`)
66///
67/// To avoid deadlock, when more than one of these locks is acquired,
68/// they must be acquired in an order consistent with the order listed above.
69///
70/// (This ordering is slightly surprising:
71/// normally a lock covering more-global state would be
72/// "outside" (or "earlier")
73/// compared to one covering more-narrowly-relevant state.)
74// pub(crate) so we can link to the doc comment and its lock hierarchy
75pub(crate) struct Inner {
76 /// A map from [`ConnectionId`] to weak [`Connection`] references.
77 ///
78 /// We use this map to give connections a manager-global identifier that can
79 /// be used to identify them from a SOCKS connection (or elsewhere outside
80 /// of the RPC system).
81 ///
82 /// We _could_ use a generational arena here, but there isn't any point:
83 /// since these identifiers are global, we need to keep them secure by
84 /// MACing anything derived from them, which in turn makes the overhead of a
85 /// HashMap negligible.
86 connections: WeakValueHashMap<ConnectionId, Weak<Connection>>,
87}
88
89/// An error from creating or using an RpcMgr.
90#[derive(Clone, Debug, thiserror::Error)]
91#[non_exhaustive]
92pub enum RpcMgrError {
93 /// At least one method had an invalid name.
94 #[error("Method {1} had an invalid name")]
95 InvalidMethodName(#[source] InvalidRpcIdentifier, String),
96}
97
98/// An [`rpc::Object`], along with its associated [`rpc::Context`].
99///
100/// The context can be used to invoke any special methods on the object.
101type ObjectWithContext = (Arc<dyn rpc::Context>, Arc<dyn rpc::Object>);
102
103impl RpcMgr {
104 /// Create a new RpcMgr.
105 pub fn new() -> Result<Arc<Self>, RpcMgrError> {
106 let problems = rpc::check_method_names([]);
107 // We warn about every problem.
108 for (m, err) in &problems {
109 warn!("Internal issue: Invalid RPC method name {m:?}: {err}");
110 }
111 let fatal_problem = problems
112 .into_iter()
113 // We don't treat UnrecognizedNamespace as fatal; somebody else might be extending our methods.
114 .find(|(_, err)| !matches!(err, InvalidRpcIdentifier::UnrecognizedNamespace));
115 if let Some((name, err)) = fatal_problem {
116 return Err(RpcMgrError::InvalidMethodName(err, name.to_owned()));
117 }
118
119 Ok(Arc::new(RpcMgr {
120 global_id_mac_key: MacKey::new(&mut rand::rng()),
121 dispatch_table: Arc::new(RwLock::new(rpc::DispatchTable::from_inventory())),
122 inner: Mutex::new(Inner {
123 connections: WeakValueHashMap::new(),
124 }),
125 }))
126 }
127
128 /// Extend our method dispatch table with the method entries in `entries`.
129 ///
130 /// Ignores any entries that
131 ///
132 /// # Panics
133 ///
134 /// Panics if any entries are conflicting, according to the logic of
135 /// [`DispatchTable::insert`](rpc::DispatchTable::insert)
136 pub fn register_rpc_methods<I>(&self, entries: I)
137 where
138 I: IntoIterator<Item = rpc::dispatch::InvokerEnt>,
139 {
140 // TODO: Conceivably we might want to get a read lock on the RPC dispatch table,
141 // check for the presence of these entries, and only take the write lock
142 // if the entries are absent. But for now, this function is called during
143 // RpcMgr initialization, so there's no reason to optimize it.
144 self.with_dispatch_table(|table| table.extend(entries));
145 }
146
147 /// Run `func` with a mutable reference to our dispatch table as an argument.
148 ///
149 /// Used to register additional methods.
150 pub fn with_dispatch_table<F, T>(&self, func: F) -> T
151 where
152 F: FnOnce(&mut rpc::DispatchTable) -> T,
153 {
154 let mut table = self.dispatch_table.write().expect("poisoned lock");
155 func(&mut table)
156 }
157
158 /// Start a new session based on this RpcMgr, with a given TorClient.
159 pub fn new_connection<F>(
160 self: &Arc<Self>,
161 require_auth: tor_rpc_connect::auth::RpcAuth,
162 create_session: F,
163 ) -> Arc<Connection>
164 where
165 F: Fn(&RpcAuthentication) -> Arc<dyn rpc::Object> + Send + Sync + 'static,
166 {
167 let connection_id = ConnectionId::from(rand::rng().random::<[u8; 16]>());
168 let connection = Connection::new(
169 connection_id,
170 self.dispatch_table.clone(),
171 self.global_id_mac_key.clone(),
172 require_auth,
173 Box::new(create_session) as _,
174 );
175
176 let mut inner = self.inner.lock().expect("poisoned lock");
177 let old = inner.connections.insert(connection_id, connection.clone());
178 assert!(
179 old.is_none(),
180 // Specifically, we shouldn't expect collisions until we have made on the
181 // order of 2^64 connections, and that shouldn't be possible on
182 // realistic systems.
183 "connection ID collision detected; this is phenomenally unlikely!",
184 );
185 connection
186 }
187
188 /// Look up an object in the context of this `RpcMgr`.
189 ///
190 /// Some object identifiers exist in a manager-global context, so that they
191 /// can be used outside of a single RPC session. This function looks up an
192 /// object by such an identifier string. It returns an error if the
193 /// identifier is invalid or the object does not exist.
194 ///
195 /// Along with the object, this additionally returns the [`rpc::Context`] associated with the
196 /// object. That context can be used to invoke any special methods on the object.
197 pub fn lookup_object(&self, id: &rpc::ObjectId) -> Result<ObjectWithContext, rpc::LookupError> {
198 GlobalId::try_decode(&self.global_id_mac_key, id)?
199 .and_then(|global_id| self.lookup_by_global_id(&global_id))
200 .ok_or_else(|| rpc::LookupError::NoObject(id.clone()))
201 }
202
203 /// As `lookup_object`, but takes a parsed and validated [`GlobalId`].
204 pub(crate) fn lookup_by_global_id(&self, id: &GlobalId) -> Option<ObjectWithContext> {
205 let connection = {
206 let inner = self.inner.lock().expect("lock poisoned");
207 let connection = inner.connections.get(&id.connection)?;
208 // Here we release the lock on self.inner, which makes it okay to
209 // invoke a method on `connection` that may take its lock.
210 drop(inner);
211 connection
212 };
213 let obj = connection.lookup_by_idx(id.local_id).ok()?;
214 Some((connection, obj))
215 }
216}