tor_rpc_connect/
connpt.rs

1//! Connect point types, and the code to parse them and resolve them.
2
3use serde::Deserialize;
4use std::{fmt::Debug, path::PathBuf, str::FromStr};
5use tor_config_path::{
6    addr::{CfgAddr, CfgAddrError},
7    CfgPath, CfgPathError, CfgPathResolver,
8};
9use tor_general_addr::general;
10
11use crate::HasClientErrorAction;
12
13/// A connect point, as deserialized from TOML.
14///
15/// Connect points tell an RPC client how to reach an RPC server,
16/// and tell an RPC server where and how to listen for connections for RPC clients.
17///
18/// This type may have members containing symbolic paths, such as
19/// `${USER_HOME}` or `${ARTI_LOCAL_STATE}`.
20/// To convert these paths to a usable format,
21/// invoke [`ParsedConnectPoint::resolve()`] on this object.
22#[derive(Clone, Debug)]
23pub struct ParsedConnectPoint(ConnectPointEnum<Unresolved>);
24
25/// A connect point, with all paths resolved.
26///
27/// Connect points tell an RPC client how to reach an RPC server,
28/// and tell an RPC server where and how to listen for connections for RPC clients.
29///
30/// This type is returned by [`ParsedConnectPoint::resolve()`],
31/// and can be used to connect or bind.
32#[derive(Clone, Debug)]
33pub struct ResolvedConnectPoint(pub(crate) ConnectPointEnum<Resolved>);
34
35impl ParsedConnectPoint {
36    /// Try to resolve all symbolic paths in this connect point,
37    /// using the rules of [`CfgPath`] and [`CfgAddr`].
38    pub fn resolve(
39        &self,
40        resolver: &CfgPathResolver,
41    ) -> Result<ResolvedConnectPoint, ResolveError> {
42        use ConnectPointEnum as CPE;
43        Ok(ResolvedConnectPoint(match &self.0 {
44            CPE::Connect(connect) => CPE::Connect(connect.resolve(resolver)?),
45            CPE::Builtin(builtin) => CPE::Builtin(builtin.clone()),
46        }))
47    }
48}
49
50impl FromStr for ParsedConnectPoint {
51    type Err = ParseError;
52
53    fn from_str(s: &str) -> Result<Self, Self::Err> {
54        let de: ConnectPointDe = toml::from_str(s).map_err(ParseError::InvalidConnectPoint)?;
55        Ok(ParsedConnectPoint(de.try_into()?))
56    }
57}
58
59/// A failure from [`ParsedConnectPoint::from_str()`].
60#[derive(Clone, Debug, thiserror::Error)]
61#[non_exhaustive]
62pub enum ParseError {
63    /// The input was not valid toml, or was an invalid connect point.
64    #[error("Invalid connect point")]
65    InvalidConnectPoint(#[source] toml::de::Error),
66    /// The input had sections or members
67    /// that are not allowed to appear in the same connect point.
68    #[error("Conflicting members in connect point")]
69    ConflictingMembers,
70    /// The input was valid toml, but did not have any recognized
71    /// connect point section.
72    #[error("Unrecognized format on connect point")]
73    UnrecognizedFormat,
74}
75impl HasClientErrorAction for ParseError {
76    fn client_action(&self) -> crate::ClientErrorAction {
77        use crate::ClientErrorAction as A;
78        match self {
79            ParseError::InvalidConnectPoint(_) => A::Abort,
80            ParseError::ConflictingMembers => A::Abort,
81            ParseError::UnrecognizedFormat => A::Decline,
82        }
83    }
84}
85
86/// A failure from [`ParsedConnectPoint::resolve()`].
87#[derive(Clone, Debug, thiserror::Error)]
88#[non_exhaustive]
89pub enum ResolveError {
90    /// There was a path in the connect point that we couldn't resolve.
91    #[error("Unable to resolve variables in path")]
92    InvalidPath(#[from] CfgPathError),
93    /// There was an address in the connect point that we couldn't resolve.
94    #[error("Unable to resolve variables in address")]
95    InvalidAddr(#[from] CfgAddrError),
96    /// After substitution, we couldn't expand the path to a string.
97    #[error("Cannot represent expanded path as string")]
98    PathNotString,
99    /// Address is not a loopback address.
100    #[error("Tried to bind or connect to a non-loopback TCP address")]
101    AddressNotLoopback,
102    /// Authorization mechanism not compatible with address family
103    #[error("Authorization type not compatible with address family")]
104    AuthNotCompatible,
105    /// Authorization mechanism not recognized
106    #[error("Authorization type not recognized as a supported type")]
107    AuthNotRecognized,
108    /// Address type not supported by the RPC connect point subsystem.
109    ///
110    /// (This can only happen if somebody adds new variants to `general::SocketAddr`.)
111    #[error("Address type not recognized")]
112    AddressTypeNotRecognized,
113    /// The name of a file or AF_UNIX socket address was a relative path.
114    #[error("Path was not absolute")]
115    PathNotAbsolute,
116}
117impl HasClientErrorAction for ResolveError {
118    fn client_action(&self) -> crate::ClientErrorAction {
119        use crate::ClientErrorAction as A;
120        match self {
121            ResolveError::InvalidPath(e) => e.client_action(),
122            ResolveError::InvalidAddr(e) => e.client_action(),
123            ResolveError::PathNotString => A::Decline,
124            ResolveError::AddressNotLoopback => A::Decline,
125            ResolveError::AuthNotCompatible => A::Abort,
126            ResolveError::AuthNotRecognized => A::Decline,
127            ResolveError::AddressTypeNotRecognized => A::Decline,
128            ResolveError::PathNotAbsolute => A::Abort,
129        }
130    }
131}
132
133/// Implementation type for a connect point.
134///
135/// This type is hidden so that the enum fields remain private.
136/// It is parameterized on a [`Addresses`] trait,
137/// to indicate whether it is in resolved or unresolved form.
138#[derive(Clone, Debug)]
139pub(crate) enum ConnectPointEnum<R: Addresses> {
140    /// Connect by opening a socket to a [`general::SocketAddr`]
141    Connect(Connect<R>),
142    /// Connect by some built-in mechanism.
143    ///
144    /// (Or, in the case of Abort, do not connect at all.)
145    Builtin(Builtin),
146}
147
148/// Trait to hold types that vary depending on whether a connect point is resolved or not.
149//
150// Note: We could use instead separate `PATH` and `ADDR` parameters,
151// but this approach makes specifying bounds significantly easier.
152pub(crate) trait Addresses {
153    /// Type to represent addresses that we can open a socket to.
154    type SocketAddr: Clone + std::fmt::Debug;
155    /// Type to represent paths on the filesystem.
156    type Path: Clone + std::fmt::Debug;
157}
158
159/// Representation of a connect point as deserialized.
160///
161/// We could instead deserialize [`ConnectPointEnum`] directly,
162/// but that would restrict our error-handling:
163/// the `toml` crate doesn't make it easy to distinguish
164/// one kind of parse error from another.
165///
166/// TODO We should revisit this choice when we add more variants
167/// or more auxiliary tables.
168#[derive(Deserialize, Clone, Debug)]
169struct ConnectPointDe {
170    /// A "connect" table.
171    connect: Option<Connect<Unresolved>>,
172    /// A "builtin" table.
173    builtin: Option<Builtin>,
174}
175impl TryFrom<ConnectPointDe> for ConnectPointEnum<Unresolved> {
176    type Error = ParseError;
177
178    fn try_from(value: ConnectPointDe) -> Result<Self, Self::Error> {
179        match value {
180            ConnectPointDe {
181                connect: Some(c),
182                builtin: None,
183            } => Ok(ConnectPointEnum::Connect(c)),
184            ConnectPointDe {
185                connect: None,
186                builtin: Some(b),
187            } => Ok(ConnectPointEnum::Builtin(b)),
188            ConnectPointDe {
189                connect: Some(_),
190                builtin: Some(_),
191            } => Err(ParseError::ConflictingMembers),
192            // This didn't have either recognized section,
193            // so it is likely itn an unrecognized format.
194            _ => Err(ParseError::UnrecognizedFormat),
195        }
196    }
197}
198
199/// A "builtin" connect point.
200///
201/// This represents an approach to connecting that is handled purely
202/// within arti.  In the future, this might include "embedded" or "owned";
203/// but for now, it only includes "abort".
204#[derive(Deserialize, Clone, Debug)]
205pub(crate) struct Builtin {
206    /// Actual strategy of built-in behavior to implement.
207    pub(crate) builtin: BuiltinVariant,
208}
209
210/// A particular built-in strategy.
211#[derive(Deserialize, Clone, Debug)]
212#[serde(rename_all = "lowercase")]
213pub(crate) enum BuiltinVariant {
214    /// This connect point must fail,
215    /// and no subsequent connect points may be tried.
216    Abort,
217}
218
219#[derive(Deserialize, Clone, Debug)]
220#[serde(bound = "R::Path : Deserialize<'de>, AddrWithStr<R::SocketAddr> : Deserialize<'de>")]
221pub(crate) struct Connect<R: Addresses> {
222    /// The address of the socket at which the client should try to reach the RPC server,
223    /// and which the RPC server should bind.
224    pub(crate) socket: AddrWithStr<R::SocketAddr>,
225    /// The address of the socket which the RPC server believes it is actually listening at.
226    ///
227    /// If absent, defaults to `socket`.
228    ///
229    /// This value is only needs to be different from `socket`
230    /// in cases where cookie authentication is in use,
231    /// and the client is sandboxed somehow (such as behind a NAT, or inside a container).
232    pub(crate) socket_canonical: Option<AddrWithStr<R::SocketAddr>>,
233    /// The authentication that the client should try to use,
234    /// and which the server should require.
235    pub(crate) auth: Auth<R>,
236}
237
238impl Connect<Unresolved> {
239    /// Convert all symbolic paths within this Connect to their resolved forms.
240    fn resolve(&self, resolver: &CfgPathResolver) -> Result<Connect<Resolved>, ResolveError> {
241        let socket = self.socket.resolve(resolver)?;
242        let socket_canonical = self
243            .socket_canonical
244            .as_ref()
245            .map(|sc| sc.resolve(resolver))
246            .transpose()?;
247        let auth = self.auth.resolve(resolver)?;
248        Connect {
249            socket,
250            socket_canonical,
251            auth,
252        }
253        .validate()
254    }
255}
256
257impl Connect<Resolved> {
258    /// Return this `Connect` only if its parts are valid and compatible.
259    fn validate(self) -> Result<Self, ResolveError> {
260        use general::SocketAddr::{Inet, Unix};
261        match (self.socket.as_ref(), &self.auth) {
262            (Inet(addr), _) if !addr.ip().is_loopback() => {
263                return Err(ResolveError::AddressNotLoopback)
264            }
265            (Inet(_), Auth::None) => return Err(ResolveError::AuthNotCompatible),
266            (_, Auth::Unrecognized(_)) => return Err(ResolveError::AuthNotRecognized),
267            (Inet(_), Auth::Cookie { .. }) => {}
268            (Unix(_), _) => {}
269            (_, _) => return Err(ResolveError::AddressTypeNotRecognized),
270        };
271        self.check_absolute_paths()?;
272        Ok(self)
273    }
274
275    /// Return an error if some path in this `Connect` is not absolute.
276    fn check_absolute_paths(&self) -> Result<(), ResolveError> {
277        sockaddr_check_absolute(self.socket.as_ref())?;
278        if let Some(sa) = &self.socket_canonical {
279            sockaddr_check_absolute(sa.as_ref())?;
280        }
281        self.auth.check_absolute_paths()?;
282        Ok(())
283    }
284}
285
286/// An authentication method for RPC implementations to use,
287/// along with its related parameters.
288#[derive(Deserialize, Clone, Debug)]
289#[serde(rename_all = "lowercase")]
290pub(crate) enum Auth<R: Addresses> {
291    /// No authentication is needed or should be expected.
292    None,
293    /// Cookie-based authentication should be used.
294    Cookie {
295        /// Path to the cookie file.
296        path: R::Path,
297    },
298    /// Unrecognized authentication method.
299    ///
300    /// (Serde will deserialize into this whenever the auth field
301    /// is something unrecognized.)
302    #[serde(untagged)]
303    Unrecognized(toml::Value),
304}
305
306impl Auth<Unresolved> {
307    /// Convert all symbolic paths within this `Auth` to their resolved forms.
308    fn resolve(&self, resolver: &CfgPathResolver) -> Result<Auth<Resolved>, ResolveError> {
309        match self {
310            Auth::None => Ok(Auth::None),
311            Auth::Cookie { path } => Ok(Auth::Cookie {
312                path: path.path(resolver)?,
313            }),
314            Auth::Unrecognized(x) => Ok(Auth::Unrecognized(x.clone())),
315        }
316    }
317}
318
319impl Auth<Resolved> {
320    /// Return an error if any path in `self` is not absolute..
321    fn check_absolute_paths(&self) -> Result<(), ResolveError> {
322        match self {
323            Auth::None => Ok(()),
324            Auth::Cookie { path } => {
325                if path.is_absolute() {
326                    Ok(())
327                } else {
328                    Err(ResolveError::PathNotAbsolute)
329                }
330            }
331            Auth::Unrecognized(_) => Ok(()),
332        }
333    }
334}
335
336/// Type parameters for unresolved connect points
337//
338// This derive should be needless, but it permits derive(Clone,Debug) elsewhere.
339#[derive(Clone, Debug)]
340struct Unresolved;
341impl Addresses for Unresolved {
342    type SocketAddr = CfgAddr;
343    type Path = CfgPath;
344}
345
346/// Type parameters for resolved connect points
347//
348// This derive should be needless, but it permits derive(Clone,Debug) elsewhere.
349#[derive(Clone, Debug)]
350pub(crate) struct Resolved;
351impl Addresses for Resolved {
352    type SocketAddr = general::SocketAddr;
353    type Path = PathBuf;
354}
355
356/// Represent an address type along with the string it was decoded from.
357///
358/// We use this type in connect points because, for some kinds of authentication,
359/// we need the literal input string that created the address.
360#[derive(Clone, Debug, derive_more::AsRef, serde_with::DeserializeFromStr)]
361pub(crate) struct AddrWithStr<A>
362where
363    A: Clone + Debug,
364{
365    /// The string representation of the address.
366    ///
367    /// For inet addresses, this is the value that appeared in the configuration.
368    /// For unix domain sockets, this is the value that appeared in the configuration,
369    /// after shell expansion.
370    string: String,
371    /// The address itself.
372    #[as_ref]
373    addr: A,
374}
375impl<A> AddrWithStr<A>
376where
377    A: Clone + Debug,
378{
379    /// Return the string representation of this address,
380    /// for use in the authentication handshake.
381    pub(crate) fn as_str(&self) -> &str {
382        self.string.as_str()
383    }
384}
385impl AddrWithStr<CfgAddr> {
386    /// Convert an `AddrWithStr<CfgAddr>` into its substituted form.
387    pub(crate) fn resolve(
388        &self,
389        resolver: &CfgPathResolver,
390    ) -> Result<AddrWithStr<general::SocketAddr>, ResolveError> {
391        let AddrWithStr { string, addr } = self;
392        let substituted = addr.substitutions_will_apply();
393        let addr = addr.address(resolver)?;
394        let string = if substituted {
395            addr.try_to_string().ok_or(ResolveError::PathNotString)?
396        } else {
397            string.clone()
398        };
399        Ok(AddrWithStr { string, addr })
400    }
401}
402impl<A> FromStr for AddrWithStr<A>
403where
404    A: Clone + Debug + FromStr,
405{
406    type Err = <A as FromStr>::Err;
407
408    fn from_str(s: &str) -> Result<Self, Self::Err> {
409        let addr = s.parse()?;
410        let string = s.to_owned();
411        Ok(Self { string, addr })
412    }
413}
414
415/// Return true if `s` is an absolute address.
416///
417/// All IP addresses are considered absolute.
418fn sockaddr_check_absolute(s: &general::SocketAddr) -> Result<(), ResolveError> {
419    match s {
420        general::SocketAddr::Inet(_) => Ok(()),
421        general::SocketAddr::Unix(sa) => match sa.as_pathname() {
422            Some(p) if !p.is_absolute() => Err(ResolveError::PathNotAbsolute),
423            _ => Ok(()),
424        },
425        _ => Err(ResolveError::AddressTypeNotRecognized),
426    }
427}
428
429#[cfg(test)]
430mod test {
431    // @@ begin test lint list maintained by maint/add_warning @@
432    #![allow(clippy::bool_assert_comparison)]
433    #![allow(clippy::clone_on_copy)]
434    #![allow(clippy::dbg_macro)]
435    #![allow(clippy::mixed_attributes_style)]
436    #![allow(clippy::print_stderr)]
437    #![allow(clippy::print_stdout)]
438    #![allow(clippy::single_char_pattern)]
439    #![allow(clippy::unwrap_used)]
440    #![allow(clippy::unchecked_duration_subtraction)]
441    #![allow(clippy::useless_vec)]
442    #![allow(clippy::needless_pass_by_value)]
443    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
444
445    use super::*;
446    use assert_matches::assert_matches;
447
448    fn parse(s: &str) -> ParsedConnectPoint {
449        s.parse().unwrap()
450    }
451
452    #[test]
453    fn examples() {
454        let _e1 = parse(
455            r#"
456[builtin]
457builtin = "abort"
458"#,
459        );
460
461        let _e2 = parse(
462            r#"
463[connect]
464socket = "unix:/var/run/arti/rpc_socket"
465auth = "none"
466"#,
467        );
468
469        let _e3 = parse(
470            r#"
471[connect]
472socket = "inet:[::1]:9191"
473socket_canonical = "inet:[::1]:2020"
474
475auth = { cookie = { path = "/home/user/.arti_rpc/cookie" } }
476"#,
477        );
478
479        let _e4 = parse(
480            r#"
481[connect]
482socket = "inet:[::1]:9191"
483socket_canonical = "inet:[::1]:2020"
484
485[connect.auth.cookie]
486path = "/home/user/.arti_rpc/cookie"
487"#,
488        );
489    }
490
491    #[test]
492    fn parse_errors() {
493        let r: Result<ParsedConnectPoint, _> = "not a toml string".parse();
494        assert_matches!(r, Err(ParseError::InvalidConnectPoint(_)));
495
496        let r: Result<ParsedConnectPoint, _> = "[squidcakes]".parse();
497        assert_matches!(r, Err(ParseError::UnrecognizedFormat));
498
499        let r: Result<ParsedConnectPoint, _> = r#"
500[builtin]
501builtin = "abort"
502
503[connect]
504socket = "inet:[::1]:9191"
505socket_canonical = "inet:[::1]:2020"
506
507auth = { cookie = { path = "/home/user/.arti_rpc/cookie" } }
508"#
509        .parse();
510        assert_matches!(r, Err(ParseError::ConflictingMembers));
511    }
512
513    #[test]
514    fn resolve_errors() {
515        let resolver = CfgPathResolver::default();
516
517        let r: ParsedConnectPoint = r#"
518[connect]
519socket = "inet:[::1]:9191"
520socket_canonical = "inet:[::1]:2020"
521
522[connect.auth.esp]
523telekinetic_handshake = 3
524"#
525        .parse()
526        .unwrap();
527        let err = r.resolve(&resolver).err();
528        assert_matches!(err, Some(ResolveError::AuthNotRecognized));
529
530        let r: ParsedConnectPoint = r#"
531[connect]
532socket = "inet:[::1]:9191"
533socket_canonical = "inet:[::1]:2020"
534
535auth = "foo"
536"#
537        .parse()
538        .unwrap();
539        let err = r.resolve(&resolver).err();
540        assert_matches!(err, Some(ResolveError::AuthNotRecognized));
541    }
542}