//! Connect point types, and the code to parse them and resolve them. use serde::Deserialize; use serde_with::DeserializeFromStr; use std::{ fmt::Debug, net::{self, IpAddr}, path::PathBuf, str::FromStr, }; use tor_config_path::{ CfgPath, CfgPathError, CfgPathResolver, addr::{CfgAddr, CfgAddrError}, }; use tor_general_addr::general::{self, AddrParseError}; #[cfg(feature = "rpc-server")] use tor_rtcompat::{NetStreamListener, NetStreamProvider}; use crate::HasClientErrorAction; /// A connect point, as deserialized from TOML. /// /// Connect points tell an RPC client how to reach an RPC server, /// and tell an RPC server where and how to listen for connections for RPC clients. /// /// This type may have members containing symbolic paths, such as /// `${USER_HOME}` or `${ARTI_LOCAL_STATE}`. /// To convert these paths to a usable format, /// invoke [`ParsedConnectPoint::resolve()`] on this object. #[derive(Clone, Debug)] pub struct ParsedConnectPoint(ConnectPointEnum); /// A connect point, with all paths resolved. /// /// Connect points tell an RPC client how to reach an RPC server, /// and tell an RPC server where and how to listen for connections for RPC clients. /// /// This type is returned by [`ParsedConnectPoint::resolve()`], /// and can be used to connect or bind. #[derive(Clone, Debug)] pub struct ResolvedConnectPoint(pub(crate) ConnectPointEnum); impl ParsedConnectPoint { /// Try to resolve all symbolic paths in this connect point, /// using the rules of [`CfgPath`] and [`CfgAddr`]. pub fn resolve( &self, resolver: &CfgPathResolver, ) -> Result { use ConnectPointEnum as CPE; Ok(ResolvedConnectPoint(match &self.0 { CPE::Connect(connect) => CPE::Connect(connect.resolve(resolver)?), CPE::Builtin(builtin) => CPE::Builtin(builtin.clone()), })) } /// Check whether authenticating this connect point grants superuser permission. pub fn superuser_permission(&self) -> crate::SuperuserPermission { self.0.superuser_permission() } /// Return true if this connect point is an explicit abort. pub fn is_explicit_abort(&self) -> bool { self.0.is_explicit_abort() } } impl ResolvedConnectPoint { /// Check whether authenticating this connect point grants superuser permission. pub fn superuser_permission(&self) -> crate::SuperuserPermission { self.0.superuser_permission() } /// Return true if this connect point is an explicit abort. pub fn is_explicit_abort(&self) -> bool { self.0.is_explicit_abort() } } impl FromStr for ParsedConnectPoint { type Err = ParseError; fn from_str(s: &str) -> Result { let de: ConnectPointDe = toml::from_str(s).map_err(ParseError::InvalidConnectPoint)?; Ok(ParsedConnectPoint(de.try_into()?)) } } /// A failure from [`ParsedConnectPoint::from_str()`]. #[derive(Clone, Debug, thiserror::Error)] #[non_exhaustive] pub enum ParseError { /// The input was not valid toml, or was an invalid connect point. #[error("Invalid connect point")] InvalidConnectPoint(#[source] toml::de::Error), /// The input had sections or members /// that are not allowed to appear in the same connect point. #[error("Conflicting members in connect point")] ConflictingMembers, /// The input was valid toml, but did not have any recognized /// connect point section. #[error("Unrecognized format on connect point")] UnrecognizedFormat, /// An inet-auto address was provided in a connect point /// that was not a loopback address. /// /// (Note that this error is only generated for inet-auto addresses. /// Other non-loopback addresses cause a [`ResolveError::AddressNotLoopback`].) #[error("inet-auto address was not a loopback address")] AutoAddressNotLoopback, } impl HasClientErrorAction for ParseError { fn client_action(&self) -> crate::ClientErrorAction { use crate::ClientErrorAction as A; match self { ParseError::InvalidConnectPoint(_) => A::Abort, ParseError::ConflictingMembers => A::Abort, ParseError::AutoAddressNotLoopback => A::Decline, ParseError::UnrecognizedFormat => A::Decline, } } } /// A failure from [`ParsedConnectPoint::resolve()`]. #[derive(Clone, Debug, thiserror::Error)] #[non_exhaustive] pub enum ResolveError { /// There was a path in the connect point that we couldn't resolve. #[error("Unable to resolve variables in path")] InvalidPath(#[from] CfgPathError), /// There was an address in the connect point that we couldn't parse. #[error("Unable to parse address")] UnparseableAddr(#[from] AddrParseError), /// There was an address in the connect point that we couldn't resolve. #[error("Unable to resolve variables in address")] InvalidAddr(#[from] CfgAddrError), /// After substitution, we couldn't expand the path to a string. #[error("Cannot represent expanded path as string")] PathNotString, /// Address is not a loopback address. #[error("Tried to bind or connect to a non-loopback TCP address")] AddressNotLoopback, /// Authorization mechanism not compatible with address family #[error("Authorization type not compatible with address family")] AuthNotCompatible, /// Authorization mechanism not recognized #[error("Authorization type not recognized as a supported type")] AuthNotRecognized, /// Address type not supported by the RPC connect point subsystem. /// /// (This can only happen if somebody adds new variants to `general::SocketAddr`.) #[error("Address type not recognized")] AddressTypeNotRecognized, /// The address was incompatible with the presence or absence of socket_address_file. #[error("inet-auto without socket_address_file, or vice versa")] AutoIncompatibleWithSocketFile, /// The name of a file or AF_UNIX socket address was a relative path. #[error("Path was not absolute")] PathNotAbsolute, } impl HasClientErrorAction for ResolveError { fn client_action(&self) -> crate::ClientErrorAction { use crate::ClientErrorAction as A; match self { ResolveError::InvalidPath(e) => e.client_action(), ResolveError::UnparseableAddr(e) => e.client_action(), ResolveError::InvalidAddr(e) => e.client_action(), ResolveError::PathNotString => A::Decline, ResolveError::AddressNotLoopback => A::Decline, ResolveError::AuthNotCompatible => A::Abort, ResolveError::AuthNotRecognized => A::Decline, ResolveError::AddressTypeNotRecognized => A::Decline, ResolveError::PathNotAbsolute => A::Abort, ResolveError::AutoIncompatibleWithSocketFile => A::Abort, } } } /// Implementation type for a connect point. /// /// This type is hidden so that the enum fields remain private. /// It is parameterized on a [`Addresses`] trait, /// to indicate whether it is in resolved or unresolved form. #[derive(Clone, Debug)] pub(crate) enum ConnectPointEnum { /// Connect by opening a socket to a [`general::SocketAddr`] Connect(Connect), /// Connect by some built-in mechanism. /// /// (Or, in the case of Abort, do not connect at all.) Builtin(Builtin), } /// Trait to hold types that vary depending on whether a connect point is resolved or not. // // Note: We could use instead separate `PATH` and `ADDR` parameters, // but this approach makes specifying bounds significantly easier. pub(crate) trait Addresses { /// Type to represent addresses that we can open a socket to. type SocketAddr: Clone + std::fmt::Debug; /// Type to represent paths on the filesystem. type Path: Clone + std::fmt::Debug; } /// Representation of a connect point as deserialized. /// /// We could instead deserialize [`ConnectPointEnum`] directly, /// but that would restrict our error-handling: /// the `toml` crate doesn't make it easy to distinguish /// one kind of parse error from another. /// /// TODO We should revisit this choice when we add more variants /// or more auxiliary tables. #[derive(Deserialize, Clone, Debug)] struct ConnectPointDe { /// A "connect" table. connect: Option>, /// A "builtin" table. builtin: Option, } impl TryFrom for ConnectPointEnum { type Error = ParseError; fn try_from(value: ConnectPointDe) -> Result { match value { ConnectPointDe { connect: Some(c), builtin: None, } => Ok(ConnectPointEnum::Connect(c)), ConnectPointDe { connect: None, builtin: Some(b), } => Ok(ConnectPointEnum::Builtin(b)), ConnectPointDe { connect: Some(_), builtin: Some(_), } => Err(ParseError::ConflictingMembers), // This didn't have either recognized section, // so it is likely itn an unrecognized format. _ => Err(ParseError::UnrecognizedFormat), } } } impl ConnectPointEnum { /// Check whether authenticating this connect point grants superuser permission. fn superuser_permission(&self) -> crate::SuperuserPermission { use crate::SuperuserPermission::*; match self { ConnectPointEnum::Connect(connect) => { if connect.superuser { Allowed } else { NotAllowed } } ConnectPointEnum::Builtin(_) => NotAllowed, } } /// Return true if this connect point is an explicit abort. fn is_explicit_abort(&self) -> bool { matches!( self, ConnectPointEnum::Builtin(Builtin { builtin: BuiltinVariant::Abort }) ) } } /// A "builtin" connect point. /// /// This represents an approach to connecting that is handled purely /// within arti. In the future, this might include "embedded" or "owned"; /// but for now, it only includes "abort". #[derive(Deserialize, Clone, Debug)] pub(crate) struct Builtin { /// Actual strategy of built-in behavior to implement. pub(crate) builtin: BuiltinVariant, } /// A particular built-in strategy. #[derive(Deserialize, Clone, Debug)] #[serde(rename_all = "lowercase")] pub(crate) enum BuiltinVariant { /// This connect point must fail, /// and no subsequent connect points may be tried. Abort, } /// Information for a connect point that is implemented by making a socket connection to an address. #[derive(Deserialize, Clone, Debug)] #[serde(bound = "R::Path : Deserialize<'de>, AddrWithStr : Deserialize<'de>")] pub(crate) struct Connect { /// The address of the socket at which the client should try to reach the RPC server, /// and which the RPC server should bind. pub(crate) socket: ConnectAddress, /// The address of the socket which the RPC server believes it is actually listening at. /// /// If absent, defaults to `socket`. /// /// This value is only needs to be different from `socket` /// in cases where cookie authentication is in use, /// and the client is sandboxed somehow (such as behind a NAT, or inside a container). pub(crate) socket_canonical: Option>, /// The authentication that the client should try to use, /// and which the server should require. pub(crate) auth: Auth, /// A file in which the actual value of an `inet-auto` address should be stored. pub(crate) socket_address_file: Option, /// If true, we should allow authenticated connections to this connect point to acquire /// superuser permissions. #[serde(default)] pub(crate) superuser: bool, } /// A target of a [`Connect`] connpt. /// /// Can be either a socket address, or an inet-auto address. #[derive(Deserialize, Clone, Debug)] #[serde(bound = "R::Path : Deserialize<'de>, AddrWithStr : Deserialize<'de>")] #[serde(untagged, expecting = "a network schema and address")] pub(crate) enum ConnectAddress { /// A socket address with an unspecified port. InetAuto(InetAutoAddress), /// A specified socket address. Socket(AddrWithStr), } /// Instructions to bind to an address chosen by the OS. #[derive(Clone, Debug, DeserializeFromStr)] pub(crate) struct InetAutoAddress { /// The address that the relay should bind to, or None if any loopback address is okay. /// /// Must be a loopback address. bind: Option, } impl std::fmt::Display for InetAutoAddress { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self.bind { Some(a) => write!(f, "inet-auto:{a}"), None => write!(f, "inet-auto:auto"), } } } impl FromStr for InetAutoAddress { type Err = ParseError; fn from_str(s: &str) -> Result { let Some(addr_part) = s.strip_prefix("inet-auto:") else { return Err(ParseError::UnrecognizedFormat); }; if addr_part == "auto" { return Ok(InetAutoAddress { bind: None }); } let Ok(addr) = IpAddr::from_str(addr_part) else { return Err(ParseError::UnrecognizedFormat); }; if addr.is_loopback() { Ok(InetAutoAddress { bind: Some(addr) }) } else { Err(ParseError::AutoAddressNotLoopback) } } } impl InetAutoAddress { /// Return a list of addresses to bind to. fn bind_to_addresses(&self) -> Vec { match self { InetAutoAddress { bind: None } => vec![ net::SocketAddr::new(net::Ipv4Addr::LOCALHOST.into(), 0).into(), net::SocketAddr::new(net::Ipv6Addr::LOCALHOST.into(), 0).into(), ], InetAutoAddress { bind: Some(ip) } => { vec![net::SocketAddr::new(*ip, 0).into()] } } } /// Having parsed `addr`, make sure it is a possible instantiation of this address. /// /// Return an error if it is not. #[cfg(feature = "rpc-client")] pub(crate) fn validate_parsed_address( &self, addr: &general::SocketAddr, ) -> Result<(), crate::ConnectError> { use general::SocketAddr::Inet; for sa in self.bind_to_addresses() { if let (Inet(specified), Inet(got)) = (sa, addr) { if specified.port() == 0 && specified.ip() == got.ip() { return Ok(()); } } } Err(crate::ConnectError::SocketAddressFileMismatch) } } /// The representation of an address as written into a socket file. #[derive(Clone, Debug)] #[cfg_attr(feature = "rpc-client", derive(Deserialize))] #[cfg_attr(feature = "rpc-server", derive(serde::Serialize))] pub(crate) struct AddressFile { /// The address to which the server is bound. pub(crate) address: String, } impl ConnectAddress { /// Return true if this is an inet-auto address. fn is_auto(&self) -> bool { matches!(self, ConnectAddress::InetAuto { .. }) } } impl ConnectAddress { /// Expand all variables within this ConnectAddress to their concrete forms. fn resolve( &self, resolver: &CfgPathResolver, ) -> Result, ResolveError> { use ConnectAddress::*; match self { InetAuto(a) => Ok(InetAuto(a.clone())), Socket(s) => Ok(Socket(s.resolve(resolver)?)), } } } impl ConnectAddress { /// Return a list of addresses to bind to. fn bind_to_addresses(&self) -> Vec { use ConnectAddress::*; match self { InetAuto(a) => a.bind_to_addresses(), Socket(a) => vec![a.as_ref().clone()], } } /// Bind a single address from this `ConnectAddress`, /// or return an error if none can be bound. #[cfg(feature = "rpc-server")] pub(crate) async fn bind( &self, runtime: &R, ) -> Result<(R::Listener, String), crate::ConnectError> where R: NetStreamProvider, { use crate::ConnectError; // We don't use any custom options on the listening socket. let listen_options = Default::default(); match self { ConnectAddress::InetAuto(auto) => { let bind_one = async |addr: &general::SocketAddr| -> Result<(R::Listener, String), crate::ConnectError> { let listener = runtime.listen(addr, &listen_options).await?; let local_addr = listener.local_addr()?.try_to_string().ok_or_else(|| ConnectError::Internal("Can't represent auto socket as string!".into()))?; Ok((listener,local_addr)) }; let mut first_error = None; for addr in auto.bind_to_addresses() { match bind_one(&addr).await { Ok(result) => { return Ok(result); } Err(e) => { if first_error.is_none() { first_error = Some(e); } } } } // if we reach here, we only got errors. Err(first_error.unwrap_or_else(|| { ConnectError::Internal("No auto addresses to bind!?".into()) })) } ConnectAddress::Socket(addr) => { let listener = runtime.listen(addr.as_ref(), &listen_options).await?; Ok((listener, addr.as_str().to_owned())) } } } } impl Connect { /// Expand all variables within this `Connect` to their concrete forms. fn resolve(&self, resolver: &CfgPathResolver) -> Result, ResolveError> { let socket = self.socket.resolve(resolver)?; let socket_canonical = self .socket_canonical .as_ref() .map(|sc| sc.resolve(resolver)) .transpose()?; let auth = self.auth.resolve(resolver)?; let socket_address_file = self .socket_address_file .as_ref() .map(|p| p.path(resolver)) .transpose()?; Connect { socket, socket_canonical, auth, socket_address_file, superuser: self.superuser, } .validate() } } impl Connect { /// Return this `Connect` only if its parts are valid and compatible. fn validate(self) -> Result { use general::SocketAddr::{Inet, Unix}; for bind_addr in self.socket.bind_to_addresses() { match (bind_addr, &self.auth) { (Inet(addr), _) if !addr.ip().is_loopback() => { return Err(ResolveError::AddressNotLoopback); } (Inet(_), Auth::None) => return Err(ResolveError::AuthNotCompatible), (_, Auth::Unrecognized(_)) => return Err(ResolveError::AuthNotRecognized), (Inet(_), Auth::Cookie { .. }) => {} (Unix(_), _) => {} (_, _) => return Err(ResolveError::AddressTypeNotRecognized), }; } if self.socket.is_auto() != self.socket_address_file.is_some() { return Err(ResolveError::AutoIncompatibleWithSocketFile); } self.check_absolute_paths()?; Ok(self) } /// Return an error if some path in this `Connect` is not absolute. fn check_absolute_paths(&self) -> Result<(), ResolveError> { for bind_addr in self.socket.bind_to_addresses() { sockaddr_check_absolute(&bind_addr)?; } if let Some(sa) = &self.socket_canonical { sockaddr_check_absolute(sa.as_ref())?; } self.auth.check_absolute_paths()?; if self .socket_address_file .as_ref() .is_some_and(|p| !p.is_absolute()) { return Err(ResolveError::PathNotAbsolute); } Ok(()) } } /// An authentication method for RPC implementations to use, /// along with its related parameters. #[derive(Deserialize, Clone, Debug)] #[serde(rename_all = "lowercase")] pub(crate) enum Auth { /// No authentication is needed or should be expected. None, /// Cookie-based authentication should be used. Cookie { /// Path to the cookie file. path: R::Path, }, /// Unrecognized authentication method. /// /// (Serde will deserialize into this whenever the auth field /// is something unrecognized.) #[serde(untagged)] Unrecognized(toml::Value), } impl Auth { /// Expand all variables within this `Auth` to their concrete forms. fn resolve(&self, resolver: &CfgPathResolver) -> Result, ResolveError> { match self { Auth::None => Ok(Auth::None), Auth::Cookie { path } => Ok(Auth::Cookie { path: path.path(resolver)?, }), Auth::Unrecognized(x) => Ok(Auth::Unrecognized(x.clone())), } } } impl Auth { /// Return an error if any path in `self` is not absolute.. fn check_absolute_paths(&self) -> Result<(), ResolveError> { match self { Auth::None => Ok(()), Auth::Cookie { path } => { if path.is_absolute() { Ok(()) } else { Err(ResolveError::PathNotAbsolute) } } Auth::Unrecognized(_) => Ok(()), } } } /// Type parameters for unresolved connect points // // This derive should be needless, but it permits derive(Clone,Debug) elsewhere. #[derive(Clone, Debug)] struct Unresolved; impl Addresses for Unresolved { type SocketAddr = String; type Path = CfgPath; } /// Type parameters for resolved connect points // // This derive should be needless, but it permits derive(Clone,Debug) elsewhere. #[derive(Clone, Debug)] pub(crate) struct Resolved; impl Addresses for Resolved { type SocketAddr = general::SocketAddr; type Path = PathBuf; } /// Represent an address type along with the string it was decoded from. /// /// We use this type in connect points because, for some kinds of authentication, /// we need the literal input string that created the address. #[derive( Clone, Debug, derive_more::AsRef, serde_with::DeserializeFromStr, serde_with::SerializeDisplay, )] pub(crate) struct AddrWithStr where A: Clone + Debug, { /// The string representation of the address. /// /// For inet addresses, this is the value that appeared in the configuration. /// For unix domain sockets, this is the value that appeared in the configuration, /// after shell expansion. string: String, /// The address itself. #[as_ref] addr: A, } impl AddrWithStr where A: Clone + Debug, { /// Return the string representation of this address, /// for use in the authentication handshake. pub(crate) fn as_str(&self) -> &str { self.string.as_str() } /// Replace the string representation of this address with the one in `other`. pub(crate) fn set_string_from(&mut self, other: &AddrWithStr) { self.string = other.string.clone(); } } impl AddrWithStr { /// Convert an `AddrWithStr` into its substituted form. pub(crate) fn resolve( &self, resolver: &CfgPathResolver, ) -> Result, ResolveError> { let AddrWithStr { string, addr } = self; let addr: CfgAddr = addr.parse()?; let substituted = addr.substitutions_will_apply(); let addr = addr.address(resolver)?; let string = if substituted { addr.try_to_string().ok_or(ResolveError::PathNotString)? } else { string.clone() }; Ok(AddrWithStr { string, addr }) } } impl FromStr for AddrWithStr where A: Clone + Debug + FromStr, { type Err = ::Err; fn from_str(s: &str) -> Result { let addr = s.parse()?; let string = s.to_owned(); Ok(Self { string, addr }) } } impl std::fmt::Display for AddrWithStr where A: Clone + Debug + std::fmt::Display, { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.string) } } /// Return true if `s` is an absolute address. /// /// All IP addresses are considered absolute. fn sockaddr_check_absolute(s: &general::SocketAddr) -> Result<(), ResolveError> { match s { general::SocketAddr::Inet(_) => Ok(()), general::SocketAddr::Unix(sa) => match sa.as_pathname() { Some(p) if !p.is_absolute() => Err(ResolveError::PathNotAbsolute), _ => Ok(()), }, _ => Err(ResolveError::AddressTypeNotRecognized), } } #[cfg(test)] mod test { // @@ begin test lint list maintained by maint/add_warning @@ #![allow(clippy::bool_assert_comparison)] #![allow(clippy::clone_on_copy)] #![allow(clippy::dbg_macro)] #![allow(clippy::mixed_attributes_style)] #![allow(clippy::print_stderr)] #![allow(clippy::print_stdout)] #![allow(clippy::single_char_pattern)] #![allow(clippy::unwrap_used)] #![allow(clippy::unchecked_time_subtraction)] #![allow(clippy::useless_vec)] #![allow(clippy::needless_pass_by_value)] #![allow(clippy::string_slice)] // See arti#2571 //! use super::*; use assert_matches::assert_matches; fn parse(s: &str) -> ParsedConnectPoint { s.parse().unwrap() } #[test] fn examples() { let _e1 = parse( r#" [builtin] builtin = "abort" "#, ); let _e2 = parse( r#" [connect] socket = "unix:/var/run/arti/rpc_socket" auth = "none" "#, ); let _e3 = parse( r#" [connect] socket = "inet:[::1]:9191" socket_canonical = "inet:[::1]:2020" auth = { cookie = { path = "/home/user/.arti_rpc/cookie" } } "#, ); let _e4 = parse( r#" [connect] socket = "inet:[::1]:9191" socket_canonical = "inet:[::1]:2020" [connect.auth.cookie] path = "/home/user/.arti_rpc/cookie" "#, ); } #[test] fn parse_errors() { let r: Result = "not a toml string".parse(); assert_matches!(r, Err(ParseError::InvalidConnectPoint(_))); let r: Result = "[squidcakes]".parse(); assert_matches!(r, Err(ParseError::UnrecognizedFormat)); let r: Result = r#" [builtin] builtin = "abort" [connect] socket = "inet:[::1]:9191" socket_canonical = "inet:[::1]:2020" auth = { cookie = { path = "/home/user/.arti_rpc/cookie" } } "# .parse(); assert_matches!(r, Err(ParseError::ConflictingMembers)); } #[test] fn resolve_errors() { let resolver = CfgPathResolver::default(); let r: ParsedConnectPoint = r#" [connect] socket = "inet:[::1]:9191" socket_canonical = "inet:[::1]:2020" [connect.auth.esp] telekinetic_handshake = 3 "# .parse() .unwrap(); let err = r.resolve(&resolver).err(); assert_matches!(err, Some(ResolveError::AuthNotRecognized)); let r: ParsedConnectPoint = r#" [connect] socket = "inet:[::1]:9191" socket_canonical = "inet:[::1]:2020" auth = "foo" "# .parse() .unwrap(); let err = r.resolve(&resolver).err(); assert_matches!(err, Some(ResolveError::AuthNotRecognized)); } }