Skip to main content

cliquenet_types/
addr.rs

1use std::{
2    borrow::Cow,
3    fmt,
4    net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
5};
6
7#[cfg(feature = "serde")]
8use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
9
10/// A network address.
11///
12/// Either an IP address and port number or else a hostname and port number.
13#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
14pub enum NetAddr {
15    Inet(IpAddr, u16),
16    Name(Cow<'static, str>, u16),
17}
18
19impl NetAddr {
20    pub fn named<S>(name: S, port: u16) -> Self
21    where
22        S: Into<Cow<'static, str>>,
23    {
24        Self::Name(name.into(), port)
25    }
26
27    /// Get the port number of an address.
28    pub fn port(&self) -> u16 {
29        match self {
30            Self::Inet(_, p) => *p,
31            Self::Name(_, p) => *p,
32        }
33    }
34
35    /// Set the address port.
36    pub fn set_port(&mut self, p: u16) {
37        match self {
38            Self::Inet(_, o) => *o = p,
39            Self::Name(_, o) => *o = p,
40        }
41    }
42
43    pub fn with_port(mut self, p: u16) -> Self {
44        match self {
45            Self::Inet(ip, _) => self = Self::Inet(ip, p),
46            Self::Name(hn, _) => self = Self::Name(hn, p),
47        }
48        self
49    }
50
51    pub fn with_offset(mut self, o: u16) -> Self {
52        debug_assert!(self.port().checked_add(o).is_some());
53        match self {
54            Self::Inet(ip, p) => self = Self::Inet(ip, p + o),
55            Self::Name(hn, p) => self = Self::Name(hn, p + o),
56        }
57        self
58    }
59
60    pub fn is_ip(&self) -> bool {
61        matches!(self, Self::Inet(..))
62    }
63
64    /// Whether this address is plausibly publicly routable. Returns `false` for IP literals
65    /// in non-globally-routable ranges (loopback, unspecified, RFC 1918 private, link-local,
66    /// broadcast, documentation, IPv6 multicast) and the literal `localhost`. Other hostnames
67    /// are trusted and return `true`. Approximates the (still unstable) `IpAddr::is_global`
68    /// using stable predicates; the IPv6 surface is incomplete (`fe80::/10` link-local and
69    /// `fc00::/7` unique-local addresses are treated as global here).
70    pub fn is_probably_global(&self) -> bool {
71        match self {
72            Self::Inet(IpAddr::V4(v4), _) => {
73                !(v4.is_loopback()
74                    || v4.is_unspecified()
75                    || v4.is_private()
76                    || v4.is_link_local()
77                    || v4.is_broadcast()
78                    || v4.is_documentation())
79            },
80            Self::Inet(IpAddr::V6(v6), _) => {
81                !(v6.is_loopback() || v6.is_unspecified() || v6.is_multicast())
82            },
83            Self::Name(host, _) => !host.eq_ignore_ascii_case("localhost"),
84        }
85    }
86
87    /// Checks that this address is well-formed.
88    ///
89    /// A hostname is dot-separated labels, each 1 to 63 characters of ASCII letters,
90    /// digits, `-` and `_`, not beginning or ending with `-`, and not digits and dots
91    /// alone. RFC 952 and RFC 1123 do not allow `_`, but names using it are in use. An
92    /// IP address must be the address it prints as, so an IPv4 address in IPv6 form is
93    /// not well-formed; [`IpAddr::to_canonical`] converts it.
94    ///
95    /// Neither the port nor the length of the whole name is checked.
96    pub fn validate(&self) -> Result<(), InvalidNetAddr> {
97        match self {
98            Self::Inet(ip, _) => {
99                if ip.to_canonical() != *ip {
100                    return Err(InvalidNetAddr("IPv4 address in IPv6 form"));
101                }
102                Ok(())
103            },
104            Self::Name(host, _) => {
105                const MAX_LABEL_LEN: usize = 63;
106
107                if host.is_empty() {
108                    return Err(InvalidNetAddr("empty hostname"));
109                }
110                if host.bytes().all(|b| b.is_ascii_digit() || b == b'.') {
111                    return Err(InvalidNetAddr("host is not an IP address nor a hostname"));
112                }
113                for label in host.split('.') {
114                    if label.is_empty() {
115                        return Err(InvalidNetAddr("hostname contains invalid dots"));
116                    }
117                    if label.len() > MAX_LABEL_LEN {
118                        return Err(InvalidNetAddr("hostname part is longer than 63 chars"));
119                    }
120                    if label.starts_with('-') || label.ends_with('-') {
121                        return Err(InvalidNetAddr("hostname part starts or ends with `-`"));
122                    }
123                    if !label
124                        .bytes()
125                        .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_')
126                    {
127                        return Err(InvalidNetAddr("hostname contains invalid characters"));
128                    }
129                }
130                Ok(())
131            },
132        }
133    }
134
135    /// The address without brackets around an IPv6 literal.
136    ///
137    /// This is how [`fmt::Display`] printed every address before IPv6 literals were
138    /// bracketed. The format is retained here for backwards compatibility.
139    pub fn unbracketed_string(&self) -> String {
140        match self {
141            Self::Inet(a, p) => format!("{a}:{p}"),
142            Self::Name(h, p) => format!("{h}:{p}"),
143        }
144    }
145}
146
147impl fmt::Display for NetAddr {
148    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149        match self {
150            Self::Inet(a @ IpAddr::V6(_), p) => write!(f, "[{a}]:{p}"),
151            Self::Inet(a, p) => write!(f, "{a}:{p}"),
152            Self::Name(h, p) => write!(f, "{h}:{p}"),
153        }
154    }
155}
156
157impl From<(&str, u16)> for NetAddr {
158    fn from((h, p): (&str, u16)) -> Self {
159        Self::Name(h.to_string().into(), p)
160    }
161}
162
163impl From<(String, u16)> for NetAddr {
164    fn from((h, p): (String, u16)) -> Self {
165        Self::Name(h.into(), p)
166    }
167}
168
169impl From<(IpAddr, u16)> for NetAddr {
170    fn from((ip, p): (IpAddr, u16)) -> Self {
171        Self::Inet(ip, p)
172    }
173}
174
175impl From<(Ipv4Addr, u16)> for NetAddr {
176    fn from((ip, p): (Ipv4Addr, u16)) -> Self {
177        Self::Inet(IpAddr::V4(ip), p)
178    }
179}
180
181impl From<(Ipv6Addr, u16)> for NetAddr {
182    fn from((ip, p): (Ipv6Addr, u16)) -> Self {
183        Self::Inet(IpAddr::V6(ip), p)
184    }
185}
186
187impl From<SocketAddr> for NetAddr {
188    fn from(a: SocketAddr) -> Self {
189        Self::Inet(a.ip(), a.port())
190    }
191}
192
193/// Grammar:
194///
195/// ```text
196/// addr = host               -- port is 0
197///      | host ":" port
198///      | "[" host "]"       -- port is 0
199///      | "[" host "]:" port
200///
201/// host = IP | name
202/// IP   = <std::net::IpAddr>
203/// name = <any sequence of characters, possibly empty>
204/// port = <u16>
205/// ```
206///
207/// - Input starting with `[` has the port after the last `]:`. With no `]:` anywhere there
208///   is no port, so `[a:80` is the name `[a:80` on port 0.
209/// - Input not starting with `[` has the port after the last `:` or defaults to 0 if there
210///   is no `:`.
211/// - `host` is an IP if `IpAddr` parses it, a name otherwise.
212/// - Parsing the port permits a leading `+` and leading zeros.
213impl std::str::FromStr for NetAddr {
214    type Err = InvalidNetAddr;
215
216    fn from_str(s: &str) -> Result<Self, Self::Err> {
217        if s.is_empty() {
218            return Err(InvalidNetAddr("the address is empty"));
219        }
220
221        let parse = |a: &str, p: Option<&str>| {
222            let p: u16 = if let Some(p) = p {
223                p.parse().map_err(|_| InvalidNetAddr("invalid port"))?
224            } else {
225                0
226            };
227            // Strip brackets from IPv6 addresses like `[::1]`.
228            let a = if a.starts_with('[') && a.ends_with(']') {
229                &a[1..a.len() - 1]
230            } else {
231                a
232            };
233            IpAddr::from_str(a)
234                .map(|a| Self::Inet(a, p))
235                .or_else(|_| Ok(Self::Name(a.to_string().into(), p)))
236        };
237
238        // Handle bracketed IPv6 like `[::1]:8080` or `[::1]` (no port).
239        if s.starts_with('[') {
240            return match s.rfind("]:") {
241                Some(i) => parse(&s[..i + 1], Some(&s[i + 2..])),
242                None => parse(s, None),
243            };
244        }
245
246        match s.rsplit_once(':') {
247            None => parse(s, None),
248            Some((a, p)) => parse(a, Some(p)),
249        }
250    }
251}
252
253impl TryFrom<&str> for NetAddr {
254    type Error = InvalidNetAddr;
255
256    fn try_from(val: &str) -> Result<Self, Self::Error> {
257        val.parse()
258    }
259}
260
261#[derive(Debug, Clone, thiserror::Error)]
262#[error("invalid network address: {0}")]
263pub struct InvalidNetAddr(&'static str);
264
265// TODO: distinguish human-readable:
266
267#[cfg(feature = "serde")]
268impl Serialize for NetAddr {
269    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
270        self.unbracketed_string().serialize(s)
271    }
272}
273
274#[cfg(feature = "serde")]
275impl<'de> Deserialize<'de> for NetAddr {
276    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
277        let s = String::deserialize(d)?;
278        let a = s.parse().map_err(de::Error::custom)?;
279        Ok(a)
280    }
281}
282
283#[cfg(test)]
284mod tests {
285    use std::{
286        iter::repeat_with,
287        net::{IpAddr, SocketAddr},
288    };
289
290    use quickcheck::{Arbitrary, Gen, quickcheck};
291
292    use super::NetAddr;
293
294    impl Arbitrary for NetAddr {
295        fn arbitrary(g: &mut Gen) -> Self {
296            let port = u16::arbitrary(g);
297            if bool::arbitrary(g) {
298                let len = u8::arbitrary(g);
299                let host: String = repeat_with(|| char::arbitrary(g))
300                    .filter(|c| !"[]".contains(*c))
301                    .take(len.into())
302                    .collect();
303                NetAddr::Name(host.into(), port)
304            } else {
305                let ip = IpAddr::arbitrary(g);
306                NetAddr::Inet(ip, port)
307            }
308        }
309    }
310
311    quickcheck! {
312        fn prop_to_string_parse_identity(a: NetAddr) -> bool {
313            a.to_string().parse().ok() == Some(a)
314        }
315    }
316
317    #[test]
318    fn empty_is_invalid() {
319        assert!("".parse::<NetAddr>().is_err())
320    }
321
322    #[test]
323    fn validate_accepts() {
324        let cases: &[&str] = &[
325            // an IP address, however it was written
326            "1.2.3.4:8080",
327            "1.2.3.4:0",
328            "1.2.3.4:65535",
329            "[::1]:9977",
330            "::1:9977",
331            "[2001:db8::1]:9000",
332            "2001:db8::1:9000",
333            "[::]:80",
334            ":::80",
335            "[::ffff:0:102:304]:80",
336            "[1.2.3.4]:80",
337            // hostnames, in any case, and with or without a port
338            "localhost:1234",
339            "example.com",
340            "a-b.c:80",
341            "a_b.example.com:80",
342            "_svc.example.com:80",
343            "Node.Example.COM:8080",
344            "[node.example.com]:8080",
345            "crpk232f2b1uqepj3qmg.bdnodes.net:9977",
346        ];
347        for s in cases {
348            let a: NetAddr = s.parse().unwrap_or_else(|_| panic!("parse {s}"));
349            assert_eq!(a.validate().map_err(|e| e.to_string()), Ok(()), "for {s}");
350        }
351    }
352
353    #[test]
354    fn validate_rejects() {
355        let cases: &[&str] = &[
356            // an IPv6 address without a port is a hostname with an empty part
357            "2001:db8::1",
358            "::1",
359            "[foo:bar]:80",
360            "[::1]:80]:90",
361            "[]:7",
362            // digits and dots that are not an IP address
363            "01.2.3.4:80",
364            "1.2.3.4.5:80",
365            "127.1:1",
366            "12345:80",
367            // hostname parts
368            "example.com.:80",
369            ".example.com:80",
370            "a..b:80",
371            "-a.b:80",
372            "a-.b:80",
373            "a b:80",
374            "รค:80",
375            "%:1",
376            "a/b:80",
377        ];
378        for s in cases {
379            let a: NetAddr = s.parse().unwrap_or_else(|_| panic!("parse {s}"));
380            assert!(a.validate().is_err(), "should be rejected: {s:?}");
381        }
382
383        // An IPv4 address in IPv6 form is one address written two ways.
384        let mapped: NetAddr = "[::ffff:1.2.3.4]:80".parse().expect("parse");
385        assert!(mapped.validate().is_err());
386        assert!(
387            mapped
388                .validate()
389                .unwrap_err()
390                .to_string()
391                .contains("IPv4 address in IPv6 form")
392        );
393        let NetAddr::Inet(ip, port) = mapped else {
394            panic!("an IP address")
395        };
396        assert_eq!(
397            NetAddr::Inet(ip.to_canonical(), port),
398            "1.2.3.4:80".parse().expect("parse")
399        );
400    }
401
402    #[test]
403    fn test_is_probably_global() {
404        let cases: &[(&str, bool)] = &[
405            ("127.0.0.1:1234", false),
406            ("0.0.0.0:1234", false),
407            ("10.0.0.1:1234", false),
408            ("172.16.5.4:1234", false),
409            ("192.168.1.1:1234", false),
410            ("169.254.0.1:1234", false),
411            ("255.255.255.255:1234", false),
412            ("192.0.2.1:1234", false),
413            ("::1:1234", false),
414            (":::1234", false),
415            ("ff00::1:1234", false),
416            ("localhost:1234", false),
417            ("LOCALHOST:1234", false),
418            ("8.8.8.8:1234", true),
419            ("1.1.1.1:1234", true),
420            ("2606:4700:4700::1111:1234", true),
421            ("example.com:1234", true),
422            ("node.internal:1234", true),
423        ];
424        for (s, expected) in cases {
425            let a: NetAddr = s.parse().unwrap_or_else(|_| panic!("parse {s}"));
426            assert_eq!(a.is_probably_global(), *expected, "for input {s}");
427        }
428    }
429
430    #[test]
431    fn ipv6_prints_as_a_socket_addr() {
432        let a: NetAddr = "::1:9977".parse().expect("parse");
433        assert_eq!(a.to_string(), "[::1]:9977");
434        assert!(a.to_string().parse::<SocketAddr>().is_ok());
435        assert!("::1:9977".parse::<SocketAddr>().is_err());
436    }
437}