tor-config: Provide misc::Listen

This commit is contained in:
Ian Jackson 2022-08-23 14:19:28 +01:00
parent 00c51bf63f
commit f588268128
3 changed files with 312 additions and 0 deletions

1
Cargo.lock generated
View File

@ -3553,6 +3553,7 @@ dependencies = [
"directories",
"dirs",
"educe",
"either",
"fs-mistrust",
"itertools",
"once_cell",

View File

@ -21,6 +21,7 @@ config = { version = "0.13", default-features = false, features = ["toml"] }
derive_builder = { version = "0.11.2", package = "derive_builder_fork_arti" }
directories = { version = "4", optional = true }
educe = "0.4.6"
either = "1"
fs-mistrust = { path = "../fs-mistrust", version = "0.4.0" }
itertools = "0.10.1"
once_cell = "1"

View File

@ -4,8 +4,13 @@
//! and layers, but which don't depend on specific elements of the Tor system.
use std::borrow::Cow;
use std::iter;
use std::net;
use std::num::NonZeroU16;
use educe::Educe;
use either::Either;
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use strum::{Display, EnumString, IntoStaticStr};
@ -74,6 +79,215 @@ impl TryFrom<PaddingLevelSerde> for PaddingLevel {
}
}
/// Specification of (possibly) something to listen on (eg, a port, or some addresses/ports)
///
/// Can represent, at least:
/// * "do not listen"
/// * Listen on the following port on localhost (IPv6 and IPv4)
/// * Listen on precisely the following address and port
/// * Listen on several addresses/ports
///
/// Currently only IP (v6 and v4) is supported.
#[derive(Clone, Hash, Debug, Ord, PartialOrd, Eq, PartialEq, Serialize, Deserialize)]
#[serde(try_from = "ListenSerde", into = "ListenSerde")]
#[derive(Educe)]
#[educe(Default)]
pub struct Listen(Vec<ListenItem>);
impl Listen {
/// Create a new `Listen` specifying no addresses (no listening)
pub fn new_none() -> Listen {
Listen(vec![])
}
/// Create a new `Listen` specifying listening on a port on localhost
///
/// Special case: if `port` is zero, specifies no listening.
pub fn new_localhost(port: u16) -> Listen {
Listen(port.try_into().ok().map(ListenItem::Localhost).into_iter().collect_vec())
}
/// Create a new `Listen`, possibly specifying listening on a port on localhost
///
/// Special case: if `port` is `Some(0)`, also specifies no listening.
pub fn new_localhost_optional(port: Option<u16>) -> Listen {
Self::new_localhost(port.unwrap_or_default())
}
/// List the network socket addresses to listen on
///
/// Fails if the listen spec involves listening on things other than IP addresses.
/// (Currently that is not possible.)
pub fn ip_addrs(
&self,
) -> Result<impl Iterator<Item = net::SocketAddr> + '_, ListenUnsupported> {
Ok(self.0.iter().flat_map(|i| i.iter()))
}
/// Get the localhost port to listen on
///
/// Returns `None` if listening is configured to be disabled.
///
/// Fails, giving an unsupported error, if the configuration
/// isn't just "listen on a single localhost port".
pub fn localhost_port_deprecated(&self) -> Result<Option<u16>, ListenUnsupported> {
use ListenItem as LI;
Ok(match &*self.0 {
[] => None,
[LI::Localhost(port)] => Some((*port).into()),
_ => return Err(ListenUnsupported {}),
})
}
}
/// [`Listen`] configuration specified something not supported by application code
#[derive(thiserror::Error, Debug, Clone)]
#[non_exhaustive]
#[error("Unsupported listening configration")]
pub struct ListenUnsupported {}
/// One item in the `Listen`
///
/// We distinguish `Localhost`,
/// rather than just storing two `net:SocketAddr`,
/// so that we can handle localhost (which means to address families) specially
/// in order to implement `localhost_port_deprecated()`.
#[derive(Clone, Hash, Debug, Ord, PartialOrd, Eq, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
enum ListenItem {
/// One port, both IPv6 and IPv4
Localhost(NonZeroU16),
/// Any other single socket address
General(net::SocketAddr),
}
impl ListenItem {
/// Return the `SocketAddr`s implied by this item
fn iter(&self) -> impl Iterator<Item = net::SocketAddr> + '_ {
use net::{IpAddr, Ipv4Addr, Ipv6Addr};
use ListenItem as LI;
match self {
&LI::Localhost(port) => Either::Left({
let port = port.into();
let addrs: [IpAddr; 2] = [Ipv6Addr::LOCALHOST.into(), Ipv4Addr::LOCALHOST.into()];
addrs
.into_iter()
.map(move |ip| net::SocketAddr::new(ip, port))
}),
LI::General(addr) => Either::Right(iter::once(addr).cloned()),
}
}
}
/// How we (de) serialize a [`Listen`]
#[derive(Serialize, Deserialize)]
#[serde(untagged)]
enum ListenSerde {
/// for `listen = false` (in TOML syntax)
Bool(bool),
/// A bare item
One(ListenItemSerde),
/// An item in a list
List(Vec<ListenItemSerde>),
}
/// One item in the list of a list-ish `Listen`, or the plain value
#[derive(Serialize, Deserialize)]
#[serde(untagged)]
enum ListenItemSerde {
/// An integer.
///
/// When appearing "loose" (in ListenSerde::One), `0` is parsed as none.
Port(u16),
/// An string which will be parsed as an address and port
///
/// When appearing "loose" (in ListenSerde::One), `""` is parsed as none.
String(String),
}
// This implementation isn't fallible, but clippy thinks it is because of the unwrap.
// The unwrap is just there because we can't pattern-match on a Vec
#[allow(clippy::fallible_impl_from)]
impl From<Listen> for ListenSerde {
fn from(l: Listen) -> ListenSerde {
let l = l.0;
match l.len() {
0 => ListenSerde::Bool(false),
1 => ListenSerde::One(l.into_iter().next().expect("len=1 but no next").into()),
_ => ListenSerde::List(l.into_iter().map(Into::into).collect()),
}
}
}
impl From<ListenItem> for ListenItemSerde {
fn from(i: ListenItem) -> ListenItemSerde {
use ListenItem as LI;
use ListenItemSerde as LIS;
match i {
LI::Localhost(port) => LIS::Port(port.into()),
LI::General(addr) => LIS::String(addr.to_string()),
}
}
}
/// Listen configuration is invalid
#[derive(thiserror::Error, Debug, Clone)]
#[non_exhaustive]
pub enum InvalidListen {
/// Bool was `true` but that's not an address.
#[error("Invalid listen specification: need actual addr/port, or `false`; not `true`")]
InvalidBool,
/// Specified listen was a string but couldn't parse to a [`net::SocketAddr`].
#[error("Invalid listen specification: failed to parse string: {0}")]
InvalidString(#[from] net::AddrParseError),
/// Specified listen was a list containing a zero integer
#[error("Invalid listen specification: zero (for no port) not permitted in list")]
ZeroPortInList,
}
impl TryFrom<ListenSerde> for Listen {
type Error = InvalidListen;
fn try_from(l: ListenSerde) -> Result<Listen, Self::Error> {
use ListenSerde as LS;
Ok(Listen(match l {
LS::Bool(false) => vec![],
LS::Bool(true) => return Err(InvalidListen::InvalidBool),
LS::One(i) if i.means_none() => vec![],
LS::One(i) => vec![i.try_into()?],
LS::List(l) => l.into_iter().map(|i| i.try_into()).try_collect()?,
}))
}
}
impl ListenItemSerde {
/// Is this item actually a sentinel, meaning "don't listen, disable this thing"?
///
/// Allowed only bare, not in a list.
fn means_none(&self) -> bool {
use ListenItemSerde as LIS;
match self {
&LIS::Port(port) => port == 0,
LIS::String(s) => s.is_empty(),
}
}
}
impl TryFrom<ListenItemSerde> for ListenItem {
type Error = InvalidListen;
fn try_from(i: ListenItemSerde) -> Result<ListenItem, Self::Error> {
use ListenItem as LI;
use ListenItemSerde as LIS;
Ok(match i {
LIS::String(s) => LI::General(s.parse()?),
LIS::Port(p) => LI::Localhost(p.try_into().map_err(|_| InvalidListen::ZeroPortInList)?),
})
}
}
#[cfg(test)]
mod test {
// @@ begin test lint list maintained by maint/add_warning @@
@ -90,6 +304,9 @@ mod test {
struct TestConfigFile {
#[serde(default)]
padding: PaddingLevel,
#[serde(default)]
listen: Option<Listen>,
}
#[test]
@ -117,4 +334,97 @@ mod test {
chk_e(r#"padding = "unknown""#);
chk_e(r#"padding = "Normal""#);
}
#[test]
fn listen_parse() {
use net::{Ipv4Addr, Ipv6Addr, SocketAddr};
use ListenItem as LI;
let localhost6 = |p| SocketAddr::new(Ipv6Addr::LOCALHOST.into(), p);
let localhost4 = |p| SocketAddr::new(Ipv4Addr::LOCALHOST.into(), p);
let unspec6 = |p| SocketAddr::new(Ipv6Addr::UNSPECIFIED.into(), p);
#[allow(clippy::needless_pass_by_value)] // we do this for consistency
fn chk(
exp_i: Vec<ListenItem>,
exp_addrs: Result<Vec<SocketAddr>, ()>,
exp_lpd: Result<Option<u16>, ()>,
s: &str,
) {
let tc: TestConfigFile = toml::from_str(s).expect(s);
let ll = tc.listen.unwrap();
eprintln!("s={:?} ll={:?}", &s, &ll);
assert_eq!(ll, Listen(exp_i));
assert_eq!(
ll.ip_addrs().map(|a| a.collect_vec()).map_err(|_| ()),
exp_addrs
);
assert_eq!(ll.localhost_port_deprecated().map_err(|_| ()), exp_lpd);
}
let chk_err = |exp, s: &str| {
let got: Result<TestConfigFile, _> = toml::from_str(s);
let got = got.expect_err(s).to_string();
assert!(got.contains(exp), "s={:?} got={:?} exp={:?}", s, got, exp);
};
let chk_none = |s: &str| {
chk(vec![], Ok(vec![]), Ok(None), &format!("listen = {}", s));
chk_err(
"", /* any error will do */
&format!("listen = [ {} ]", s),
);
};
let chk_1 = |v: ListenItem, addrs: Vec<SocketAddr>, port, s| {
chk(
vec![v.clone()],
Ok(addrs.clone()),
port,
&format!("listen = {}", s),
);
chk(
vec![v.clone()],
Ok(addrs.clone()),
port,
&format!("listen = [ {} ]", s),
);
chk(
vec![v, LI::Localhost(23.try_into().unwrap())],
Ok([addrs, vec![localhost6(23), localhost4(23)]]
.into_iter()
.flatten()
.collect()),
Err(()),
&format!("listen = [ {}, 23 ]", s),
);
};
chk_none(r#""""#);
chk_none(r#"0"#);
chk_none(r#"false"#);
chk(vec![], Ok(vec![]), Ok(None), r#"listen = []"#);
chk_1(
LI::Localhost(42.try_into().unwrap()),
vec![localhost6(42), localhost4(42)],
Ok(Some(42)),
"42",
);
chk_1(
LI::General(unspec6(56)),
vec![unspec6(56)],
Err(()),
r#""[::]:56""#,
);
let chk_err_1 = |e, el, s| {
chk_err(e, &format!("listen = {}", s));
chk_err(el, &format!("listen = [ {} ]", s));
chk_err(el, &format!("listen = [ 23, {}, 77 ]", s));
};
chk_err_1("need actual addr/port", "did not match any variant", "true");
chk_err("did not match any variant", r#"listen = [ [] ]"#);
}
}