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#[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 pub fn port(&self) -> u16 {
29 match self {
30 Self::Inet(_, p) => *p,
31 Self::Name(_, p) => *p,
32 }
33 }
34
35 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 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 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 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
193impl 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 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 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#[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 "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 "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 "2001:db8::1",
358 "::1",
359 "[foo:bar]:80",
360 "[::1]:80]:90",
361 "[]:7",
362 "01.2.3.4:80",
364 "1.2.3.4.5:80",
365 "127.1:1",
366 "12345:80",
367 "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 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}