tor-config: Provide misc::Listen
This commit is contained in:
parent
00c51bf63f
commit
f588268128
|
@ -3553,6 +3553,7 @@ dependencies = [
|
||||||
"directories",
|
"directories",
|
||||||
"dirs",
|
"dirs",
|
||||||
"educe",
|
"educe",
|
||||||
|
"either",
|
||||||
"fs-mistrust",
|
"fs-mistrust",
|
||||||
"itertools",
|
"itertools",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
|
|
@ -21,6 +21,7 @@ config = { version = "0.13", default-features = false, features = ["toml"] }
|
||||||
derive_builder = { version = "0.11.2", package = "derive_builder_fork_arti" }
|
derive_builder = { version = "0.11.2", package = "derive_builder_fork_arti" }
|
||||||
directories = { version = "4", optional = true }
|
directories = { version = "4", optional = true }
|
||||||
educe = "0.4.6"
|
educe = "0.4.6"
|
||||||
|
either = "1"
|
||||||
fs-mistrust = { path = "../fs-mistrust", version = "0.4.0" }
|
fs-mistrust = { path = "../fs-mistrust", version = "0.4.0" }
|
||||||
itertools = "0.10.1"
|
itertools = "0.10.1"
|
||||||
once_cell = "1"
|
once_cell = "1"
|
||||||
|
|
|
@ -4,8 +4,13 @@
|
||||||
//! and layers, but which don't depend on specific elements of the Tor system.
|
//! and layers, but which don't depend on specific elements of the Tor system.
|
||||||
|
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
use std::iter;
|
||||||
|
use std::net;
|
||||||
|
use std::num::NonZeroU16;
|
||||||
|
|
||||||
use educe::Educe;
|
use educe::Educe;
|
||||||
|
use either::Either;
|
||||||
|
use itertools::Itertools;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use strum::{Display, EnumString, IntoStaticStr};
|
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)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
// @@ begin test lint list maintained by maint/add_warning @@
|
// @@ begin test lint list maintained by maint/add_warning @@
|
||||||
|
@ -90,6 +304,9 @@ mod test {
|
||||||
struct TestConfigFile {
|
struct TestConfigFile {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
padding: PaddingLevel,
|
padding: PaddingLevel,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
listen: Option<Listen>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -117,4 +334,97 @@ mod test {
|
||||||
chk_e(r#"padding = "unknown""#);
|
chk_e(r#"padding = "unknown""#);
|
||||||
chk_e(r#"padding = "Normal""#);
|
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 = [ [] ]"#);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue