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