tor_config/
listen.rs

1//! Configuration for ports and addresses to listen on.
2
3use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
4use std::{fmt::Display, iter, num::NonZeroU16};
5
6use either::Either;
7use itertools::Itertools as _;
8use serde::{Deserialize, Serialize};
9
10/// Specification of (possibly) something to listen on (eg, a port, or some addresses/ports)
11///
12/// Can represent, at least:
13///  * "do not listen"
14///  * Listen on the following port on localhost (IPv6 and IPv4)
15///  * Listen on precisely the following address and port
16///  * Listen on several addresses/ports
17///
18/// Currently only IP (v6 and v4) is supported.
19#[derive(Clone, Hash, Debug, Ord, PartialOrd, Eq, PartialEq, Serialize, Deserialize)]
20#[serde(try_from = "ListenSerde", into = "ListenSerde")]
21#[derive(Default)]
22pub struct Listen(Vec<ListenItem>);
23
24impl Listen {
25    /// Create a new `Listen` specifying no addresses (no listening)
26    pub fn new_none() -> Listen {
27        Listen(vec![])
28    }
29
30    /// Create a new `Listen` specifying listening on a port on localhost
31    ///
32    /// Special case: if `port` is zero, specifies no listening.
33    pub fn new_localhost(port: u16) -> Listen {
34        Listen(
35            port.try_into()
36                .ok()
37                .map(ListenItem::Localhost)
38                .into_iter()
39                .collect_vec(),
40        )
41    }
42
43    /// Create a new `Listen`, possibly specifying listening on a port on localhost
44    ///
45    /// Special case: if `port` is `Some(0)`, also specifies no listening.
46    pub fn new_localhost_optional(port: Option<u16>) -> Listen {
47        Self::new_localhost(port.unwrap_or_default())
48    }
49
50    /// Return true if no listening addresses have been configured
51    pub fn is_empty(&self) -> bool {
52        self.0.is_empty()
53    }
54
55    /// List the network socket addresses to listen on
56    ///
57    /// Each returned item is a list of `SocketAddr`,
58    /// of which *at least one* must be successfully bound.
59    /// It is OK if the others (up to all but one of them)
60    /// fail with `EAFNOSUPPORT` ("Address family not supported").
61    /// This allows handling of support, or non-support,
62    /// for particular address families, eg IPv6 vs IPv4 localhost.
63    /// Other errors (eg, `EADDRINUSE`) should always be treated as serious problems.
64    ///
65    /// Fails if the listen spec involves listening on things other than IP addresses.
66    /// (Currently that is not possible.)
67    pub fn ip_addrs(
68        &self,
69    ) -> Result<impl Iterator<Item = impl Iterator<Item = SocketAddr> + '_> + '_, ListenUnsupported>
70    {
71        Ok(self.0.iter().map(|i| i.iter()))
72    }
73
74    /// Get the localhost port to listen on
75    ///
76    /// Returns `None` if listening is configured to be disabled.
77    ///
78    /// Fails, giving an unsupported error, if the configuration
79    /// isn't just "listen on a single localhost port in all address families"
80    pub fn localhost_port_legacy(&self) -> Result<Option<u16>, ListenUnsupported> {
81        use ListenItem as LI;
82        Ok(match &*self.0 {
83            [] => None,
84            [LI::Localhost(port)] => Some((*port).into()),
85            _ => return Err(ListenUnsupported {}),
86        })
87    }
88
89    /// Get a single address to listen on
90    ///
91    /// Returns `None` if listening is configured to be disabled.
92    ///
93    /// If the configuration is "listen on a single port",
94    /// treats this as a request to listening on IPv4 only.
95    /// Use of this function implies a bug:
96    /// lack of proper support for the current internet protocol IPv6.
97    /// It should only be used if an underlying library or facility is likewise buggy.
98    ///
99    /// Fails, giving an unsupported error, if the configuration
100    /// isn't just "listen on a single port on one address family".
101    pub fn single_address_legacy(&self) -> Result<Option<SocketAddr>, ListenUnsupported> {
102        use ListenItem as LI;
103        Ok(match &*self.0 {
104            [] => None,
105            [LI::Localhost(port)] => Some((Ipv4Addr::LOCALHOST, u16::from(*port)).into()),
106            [LI::General(sa)] => Some(*sa),
107            _ => return Err(ListenUnsupported {}),
108        })
109    }
110
111    /// Return true if this `Listen` only configures listening on localhost.
112    pub fn is_localhost_only(&self) -> bool {
113        self.0.iter().all(ListenItem::is_localhost)
114    }
115}
116
117impl Display for Listen {
118    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
119        let mut sep = "";
120        for a in &self.0 {
121            write!(f, "{sep}{a}")?;
122            sep = ", ";
123        }
124        Ok(())
125    }
126}
127/// [`Listen`] configuration specified something not supported by application code
128#[derive(thiserror::Error, Debug, Clone)]
129#[non_exhaustive]
130#[error("Unsupported listening configuration")]
131pub struct ListenUnsupported {}
132
133/// One item in the `Listen`
134///
135/// We distinguish `Localhost`,
136/// rather than just storing two `net:SocketAddr`,
137/// so that we can handle localhost (which means two address families) specially
138/// in order to implement `localhost_port_legacy()`.
139#[derive(Clone, Hash, Debug, Ord, PartialOrd, Eq, PartialEq, Serialize, Deserialize)]
140#[serde(untagged)]
141enum ListenItem {
142    /// One port, both IPv6 and IPv4
143    Localhost(NonZeroU16),
144
145    /// Any other single socket address
146    General(SocketAddr),
147}
148
149impl ListenItem {
150    /// Return the `SocketAddr`s implied by this item
151    fn iter(&self) -> impl Iterator<Item = SocketAddr> + '_ {
152        use ListenItem as LI;
153        match self {
154            &LI::Localhost(port) => Either::Left({
155                let port = port.into();
156                let addrs: [IpAddr; 2] = [Ipv6Addr::LOCALHOST.into(), Ipv4Addr::LOCALHOST.into()];
157                addrs.into_iter().map(move |ip| SocketAddr::new(ip, port))
158            }),
159            LI::General(addr) => Either::Right(iter::once(addr).cloned()),
160        }
161    }
162
163    /// Return true if this is a localhost address.
164    fn is_localhost(&self) -> bool {
165        use ListenItem as LI;
166        match self {
167            LI::Localhost(_) => true,
168            LI::General(addr) => addr.ip().is_loopback(),
169        }
170    }
171}
172
173impl Display for ListenItem {
174    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
175        match self {
176            ListenItem::Localhost(port) => write!(f, "localhost port {}", port)?,
177            ListenItem::General(addr) => write!(f, "{}", addr)?,
178        }
179        Ok(())
180    }
181}
182/// How we (de) serialize a [`Listen`]
183#[derive(Serialize, Deserialize)]
184#[serde(untagged)]
185enum ListenSerde {
186    /// for `listen = false` (in TOML syntax)
187    Bool(bool),
188
189    /// A bare item
190    One(ListenItemSerde),
191
192    /// An item in a list
193    List(Vec<ListenItemSerde>),
194}
195
196/// One item in the list of a list-ish `Listen`, or the plain value
197#[derive(Serialize, Deserialize)]
198#[serde(untagged)]
199enum ListenItemSerde {
200    /// An integer.
201    ///
202    /// When appearing "loose" (in ListenSerde::One), `0` is parsed as none.
203    Port(u16),
204
205    /// An string which will be parsed as an address and port
206    ///
207    /// When appearing "loose" (in ListenSerde::One), `""` is parsed as none.
208    String(String),
209}
210
211// This implementation isn't fallible, but clippy thinks it is because of the unwrap.
212// The unwrap is just there because we can't pattern-match on a Vec
213#[allow(clippy::fallible_impl_from)]
214impl From<Listen> for ListenSerde {
215    fn from(l: Listen) -> ListenSerde {
216        let l = l.0;
217        match l.len() {
218            0 => ListenSerde::Bool(false),
219            1 => ListenSerde::One(l.into_iter().next().expect("len=1 but no next").into()),
220            _ => ListenSerde::List(l.into_iter().map(Into::into).collect()),
221        }
222    }
223}
224impl From<ListenItem> for ListenItemSerde {
225    fn from(i: ListenItem) -> ListenItemSerde {
226        use ListenItem as LI;
227        use ListenItemSerde as LIS;
228        match i {
229            LI::Localhost(port) => LIS::Port(port.into()),
230            LI::General(addr) => LIS::String(addr.to_string()),
231        }
232    }
233}
234
235/// Listen configuration is invalid
236#[derive(thiserror::Error, Debug, Clone)]
237#[non_exhaustive]
238pub enum InvalidListen {
239    /// Bool was `true` but that's not an address.
240    #[error("Invalid listen specification: need actual addr/port, or `false`; not `true`")]
241    InvalidBool,
242
243    /// Specified listen was a string but couldn't parse to a [`SocketAddr`].
244    #[error("Invalid listen specification: failed to parse string: {0}")]
245    InvalidString(#[from] std::net::AddrParseError),
246
247    /// Specified listen was a list containing a zero integer
248    #[error("Invalid listen specification: zero (for no port) not permitted in list")]
249    ZeroPortInList,
250}
251impl TryFrom<ListenSerde> for Listen {
252    type Error = InvalidListen;
253
254    fn try_from(l: ListenSerde) -> Result<Listen, Self::Error> {
255        use ListenSerde as LS;
256        Ok(Listen(match l {
257            LS::Bool(false) => vec![],
258            LS::Bool(true) => return Err(InvalidListen::InvalidBool),
259            LS::One(i) if i.means_none() => vec![],
260            LS::One(i) => vec![i.try_into()?],
261            LS::List(l) => l.into_iter().map(|i| i.try_into()).try_collect()?,
262        }))
263    }
264}
265impl ListenItemSerde {
266    /// Is this item actually a sentinel, meaning "don't listen, disable this thing"?
267    ///
268    /// Allowed only bare, not in a list.
269    fn means_none(&self) -> bool {
270        use ListenItemSerde as LIS;
271        match self {
272            &LIS::Port(port) => port == 0,
273            LIS::String(s) => s.is_empty(),
274        }
275    }
276}
277impl TryFrom<ListenItemSerde> for ListenItem {
278    type Error = InvalidListen;
279
280    fn try_from(i: ListenItemSerde) -> Result<ListenItem, Self::Error> {
281        use ListenItem as LI;
282        use ListenItemSerde as LIS;
283        Ok(match i {
284            LIS::String(s) => LI::General(s.parse()?),
285            LIS::Port(p) => LI::Localhost(p.try_into().map_err(|_| InvalidListen::ZeroPortInList)?),
286        })
287    }
288}
289
290#[cfg(test)]
291mod test {
292    // @@ begin test lint list maintained by maint/add_warning @@
293    #![allow(clippy::bool_assert_comparison)]
294    #![allow(clippy::clone_on_copy)]
295    #![allow(clippy::dbg_macro)]
296    #![allow(clippy::mixed_attributes_style)]
297    #![allow(clippy::print_stderr)]
298    #![allow(clippy::print_stdout)]
299    #![allow(clippy::single_char_pattern)]
300    #![allow(clippy::unwrap_used)]
301    #![allow(clippy::unchecked_duration_subtraction)]
302    #![allow(clippy::useless_vec)]
303    #![allow(clippy::needless_pass_by_value)]
304    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
305    use super::*;
306
307    #[derive(Debug, Default, Deserialize, Serialize)]
308    struct TestConfigFile {
309        #[serde(default)]
310        listen: Option<Listen>,
311    }
312
313    #[test]
314    fn listen_parse() {
315        use ListenItem as LI;
316
317        let localhost6 = |p| SocketAddr::new(Ipv6Addr::LOCALHOST.into(), p);
318        let localhost4 = |p| SocketAddr::new(Ipv4Addr::LOCALHOST.into(), p);
319        let unspec6 = |p| SocketAddr::new(Ipv6Addr::UNSPECIFIED.into(), p);
320
321        #[allow(clippy::needless_pass_by_value)] // we do this for consistency
322        fn chk(
323            exp_i: Vec<ListenItem>,
324            exp_addrs: Result<Vec<Vec<SocketAddr>>, ()>,
325            exp_lpd: Result<Option<u16>, ()>,
326            s: &str,
327        ) {
328            let tc: TestConfigFile = toml::from_str(s).expect(s);
329            let ll = tc.listen.unwrap();
330            eprintln!("s={:?} ll={:?}", &s, &ll);
331            assert_eq!(ll, Listen(exp_i));
332            assert_eq!(
333                ll.ip_addrs()
334                    .map(|a| a.map(|l| l.collect_vec()).collect_vec())
335                    .map_err(|_| ()),
336                exp_addrs
337            );
338            assert_eq!(ll.localhost_port_legacy().map_err(|_| ()), exp_lpd);
339        }
340
341        let chk_err = |exp, s: &str| {
342            let got: Result<TestConfigFile, _> = toml::from_str(s);
343            let got = got.expect_err(s).to_string();
344            assert!(got.contains(exp), "s={:?} got={:?} exp={:?}", s, got, exp);
345        };
346
347        let chk_none = |s: &str| {
348            chk(vec![], Ok(vec![]), Ok(None), &format!("listen = {}", s));
349            chk_err(
350                "", /* any error will do */
351                &format!("listen = [ {} ]", s),
352            );
353        };
354
355        let chk_1 = |v: ListenItem, addrs: Vec<Vec<SocketAddr>>, port, s| {
356            chk(
357                vec![v.clone()],
358                Ok(addrs.clone()),
359                port,
360                &format!("listen = {}", s),
361            );
362            chk(
363                vec![v.clone()],
364                Ok(addrs.clone()),
365                port,
366                &format!("listen = [ {} ]", s),
367            );
368            chk(
369                vec![v, LI::Localhost(23.try_into().unwrap())],
370                Ok([addrs, vec![vec![localhost6(23), localhost4(23)]]]
371                    .into_iter()
372                    .flatten()
373                    .collect()),
374                Err(()),
375                &format!("listen = [ {}, 23 ]", s),
376            );
377        };
378
379        chk_none(r#""""#);
380        chk_none(r#"0"#);
381        chk_none(r#"false"#);
382        chk(vec![], Ok(vec![]), Ok(None), r#"listen = []"#);
383
384        chk_1(
385            LI::Localhost(42.try_into().unwrap()),
386            vec![vec![localhost6(42), localhost4(42)]],
387            Ok(Some(42)),
388            "42",
389        );
390        chk_1(
391            LI::General(unspec6(56)),
392            vec![vec![unspec6(56)]],
393            Err(()),
394            r#""[::]:56""#,
395        );
396
397        let chk_err_1 = |e, el, s| {
398            chk_err(e, &format!("listen = {}", s));
399            chk_err(el, &format!("listen = [ {} ]", s));
400            chk_err(el, &format!("listen = [ 23, {}, 77 ]", s));
401        };
402
403        chk_err_1("need actual addr/port", "did not match any variant", "true");
404        chk_err("did not match any variant", r#"listen = [ [] ]"#);
405    }
406
407    #[test]
408    fn display_listen() {
409        let empty = Listen::new_none();
410        assert_eq!(empty.to_string(), "");
411
412        let one_port = Listen::new_localhost(1234);
413        assert_eq!(one_port.to_string(), "localhost port 1234");
414
415        let multi_port = Listen(vec![
416            ListenItem::Localhost(1111.try_into().unwrap()),
417            ListenItem::Localhost(2222.try_into().unwrap()),
418        ]);
419        assert_eq!(
420            multi_port.to_string(),
421            "localhost port 1111, localhost port 2222"
422        );
423
424        let multi_addr = Listen(vec![
425            ListenItem::Localhost(1234.try_into().unwrap()),
426            ListenItem::General("1.2.3.4:5678".parse().unwrap()),
427        ]);
428        assert_eq!(multi_addr.to_string(), "localhost port 1234, 1.2.3.4:5678");
429    }
430
431    #[test]
432    fn is_localhost() {
433        fn localhost_only(s: &str) -> bool {
434            let tc: TestConfigFile = toml::from_str(s).expect(s);
435            tc.listen.unwrap().is_localhost_only()
436        }
437
438        assert_eq!(localhost_only(r#"listen = [ ]"#), true);
439        assert_eq!(localhost_only(r#"listen = [ 3 ]"#), true);
440        assert_eq!(localhost_only(r#"listen = [ 3, 10 ]"#), true);
441        assert_eq!(localhost_only(r#"listen = [ "127.0.0.1:9050" ]"#), true);
442        assert_eq!(localhost_only(r#"listen = [ "[::1]:9050" ]"#), true);
443        assert_eq!(
444            localhost_only(r#"listen = [ "[::1]:9050", "192.168.0.1:1234" ]"#),
445            false
446        );
447        assert_eq!(localhost_only(r#"listen = [  "192.168.0.1:1234" ]"#), false);
448    }
449}