kumo_address/
resolvable.rs

1use crate::socket::SocketAddress;
2use hickory_proto::rr::Name;
3use serde::{Deserialize, Serialize};
4use std::net::{SocketAddr, SocketAddrV4, SocketAddrV6};
5use std::os::unix::net::SocketAddr as UnixSocketAddr;
6use std::str::FromStr;
7use thiserror::Error;
8
9/// An address that can be resolved to one or more concrete socket addresses.
10///
11/// Unlike [`SocketAddress`], the `Hostname` arm accepts a DNS name plus a
12/// required port; resolution to a concrete IP requires a DNS lookup, which
13/// is performed by `dns_resolver::resolve_socket_addr`.
14#[derive(Clone, Serialize, Deserialize)]
15#[serde(try_from = "String", into = "String")]
16pub enum ResolvableSocketAddr {
17    UnixDomain(Box<UnixSocketAddr>),
18    V4(Box<SocketAddrV4>),
19    V6(Box<SocketAddrV6>),
20    /// A DNS name plus a required port. The host is stored verbatim
21    /// (no IDNA normalization), but has been validated as a syntactically
22    /// well-formed DNS name at construction time.
23    Hostname {
24        host: String,
25        port: u16,
26    },
27}
28
29/// Reason why a candidate string could not be parsed as a
30/// [`ResolvableSocketAddr`]. Each field is the failure reason for one of the
31/// three forms we attempt, in order: bracketed/literal IP socket, unix
32/// domain socket path, and `host:port`.
33#[derive(Error, Debug)]
34#[error(
35    "failed to parse {candidate:?} as a resolvable socket address: \
36    not an IP socket ({socket}); \
37    not a unix socket path ({unix}); \
38    not host:port ({hostname})"
39)]
40pub struct ResolvableAddressParseError {
41    pub candidate: String,
42    pub socket: std::net::AddrParseError,
43    pub unix: std::io::Error,
44    pub hostname: HostnamePortParseError,
45}
46
47impl PartialEq for ResolvableAddressParseError {
48    fn eq(&self, other: &Self) -> bool {
49        self.to_string().eq(&other.to_string())
50    }
51}
52
53#[derive(Error, Debug)]
54pub enum HostnamePortParseError {
55    #[error("missing :port suffix")]
56    MissingPort,
57    #[error("invalid port {0:?}")]
58    InvalidPort(String),
59    #[error("invalid hostname {host:?}: {reason}")]
60    InvalidHostname { host: String, reason: String },
61}
62
63impl std::fmt::Debug for ResolvableSocketAddr {
64    fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
65        <Self as std::fmt::Display>::fmt(self, fmt)
66    }
67}
68
69impl std::fmt::Display for ResolvableSocketAddr {
70    fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
71        match self {
72            Self::UnixDomain(unix) => match unix.as_pathname() {
73                Some(path) => path.display().fmt(fmt),
74                None => write!(fmt, "<unbound unix domain>"),
75            },
76            Self::V4(a) => a.fmt(fmt),
77            Self::V6(a) => a.fmt(fmt),
78            Self::Hostname { host, port } => write!(fmt, "{host}:{port}"),
79        }
80    }
81}
82
83impl ResolvableSocketAddr {
84    /// Returns the unix domain socket representation of the address
85    pub fn unix(&self) -> Option<UnixSocketAddr> {
86        match self {
87            Self::UnixDomain(unix) => Some((**unix).clone()),
88            Self::V4(_) | Self::V6(_) | Self::Hostname { .. } => None,
89        }
90    }
91
92    /// Returns the port number, if any. Unix domain sockets have no port.
93    pub fn port(&self) -> Option<u16> {
94        match self {
95            Self::UnixDomain(_) => None,
96            Self::V4(a) => Some(a.port()),
97            Self::V6(a) => Some(a.port()),
98            Self::Hostname { port, .. } => Some(*port),
99        }
100    }
101}
102
103impl From<ResolvableSocketAddr> for String {
104    fn from(a: ResolvableSocketAddr) -> String {
105        format!("{a}")
106    }
107}
108
109impl TryFrom<String> for ResolvableSocketAddr {
110    type Error = ResolvableAddressParseError;
111    fn try_from(s: String) -> Result<ResolvableSocketAddr, Self::Error> {
112        ResolvableSocketAddr::from_str(&s)
113    }
114}
115
116fn parse_hostname_port(s: &str) -> Result<(String, u16), HostnamePortParseError> {
117    let (host, port) = s
118        .rsplit_once(':')
119        .ok_or(HostnamePortParseError::MissingPort)?;
120    let port: u16 = port
121        .parse()
122        .map_err(|_| HostnamePortParseError::InvalidPort(port.to_string()))?;
123    if host.is_empty() {
124        return Err(HostnamePortParseError::InvalidHostname {
125            host: host.to_string(),
126            reason: "hostname is empty".to_string(),
127        });
128    }
129    Name::from_str_relaxed(host).map_err(|e| HostnamePortParseError::InvalidHostname {
130        host: host.to_string(),
131        reason: e.to_string(),
132    })?;
133    Ok((host.to_string(), port))
134}
135
136impl FromStr for ResolvableSocketAddr {
137    type Err = ResolvableAddressParseError;
138    fn from_str(s: &str) -> Result<ResolvableSocketAddr, Self::Err> {
139        match SocketAddress::from_str(s) {
140            Ok(SocketAddress::UnixDomain(p)) => Ok(ResolvableSocketAddr::UnixDomain(p)),
141            Ok(SocketAddress::V4(a)) => Ok(ResolvableSocketAddr::V4(Box::new(a))),
142            Ok(SocketAddress::V6(a)) => Ok(ResolvableSocketAddr::V6(Box::new(a))),
143            Err(socket_err) => match parse_hostname_port(s) {
144                Ok((host, port)) => Ok(ResolvableSocketAddr::Hostname { host, port }),
145                Err(hostname) => Err(ResolvableAddressParseError {
146                    candidate: s.to_string(),
147                    socket: socket_err.net_err,
148                    unix: socket_err.unix_err,
149                    hostname,
150                }),
151            },
152        }
153    }
154}
155
156impl PartialEq for ResolvableSocketAddr {
157    fn eq(&self, other: &Self) -> bool {
158        match (self, other) {
159            (Self::UnixDomain(a), Self::UnixDomain(b)) => {
160                match (a.as_pathname(), b.as_pathname()) {
161                    (Some(a), Some(b)) => a.eq(b),
162                    (None, None) => true,
163                    _ => false,
164                }
165            }
166            (Self::V4(a), Self::V4(b)) => a.eq(b),
167            (Self::V6(a), Self::V6(b)) => a.eq(b),
168            (Self::Hostname { host: ah, port: ap }, Self::Hostname { host: bh, port: bp }) => {
169                ah == bh && ap == bp
170            }
171            _ => false,
172        }
173    }
174}
175
176impl Eq for ResolvableSocketAddr {}
177
178impl From<UnixSocketAddr> for ResolvableSocketAddr {
179    fn from(unix: UnixSocketAddr) -> Self {
180        Self::UnixDomain(unix.into())
181    }
182}
183
184impl From<tokio::net::unix::SocketAddr> for ResolvableSocketAddr {
185    fn from(unix: tokio::net::unix::SocketAddr) -> Self {
186        let unix: UnixSocketAddr = unix.into();
187        unix.into()
188    }
189}
190
191impl From<SocketAddr> for ResolvableSocketAddr {
192    fn from(a: SocketAddr) -> Self {
193        match a {
194            SocketAddr::V4(a) => Self::V4(Box::new(a)),
195            SocketAddr::V6(a) => Self::V6(Box::new(a)),
196        }
197    }
198}
199
200impl From<SocketAddrV4> for ResolvableSocketAddr {
201    fn from(a: SocketAddrV4) -> Self {
202        Self::V4(Box::new(a))
203    }
204}
205
206impl From<SocketAddrV6> for ResolvableSocketAddr {
207    fn from(a: SocketAddrV6) -> Self {
208        Self::V6(Box::new(a))
209    }
210}
211
212impl From<SocketAddress> for ResolvableSocketAddr {
213    fn from(a: SocketAddress) -> Self {
214        match a {
215            SocketAddress::UnixDomain(p) => Self::UnixDomain(p),
216            SocketAddress::V4(a) => Self::V4(Box::new(a)),
217            SocketAddress::V6(a) => Self::V6(Box::new(a)),
218        }
219    }
220}
221
222#[cfg(test)]
223mod test {
224    use super::*;
225    use std::net::{Ipv4Addr, Ipv6Addr};
226
227    #[test]
228    fn parse_literal_v4() {
229        k9::assert_equal!(
230            "10.0.0.1:25".parse::<ResolvableSocketAddr>().unwrap(),
231            ResolvableSocketAddr::V4(Box::new(SocketAddrV4::new(Ipv4Addr::new(10, 0, 0, 1), 25)))
232        );
233        k9::assert_equal!(
234            "[10.0.0.1]:25".parse::<ResolvableSocketAddr>().unwrap(),
235            ResolvableSocketAddr::V4(Box::new(SocketAddrV4::new(Ipv4Addr::new(10, 0, 0, 1), 25)))
236        );
237    }
238
239    #[test]
240    fn parse_literal_v6() {
241        k9::assert_equal!(
242            "[::1]:100".parse::<ResolvableSocketAddr>().unwrap(),
243            ResolvableSocketAddr::V6(Box::new(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 100, 0, 0)))
244        );
245    }
246
247    #[test]
248    fn parse_unix() {
249        k9::assert_equal!(
250            "/some/path".parse::<ResolvableSocketAddr>().unwrap(),
251            ResolvableSocketAddr::UnixDomain(
252                UnixSocketAddr::from_pathname("/some/path").unwrap().into()
253            )
254        );
255    }
256
257    #[test]
258    fn parse_hostname() {
259        k9::assert_equal!(
260            "mx.example.com:25".parse::<ResolvableSocketAddr>().unwrap(),
261            ResolvableSocketAddr::Hostname {
262                host: "mx.example.com".to_string(),
263                port: 25
264            }
265        );
266    }
267
268    #[test]
269    fn reject_bogus_hostname() {
270        let err = "I have spaces:25"
271            .parse::<ResolvableSocketAddr>()
272            .unwrap_err();
273        k9::assert_equal!(
274            format!("{err:#}"),
275            "failed to parse \"I have spaces:25\" as a resolvable socket address: \
276            not an IP socket (invalid socket address syntax); \
277            not a unix socket path (unix domain path must be absolute); \
278            not host:port (invalid hostname \"I have spaces\": unrecognized char:  )"
279        );
280    }
281
282    #[test]
283    fn reject_missing_port() {
284        let err = "mx.example.com"
285            .parse::<ResolvableSocketAddr>()
286            .unwrap_err();
287        k9::assert_equal!(
288            format!("{err:#}"),
289            "failed to parse \"mx.example.com\" as a resolvable socket address: \
290            not an IP socket (invalid socket address syntax); \
291            not a unix socket path (unix domain path must be absolute); \
292            not host:port (missing :port suffix)"
293        );
294    }
295
296    #[test]
297    fn reject_bad_port() {
298        let err = "mx.example.com:bogus"
299            .parse::<ResolvableSocketAddr>()
300            .unwrap_err();
301        k9::assert_equal!(
302            format!("{err:#}"),
303            "failed to parse \"mx.example.com:bogus\" as a resolvable socket address: \
304            not an IP socket (invalid socket address syntax); \
305            not a unix socket path (unix domain path must be absolute); \
306            not host:port (invalid port \"bogus\")"
307        );
308    }
309
310    #[test]
311    fn reject_unbracketed_v6_with_port() {
312        // An unbracketed IPv6 literal followed by `:port` is ambiguous with
313        // `host:port` and is not accepted by the standard socket parser.
314        // Require the bracketed form (e.g. `[::1]:100`) instead.
315        let err = "::1:100".parse::<ResolvableSocketAddr>().unwrap_err();
316        k9::assert_equal!(
317            format!("{err:#}"),
318            "failed to parse \"::1:100\" as a resolvable socket address: \
319            not an IP socket (invalid socket address syntax); \
320            not a unix socket path (unix domain path must be absolute); \
321            not host:port (invalid hostname \"::1\": Malformed label: ::1)"
322        );
323
324        let err = "fe80::1:100".parse::<ResolvableSocketAddr>().unwrap_err();
325        k9::assert_equal!(
326            format!("{err:#}"),
327            "failed to parse \"fe80::1:100\" as a resolvable socket address: \
328            not an IP socket (invalid socket address syntax); \
329            not a unix socket path (unix domain path must be absolute); \
330            not host:port (invalid hostname \"fe80::1\": Malformed label: fe80::1)"
331        );
332    }
333
334    #[test]
335    fn display_roundtrip() {
336        for s in &[
337            "10.0.0.1:25",
338            "[::1]:100",
339            "/some/path",
340            "mx.example.com:25",
341        ] {
342            let a: ResolvableSocketAddr = s.parse().unwrap();
343            let back: ResolvableSocketAddr = a.to_string().parse().unwrap();
344            k9::assert_equal!(a, back);
345        }
346    }
347}