1use local_ip_address::{Error, list_afinet_netifas, local_ip, local_ipv6};
7use std::ffi::OsString;
8use std::{
9 net::{IpAddr, Ipv4Addr, Ipv6Addr},
10 sync::OnceLock,
11};
12
13const DEFAULT_LOOPBACK: IpAddr = IpAddr::V4(Ipv4Addr::LOCALHOST);
14static LOCAL_IP_FOR_ADVERTISE: OnceLock<IpAddr> = OnceLock::new();
15
16pub(crate) fn host_override_from_env(name: &str) -> anyhow::Result<Option<String>> {
18 host_override_from_lookup(name, |key| std::env::var_os(key))
19}
20
21fn host_override_from_lookup(
22 name: &str,
23 mut get_env: impl FnMut(&str) -> Option<OsString>,
24) -> anyhow::Result<Option<String>> {
25 let Some(value) = get_env(name) else {
26 return Ok(None);
27 };
28 let value = value
29 .into_string()
30 .map_err(|_| anyhow::anyhow!("{name} must contain valid Unicode"))?;
31 let value = value.trim();
32 Ok((!value.is_empty()).then(|| value.to_string()))
33}
34
35pub trait IpResolver {
40 fn local_ip(&self) -> Result<IpAddr, Error>;
41 fn local_ipv6(&self) -> Result<IpAddr, Error>;
42
43 fn list_afinet_netifas(&self) -> Result<Vec<(String, IpAddr)>, Error> {
44 list_afinet_netifas()
45 }
46
47 fn list_up_afinet_netifas(&self) -> Result<Vec<(String, IpAddr)>, Error> {
52 self.list_afinet_netifas()
53 }
54}
55
56pub struct DefaultIpResolver;
58
59impl IpResolver for DefaultIpResolver {
60 fn local_ip(&self) -> Result<IpAddr, Error> {
61 local_ip()
62 }
63
64 fn local_ipv6(&self) -> Result<IpAddr, Error> {
65 local_ipv6()
66 }
67
68 #[cfg(unix)]
69 fn list_up_afinet_netifas(&self) -> Result<Vec<(String, IpAddr)>, Error> {
70 let interfaces = nix::ifaddrs::getifaddrs()
71 .map_err(|error| Error::StrategyError(format!("getifaddrs failed: {error}")))?;
72 Ok(up_ip_interfaces(interfaces))
73 }
74}
75
76#[cfg(unix)]
77fn up_ip_interfaces(
78 interfaces: impl IntoIterator<Item = nix::ifaddrs::InterfaceAddress>,
79) -> Vec<(String, IpAddr)> {
80 use nix::net::if_::InterfaceFlags;
81
82 interfaces
83 .into_iter()
84 .filter(|interface| interface.flags.contains(InterfaceFlags::IFF_UP))
85 .filter_map(|interface| {
86 let address = interface.address?;
87 let ip = if let Some(address) = address.as_sockaddr_in() {
88 IpAddr::V4(address.ip())
89 } else {
90 IpAddr::V6(address.as_sockaddr_in6()?.ip())
91 };
92 Some((interface.interface_name, ip))
93 })
94 .collect()
95}
96
97#[derive(Debug, thiserror::Error)]
98pub(crate) enum IpResolutionError {
99 #[error("failed to enumerate network interfaces: {0}")]
100 InterfaceEnumeration(#[source] Error),
101
102 #[error("interface not found: {0}")]
103 InterfaceNotFound(String),
104
105 #[error("interface has no usable IP address: {0}")]
106 NoUsableInterfaceAddress(String),
107
108 #[error("IP address is not usable: {0}")]
109 UnusableAddress(IpAddr),
110
111 #[error("invalid IP literal: {0}")]
112 InvalidLiteral(String),
113}
114
115#[derive(Clone, Copy, Debug, Eq, PartialEq)]
116pub(crate) struct ResolvedHost {
117 bind_ip: IpAddr,
118 advertise_ip: IpAddr,
119 used_loopback_fallback: bool,
120}
121
122impl ResolvedHost {
123 pub(crate) fn same_address(address: IpAddr) -> Self {
124 Self {
125 bind_ip: address,
126 advertise_ip: address,
127 used_loopback_fallback: false,
128 }
129 }
130
131 pub(crate) fn loopback_fallback(address: IpAddr) -> Self {
132 Self {
133 bind_ip: address,
134 advertise_ip: address,
135 used_loopback_fallback: true,
136 }
137 }
138
139 pub(crate) fn bind_ip(self) -> IpAddr {
140 self.bind_ip
141 }
142
143 pub(crate) fn advertise_ip(self) -> IpAddr {
144 self.advertise_ip
145 }
146
147 pub(crate) fn used_loopback_fallback(self) -> bool {
148 self.used_loopback_fallback
149 }
150}
151
152#[derive(Default)]
153struct AddressCandidates {
154 ipv4: Option<Ipv4Addr>,
155 ipv6: Option<Ipv6Addr>,
156 ipv4_loopback: Option<Ipv4Addr>,
157 ipv6_loopback: Option<Ipv6Addr>,
158}
159
160impl AddressCandidates {
161 fn consider(&mut self, address: IpAddr) {
162 let address = address.to_canonical();
163 if !is_usable(address) {
164 return;
165 }
166
167 match address {
168 IpAddr::V4(address) if address.is_loopback() => {
169 self.ipv4_loopback.get_or_insert(address);
170 }
171 IpAddr::V4(address) => {
172 self.ipv4.get_or_insert(address);
173 }
174 IpAddr::V6(address) if address.is_loopback() => {
175 self.ipv6_loopback.get_or_insert(address);
176 }
177 IpAddr::V6(address) => {
178 self.ipv6.get_or_insert(address);
179 }
180 }
181 }
182
183 fn preferred_non_loopback(&self) -> Option<IpAddr> {
184 self.ipv4
185 .map(IpAddr::V4)
186 .or_else(|| self.ipv6.map(IpAddr::V6))
187 }
188
189 fn preferred_loopback(&self) -> Option<IpAddr> {
190 self.ipv4_loopback
191 .map(IpAddr::V4)
192 .or_else(|| self.ipv6_loopback.map(IpAddr::V6))
193 }
194
195 fn non_loopback_for(&self, family: IpAddr) -> Option<IpAddr> {
196 match family {
197 IpAddr::V4(_) => self.ipv4.map(IpAddr::V4),
198 IpAddr::V6(_) => self.ipv6.map(IpAddr::V6),
199 }
200 }
201
202 fn loopback_for(&self, family: IpAddr) -> Option<IpAddr> {
203 match family {
204 IpAddr::V4(_) => self.ipv4_loopback.map(IpAddr::V4),
205 IpAddr::V6(_) => self.ipv6_loopback.map(IpAddr::V6),
206 }
207 }
208
209 fn other_non_loopback(&self, family: IpAddr) -> Option<IpAddr> {
210 match family {
211 IpAddr::V4(_) => self.ipv6.map(IpAddr::V6),
212 IpAddr::V6(_) => self.ipv4.map(IpAddr::V4),
213 }
214 }
215}
216
217fn local_candidates<R: IpResolver>(resolver: &R) -> Result<AddressCandidates, IpResolutionError> {
218 let interfaces = resolver
219 .list_up_afinet_netifas()
220 .map_err(IpResolutionError::InterfaceEnumeration)?;
221 let mut candidates = AddressCandidates::default();
222
223 for routed in [resolver.local_ip(), resolver.local_ipv6()]
229 .into_iter()
230 .flatten()
231 {
232 let routed = routed.to_canonical();
233 if interfaces
234 .iter()
235 .any(|(_, address)| address.to_canonical() == routed)
236 {
237 candidates.consider(routed);
238 }
239 }
240
241 for (_, address) in interfaces {
242 candidates.consider(address);
243 }
244 Ok(candidates)
245}
246
247pub(crate) fn resolve_local_host<R: IpResolver>(
250 resolver: &R,
251) -> Result<ResolvedHost, IpResolutionError> {
252 let candidates = local_candidates(resolver)?;
253 if let Some(address) = candidates.preferred_non_loopback() {
254 return Ok(ResolvedHost::same_address(address));
255 }
256
257 Ok(ResolvedHost::loopback_fallback(
258 candidates.preferred_loopback().unwrap_or(DEFAULT_LOOPBACK),
259 ))
260}
261
262pub(crate) fn resolve_host_or_interface<R: IpResolver>(
268 host_or_interface: &str,
269 resolver: &R,
270) -> Result<ResolvedHost, IpResolutionError> {
271 let host_or_interface = host_or_interface.trim();
272 if let Some(address) = parse_ip_literal(host_or_interface)? {
273 if address.is_unspecified() {
274 return resolve_wildcard(address, &local_candidates(resolver)?);
275 }
276
277 validate_configured_address(address)?;
278 return Ok(ResolvedHost::same_address(address));
279 }
280
281 let interfaces = resolver
282 .list_afinet_netifas()
283 .map_err(IpResolutionError::InterfaceEnumeration)?;
284 let mut interface_found = false;
285 let mut candidates = AddressCandidates::default();
286
287 for (name, address) in interfaces {
288 if name != host_or_interface {
289 continue;
290 }
291
292 interface_found = true;
293 candidates.consider(address);
294 }
295
296 if !interface_found {
297 return Err(IpResolutionError::InterfaceNotFound(
298 host_or_interface.to_string(),
299 ));
300 }
301
302 candidates
303 .preferred_non_loopback()
304 .or_else(|| candidates.preferred_loopback())
305 .map(ResolvedHost::same_address)
306 .ok_or_else(|| IpResolutionError::NoUsableInterfaceAddress(host_or_interface.to_string()))
307}
308
309fn resolve_wildcard(
310 wildcard: IpAddr,
311 candidates: &AddressCandidates,
312) -> Result<ResolvedHost, IpResolutionError> {
313 if let Some(advertise_ip) = candidates.non_loopback_for(wildcard) {
314 return Ok(ResolvedHost {
315 bind_ip: wildcard,
316 advertise_ip,
317 used_loopback_fallback: false,
318 });
319 }
320
321 if let Some(advertise_ip) = candidates.other_non_loopback(wildcard) {
322 return Ok(ResolvedHost {
323 bind_ip: unspecified_for(advertise_ip),
324 advertise_ip,
325 used_loopback_fallback: false,
326 });
327 }
328
329 let advertise_ip = candidates
330 .loopback_for(wildcard)
331 .or_else(|| candidates.preferred_loopback())
332 .unwrap_or_else(|| loopback_for(wildcard));
333 Ok(ResolvedHost {
334 bind_ip: unspecified_for(advertise_ip),
335 advertise_ip,
336 used_loopback_fallback: true,
337 })
338}
339
340pub(crate) fn resolve_advertise_ip_for_bind<R: IpResolver>(
345 bind_ip: IpAddr,
346 resolver: &R,
347) -> Result<IpAddr, IpResolutionError> {
348 let bind_ip = bind_ip.to_canonical();
349 if !bind_ip.is_unspecified() {
350 validate_configured_address(bind_ip)?;
351 return Ok(bind_ip);
352 }
353
354 let candidates = local_candidates(resolver)?;
355 let dual_stack_fallback = bind_ip
356 .is_ipv6()
357 .then(|| candidates.other_non_loopback(bind_ip))
358 .flatten();
359 Ok(candidates
360 .non_loopback_for(bind_ip)
361 .or(dual_stack_fallback)
362 .or_else(|| candidates.loopback_for(bind_ip))
363 .unwrap_or_else(|| loopback_for(bind_ip)))
364}
365
366fn unspecified_for(address: IpAddr) -> IpAddr {
367 match address {
368 IpAddr::V4(_) => IpAddr::V4(Ipv4Addr::UNSPECIFIED),
369 IpAddr::V6(_) => IpAddr::V6(Ipv6Addr::UNSPECIFIED),
370 }
371}
372
373fn loopback_for(address: IpAddr) -> IpAddr {
374 match address {
375 IpAddr::V4(_) => IpAddr::V4(Ipv4Addr::LOCALHOST),
376 IpAddr::V6(_) => IpAddr::V6(Ipv6Addr::LOCALHOST),
377 }
378}
379
380fn parse_ip_literal(value: &str) -> Result<Option<IpAddr>, IpResolutionError> {
381 let has_open_bracket = value.starts_with('[');
382 let has_close_bracket = value.ends_with(']');
383
384 if has_open_bracket || has_close_bracket {
385 if !(has_open_bracket && has_close_bracket) {
386 return Err(IpResolutionError::InvalidLiteral(value.to_string()));
387 }
388
389 let inner = &value[1..value.len() - 1];
390 return inner
391 .parse::<Ipv6Addr>()
392 .map(|address| Some(IpAddr::V6(address).to_canonical()))
393 .map_err(|_| IpResolutionError::InvalidLiteral(value.to_string()));
394 }
395
396 match value.parse::<IpAddr>() {
397 Ok(address) => Ok(Some(address.to_canonical())),
398 Err(_) if looks_like_ip_literal(value) => {
399 Err(IpResolutionError::InvalidLiteral(value.to_string()))
400 }
401 Err(_) => Ok(None),
402 }
403}
404
405fn looks_like_ip_literal(value: &str) -> bool {
406 value.bytes().filter(|byte| *byte == b':').count() >= 2
407 || (value.contains('.')
408 && value
409 .chars()
410 .all(|character| character.is_ascii_digit() || character == '.'))
411}
412
413fn validate_configured_address(address: IpAddr) -> Result<(), IpResolutionError> {
414 if is_usable(address) {
415 Ok(())
416 } else {
417 Err(IpResolutionError::UnusableAddress(address))
418 }
419}
420
421fn is_usable(address: IpAddr) -> bool {
422 match address {
423 IpAddr::V4(address) => {
424 !address.is_unspecified() && !address.is_multicast() && !address.is_broadcast()
425 }
426 IpAddr::V6(address) => {
427 !address.is_unspecified() && !address.is_multicast() && !address.is_unicast_link_local()
428 }
429 }
430}
431
432pub fn local_ip_for_advertise() -> String {
439 cached_local_ip_for_advertise(&LOCAL_IP_FOR_ADVERTISE, &DefaultIpResolver)
440}
441
442#[deprecated(note = "DYN_TCP_RPC_HOST is resolved by the request-plane server at startup")]
444pub fn tcp_rpc_host_from_env() -> String {
445 std::env::var(crate::config::environment_names::request_plane::DYN_TCP_RPC_HOST)
446 .unwrap_or_else(|_| local_ip_for_advertise())
447}
448
449fn cached_local_ip_for_advertise<R: IpResolver>(cache: &OnceLock<IpAddr>, resolver: &R) -> String {
450 if let Some(address) = cache.get() {
451 return format_host(*address);
452 }
453
454 match resolve_local_host(resolver) {
455 Ok(resolved) if !resolved.used_loopback_fallback() => {
456 let address = resolved.advertise_ip();
457 if cache.set(address).is_err() {
458 return format_host(*cache.get().expect("advertisement cache was initialized"));
459 }
460 format_host(address)
461 }
462 Ok(resolved) => {
463 let loopback = resolved.advertise_ip();
464 tracing::warn!(
465 %loopback,
466 "No usable non-loopback IP address found; advertising loopback"
467 );
468 format_host(loopback)
469 }
470 Err(error) => {
471 tracing::warn!(
472 %error,
473 loopback = %DEFAULT_LOOPBACK,
474 "Failed to resolve a usable local IP address; advertising loopback"
475 );
476 format_host(DEFAULT_LOOPBACK)
477 }
478 }
479}
480
481fn format_host(address: IpAddr) -> String {
482 match address {
483 IpAddr::V6(_) => format!("[{address}]"),
484 IpAddr::V4(_) => address.to_string(),
485 }
486}
487
488#[cfg(test)]
489pub(crate) mod test_support {
490 use super::*;
491 use std::cell::Cell;
492
493 #[derive(Clone, Copy)]
494 pub(crate) enum ProbeOutcome {
495 Found(&'static str),
496 NotFound,
497 Strategy(&'static str),
498 Platform(&'static str),
499 }
500
501 impl ProbeOutcome {
502 fn result(self) -> Result<IpAddr, Error> {
503 match self {
504 Self::Found(address) => Ok(address.parse().expect("valid test address")),
505 Self::NotFound => Err(Error::LocalIpAddressNotFound),
506 Self::Strategy(message) => Err(Error::StrategyError(message.to_string())),
507 Self::Platform(platform) => Err(Error::PlatformNotSupported(platform.to_string())),
508 }
509 }
510
511 fn error(self) -> Error {
512 self.result().unwrap_err()
513 }
514 }
515
516 pub(crate) struct StubResolver {
517 pub(crate) ipv4: ProbeOutcome,
518 pub(crate) ipv6: ProbeOutcome,
519 pub(crate) interfaces: Vec<(&'static str, IpAddr)>,
520 pub(crate) down_interfaces: Vec<&'static str>,
521 pub(crate) interface_error: Option<ProbeOutcome>,
522 pub(crate) interface_calls: Cell<usize>,
523 }
524
525 impl StubResolver {
526 pub(crate) fn new(ipv4: ProbeOutcome, ipv6: ProbeOutcome) -> Self {
527 Self {
528 ipv4,
529 ipv6,
530 interfaces: vec![
531 ("lo", IpAddr::V4(Ipv4Addr::LOCALHOST)),
532 ("lo", IpAddr::V6(Ipv6Addr::LOCALHOST)),
533 ],
534 down_interfaces: Vec::new(),
535 interface_error: None,
536 interface_calls: Cell::new(0),
537 }
538 }
539
540 pub(crate) fn not_found() -> Self {
541 Self::new(ProbeOutcome::NotFound, ProbeOutcome::NotFound)
542 }
543 }
544
545 impl IpResolver for StubResolver {
546 fn local_ip(&self) -> Result<IpAddr, Error> {
547 self.ipv4.result()
548 }
549
550 fn local_ipv6(&self) -> Result<IpAddr, Error> {
551 self.ipv6.result()
552 }
553
554 fn list_afinet_netifas(&self) -> Result<Vec<(String, IpAddr)>, Error> {
555 self.interface_calls.set(self.interface_calls.get() + 1);
556 if let Some(error) = self.interface_error {
557 return Err(error.error());
558 }
559
560 Ok(self
561 .interfaces
562 .iter()
563 .map(|(name, address)| ((*name).to_string(), *address))
564 .collect())
565 }
566
567 fn list_up_afinet_netifas(&self) -> Result<Vec<(String, IpAddr)>, Error> {
568 Ok(self
569 .list_afinet_netifas()?
570 .into_iter()
571 .filter(|(name, _)| !self.down_interfaces.contains(&name.as_str()))
572 .collect())
573 }
574 }
575}
576
577#[cfg(test)]
578mod tests {
579 use super::test_support::{ProbeOutcome, StubResolver};
580 use super::*;
581 use ProbeOutcome::{Found, NotFound, Platform, Strategy};
582
583 fn ip(address: &str) -> IpAddr {
584 address.parse().unwrap()
585 }
586
587 fn interface(name: &'static str, addresses: &[&str]) -> Vec<(&'static str, IpAddr)> {
588 addresses
589 .iter()
590 .map(|address| (name, ip(address)))
591 .collect()
592 }
593
594 #[test]
595 fn ipv6_only_inventory_uses_global_ipv6_when_family_probes_fail() {
596 let mut resolver = StubResolver::new(Strategy("IPv4 failed"), NotFound);
597 resolver.interfaces = vec![
598 ("lo", ip("127.0.0.1")),
599 ("lo", ip("::1")),
600 ("eth0", ip("2001:db8::5")),
601 ];
602
603 let resolved = resolve_local_host(&resolver).unwrap();
604 assert_eq!(resolved.bind_ip(), ip("2001:db8::5"));
605 assert!(!resolved.used_loopback_fallback());
606 assert_eq!(resolver.interface_calls.get(), 1);
607 }
608
609 #[test]
610 fn routed_address_is_preferred_over_earlier_interfaces() {
611 let mut resolver = StubResolver::new(Found("10.3.10.72"), Found("2001:db8::72"));
612 resolver.interfaces = vec![
613 ("lo", ip("127.0.0.1")),
614 ("bmc_redfish0", ip("10.0.1.2")),
615 ("bmc_redfish0", ip("fd00::2")),
616 ("eth0", ip("10.3.10.72")),
617 ("eth0", ip("2001:db8::72")),
618 ];
619
620 assert_eq!(
621 resolve_local_host(&resolver).unwrap().advertise_ip(),
622 ip("10.3.10.72")
623 );
624 let wildcard = resolve_host_or_interface("0.0.0.0", &resolver).unwrap();
625 assert_eq!(wildcard.bind_ip(), ip("0.0.0.0"));
626 assert_eq!(wildcard.advertise_ip(), ip("10.3.10.72"));
627 assert_eq!(
628 resolve_advertise_ip_for_bind(ip("0.0.0.0"), &resolver).unwrap(),
629 ip("10.3.10.72")
630 );
631 assert_eq!(
632 resolve_advertise_ip_for_bind(ip("::"), &resolver).unwrap(),
633 ip("2001:db8::72")
634 );
635 assert_eq!(
636 resolve_host_or_interface("bmc_redfish0", &resolver)
637 .unwrap()
638 .advertise_ip(),
639 ip("10.0.1.2")
640 );
641 }
642
643 #[test]
644 fn routed_address_outside_up_snapshot_is_ignored() {
645 for routed in ["198.51.100.1", "192.0.2.99"] {
646 let mut resolver = StubResolver::new(Found(routed), NotFound);
647 resolver.interfaces = vec![
648 ("lo", ip("127.0.0.1")),
649 ("stale0", ip("198.51.100.1")),
650 ("eth0", ip("192.0.2.10")),
651 ];
652 resolver.down_interfaces.push("stale0");
653
654 assert_eq!(
655 resolve_local_host(&resolver).unwrap().advertise_ip(),
656 ip("192.0.2.10")
657 );
658 }
659 }
660
661 #[cfg(unix)]
662 #[test]
663 fn system_inventory_keeps_only_up_ip_addresses() {
664 use nix::{
665 ifaddrs::InterfaceAddress, net::if_::InterfaceFlags, sys::socket::SockaddrStorage,
666 };
667 use std::{net::SocketAddr, os::fd::AsRawFd, os::unix::net::UnixDatagram};
668
669 let socket = UnixDatagram::unbound().unwrap();
670 let unsupported =
671 nix::sys::socket::getsockname::<SockaddrStorage>(socket.as_raw_fd()).unwrap();
672 let up = InterfaceFlags::IFF_UP;
673 let entries = [
674 (
675 "stale0",
676 InterfaceFlags::empty(),
677 Some(SocketAddr::new(ip("198.51.100.1"), 0).into()),
678 ),
679 ("eth0", up, Some(SocketAddr::new(ip("192.0.2.1"), 0).into())),
680 (
681 "eth0",
682 up,
683 Some(SocketAddr::new(ip("2001:db8::1"), 0).into()),
684 ),
685 (
686 "lo",
687 up | InterfaceFlags::IFF_LOOPBACK,
688 Some(SocketAddr::new(ip("127.0.0.1"), 0).into()),
689 ),
690 ("unsupported", up, Some(unsupported)),
691 ("no-address", up, None),
692 ];
693 let interfaces = entries
694 .into_iter()
695 .map(|(name, flags, address)| InterfaceAddress {
696 interface_name: name.to_string(),
697 flags,
698 address,
699 netmask: None,
700 broadcast: None,
701 destination: None,
702 });
703
704 assert_eq!(
705 up_ip_interfaces(interfaces),
706 vec![
707 ("eth0".to_string(), ip("192.0.2.1")),
708 ("eth0".to_string(), ip("2001:db8::1")),
709 ("lo".to_string(), ip("127.0.0.1")),
710 ]
711 );
712 }
713
714 #[test]
715 fn automatic_selection_excludes_down_interfaces_but_explicit_lookup_keeps_them() {
716 for active in [Some("192.0.2.1"), Some("2001:db8::1"), None] {
717 let mut resolver = StubResolver::not_found();
718 resolver
719 .interfaces
720 .insert(0, ("stale0", ip("198.51.100.1")));
721 resolver.down_interfaces.push("stale0");
722 if let Some(address) = active {
723 resolver.interfaces.push(("eth0", ip(address)));
724 }
725 let expected = active.map(ip).unwrap_or(DEFAULT_LOOPBACK);
726 assert_eq!(
727 resolve_local_host(&resolver).unwrap().advertise_ip(),
728 expected
729 );
730 assert_eq!(
731 resolve_host_or_interface("0.0.0.0", &resolver)
732 .unwrap()
733 .advertise_ip(),
734 expected
735 );
736 assert_eq!(
737 resolve_advertise_ip_for_bind(unspecified_for(expected), &resolver).unwrap(),
738 expected
739 );
740 assert_eq!(
741 resolve_host_or_interface("stale0", &resolver)
742 .unwrap()
743 .advertise_ip(),
744 ip("198.51.100.1")
745 );
746 }
747 }
748
749 #[test]
750 fn configured_literals_are_parsed_and_validated() {
751 let resolver = StubResolver::not_found();
752
753 for (literal, expected) in [
754 ("192.0.2.10", "192.0.2.10"),
755 ("169.254.1.1", "169.254.1.1"),
756 ("2001:db8::2", "2001:db8::2"),
757 ("[2001:db8::2]", "2001:db8::2"),
758 ("fd00::2", "fd00::2"),
759 (" 127.0.0.1 ", "127.0.0.1"),
760 ("\t[::1] ", "::1"),
761 ("::ffff:192.0.2.10", "192.0.2.10"),
762 ("[::ffff:127.0.0.1]", "127.0.0.1"),
763 ] {
764 let resolved = resolve_host_or_interface(literal, &resolver).unwrap();
765 assert_eq!(resolved.bind_ip(), ip(expected));
766 assert_eq!(resolved.advertise_ip(), resolved.bind_ip());
767 }
768
769 for literal in ["[::1", "::1]", "[not-an-ip]", "192.0.2.999"] {
770 let error = resolve_host_or_interface(literal, &resolver)
771 .unwrap_err()
772 .to_string();
773 assert!(error.contains("invalid IP literal"), "{error}");
774 }
775
776 for literal in [
777 "224.0.0.1",
778 "255.255.255.255",
779 "fe80::1",
780 "ff02::1",
781 "::ffff:224.0.0.1",
782 "[::ffff:255.255.255.255]",
783 ] {
784 let error = resolve_host_or_interface(literal, &resolver).unwrap_err();
785 assert!(matches!(error, IpResolutionError::UnusableAddress(_)));
786 }
787 }
788
789 #[test]
790 fn wildcard_switches_bind_family_before_using_loopback() {
791 let mut resolver = StubResolver::new(NotFound, NotFound);
792 resolver.interfaces = interface("eth0", &["192.0.2.20"]);
793 let cases = [
794 ("0.0.0.0", ip("0.0.0.0"), ip("192.0.2.20")),
795 ("::", ip("0.0.0.0"), ip("192.0.2.20")),
796 ("[::]", ip("0.0.0.0"), ip("192.0.2.20")),
797 ("::ffff:0.0.0.0", ip("0.0.0.0"), ip("192.0.2.20")),
798 (" [::ffff:0.0.0.0] ", ip("0.0.0.0"), ip("192.0.2.20")),
799 ];
800
801 for (literal, bind_ip, advertise_ip) in cases {
802 let resolved = resolve_host_or_interface(literal, &resolver).unwrap();
803 assert_eq!(resolved.bind_ip(), bind_ip);
804 assert_eq!(resolved.advertise_ip(), advertise_ip);
805 }
806 }
807
808 #[test]
809 fn wildcard_prefers_loopback_in_requested_family() {
810 let mut resolver = StubResolver::not_found();
811 resolver.interfaces = vec![("lo", ip("127.0.0.1")), ("lo", ip("::1"))];
812
813 for (literal, expected_bind, expected_advertise) in [
814 ("0.0.0.0", ip("0.0.0.0"), ip("127.0.0.1")),
815 ("::", ip("::"), ip("::1")),
816 ("[::]", ip("::"), ip("::1")),
817 ("::ffff:0.0.0.0", ip("0.0.0.0"), ip("127.0.0.1")),
818 ] {
819 let resolved = resolve_host_or_interface(literal, &resolver).unwrap();
820 assert_eq!(resolved.bind_ip(), expected_bind);
821 assert_eq!(resolved.advertise_ip(), expected_advertise);
822 }
823
824 resolver.interfaces = vec![("lo", ip("127.0.0.1"))];
825 let resolved = resolve_host_or_interface("::", &resolver).unwrap();
826 assert_eq!(resolved.bind_ip(), ip("0.0.0.0"));
827 assert_eq!(resolved.advertise_ip(), ip("127.0.0.1"));
828
829 resolver.interfaces.clear();
830 let resolved = resolve_host_or_interface("::", &resolver).unwrap();
831 assert_eq!(resolved.bind_ip(), ip("::"));
832 assert_eq!(resolved.advertise_ip(), ip("::1"));
833 }
834
835 #[test]
836 fn interface_alias_preserves_order_and_accepts_ipv4_link_local() {
837 let mut resolver = StubResolver::not_found();
838 resolver.interfaces = interface("eth0:1", &["2001:db8::2", "169.254.10.5", "192.0.2.10"]);
839
840 for name in ["eth0:1", " eth0:1\t"] {
841 let resolved = resolve_host_or_interface(name, &resolver).unwrap();
842 assert_eq!(resolved.advertise_ip(), ip("169.254.10.5"));
843 }
844 }
845
846 #[test]
847 fn interface_errors_retain_context() {
848 let mut resolver = StubResolver::not_found();
849 resolver.interfaces = vec![("eth0", ip("fe80::1"))];
850
851 for (name, expected) in [
852 ("missing", "interface not found: missing"),
853 ("", "interface not found: "),
854 (" \t", "interface not found: "),
855 ("eth0", "interface has no usable IP address: eth0"),
856 ] {
857 let error = resolve_host_or_interface(name, &resolver)
858 .unwrap_err()
859 .to_string();
860 assert!(error.contains(expected), "{error}");
861 }
862
863 resolver.interface_error = Some(Platform("test-platform"));
864 let error = resolve_host_or_interface("eth0", &resolver)
865 .unwrap_err()
866 .to_string();
867 assert!(
868 error.contains("failed to enumerate network interfaces"),
869 "{error}"
870 );
871 assert!(error.contains("test-platform"), "{error}");
872 }
873
874 #[test]
875 fn loopback_only_host_preserves_ipv4_compatibility() {
876 let mut resolver = StubResolver::not_found();
877 resolver.interfaces = vec![
878 ("lo", IpAddr::V4(Ipv4Addr::LOCALHOST)),
879 ("lo", IpAddr::V6(Ipv6Addr::LOCALHOST)),
880 ];
881
882 let resolved = resolve_local_host(&resolver).unwrap();
883 assert_eq!(resolved.advertise_ip(), DEFAULT_LOOPBACK);
884 assert!(resolved.used_loopback_fallback());
885 }
886
887 #[test]
888 fn bound_wildcard_advertisement_stays_in_bound_family() {
889 let mut resolver = StubResolver::not_found();
890 resolver.interfaces = vec![
891 ("lo", ip("127.0.0.1")),
892 ("lo", ip("::1")),
893 ("eth0", ip("2001:db8::20")),
894 ];
895
896 assert_eq!(
897 resolve_advertise_ip_for_bind(ip("0.0.0.0"), &resolver).unwrap(),
898 ip("127.0.0.1")
899 );
900 assert_eq!(
901 resolve_advertise_ip_for_bind(ip("::"), &resolver).unwrap(),
902 ip("2001:db8::20")
903 );
904 assert_eq!(
905 resolve_advertise_ip_for_bind(ip("::ffff:0.0.0.0"), &resolver).unwrap(),
906 ip("127.0.0.1")
907 );
908 assert_eq!(
909 resolve_advertise_ip_for_bind(ip("::ffff:127.0.0.1"), &resolver).unwrap(),
910 ip("127.0.0.1")
911 );
912 }
913
914 #[test]
915 fn ipv6_wildcard_advertisement_falls_back_to_ipv4() {
916 let mut resolver = StubResolver::not_found();
917 resolver.interfaces = vec![
918 ("lo", ip("127.0.0.1")),
919 ("lo", ip("::1")),
920 ("eth0", ip("192.0.2.20")),
921 ];
922 assert_eq!(
923 resolve_advertise_ip_for_bind(ip("::"), &resolver).unwrap(),
924 ip("192.0.2.20")
925 );
926
927 resolver.interfaces.push(("eth0", ip("2001:db8::20")));
928 assert_eq!(
929 resolve_advertise_ip_for_bind(ip("::"), &resolver).unwrap(),
930 ip("2001:db8::20")
931 );
932
933 resolver.interfaces.retain(|(name, _)| *name == "lo");
934 assert_eq!(
935 resolve_advertise_ip_for_bind(ip("::"), &resolver).unwrap(),
936 ip("::1")
937 );
938 }
939
940 #[test]
941 fn mapped_interface_addresses_use_ipv4_classification() {
942 let mut resolver = StubResolver::not_found();
943 resolver.interfaces = interface(
944 "eth0",
945 &[
946 "::ffff:0.0.0.0",
947 "::ffff:224.0.0.1",
948 "::ffff:255.255.255.255",
949 "::ffff:127.0.0.1",
950 "2001:db8::20",
951 ],
952 );
953 assert_eq!(
954 resolve_local_host(&resolver).unwrap().advertise_ip(),
955 ip("2001:db8::20")
956 );
957 resolver.interfaces.push(("eth0", ip("::ffff:192.0.2.1")));
958 assert_eq!(
959 resolve_local_host(&resolver).unwrap().advertise_ip(),
960 ip("192.0.2.1")
961 );
962 assert_eq!(
963 resolve_host_or_interface("eth0", &resolver)
964 .unwrap()
965 .advertise_ip(),
966 ip("192.0.2.1")
967 );
968 }
969
970 #[test]
971 fn transient_fallbacks_are_not_cached_but_success_is() {
972 let cache = OnceLock::new();
973 let mut enumeration_error = StubResolver::not_found();
974 enumeration_error.interface_error = Some(Platform("transient"));
975 assert_eq!(
976 cached_local_ip_for_advertise(&cache, &enumeration_error),
977 "127.0.0.1"
978 );
979 assert!(cache.get().is_none());
980
981 let loopback = StubResolver::not_found();
982 assert_eq!(
983 cached_local_ip_for_advertise(&cache, &loopback),
984 "127.0.0.1"
985 );
986 assert!(cache.get().is_none());
987
988 let mut success = StubResolver::not_found();
989 success.interfaces.push(("eth0", ip("2001:db8::10")));
990 assert_eq!(
991 cached_local_ip_for_advertise(&cache, &success),
992 "[2001:db8::10]"
993 );
994 assert_eq!(cache.get(), Some(&ip("2001:db8::10")));
995
996 assert_eq!(
997 cached_local_ip_for_advertise(&cache, &loopback),
998 "[2001:db8::10]"
999 );
1000 }
1001
1002 #[test]
1003 fn legacy_resolver_implementation_remains_source_compatible() {
1004 struct LegacyResolver;
1005
1006 impl IpResolver for LegacyResolver {
1007 fn local_ip(&self) -> Result<IpAddr, Error> {
1008 Err(Error::LocalIpAddressNotFound)
1009 }
1010
1011 fn local_ipv6(&self) -> Result<IpAddr, Error> {
1012 Err(Error::LocalIpAddressNotFound)
1013 }
1014 }
1015
1016 fn assert_resolver<T: IpResolver>() {}
1017 assert_resolver::<LegacyResolver>();
1018 }
1019
1020 #[test]
1021 fn host_override_is_trimmed_and_empty_is_unset() {
1022 assert_eq!(
1023 host_override_from_lookup("TEST_HOST", |_| Some(OsString::from(" ib0 "))).unwrap(),
1024 Some("ib0".to_string())
1025 );
1026 assert_eq!(
1027 host_override_from_lookup("TEST_HOST", |_| Some(OsString::from(" \t"))).unwrap(),
1028 None
1029 );
1030 assert_eq!(
1031 host_override_from_lookup("TEST_HOST", |_| None).unwrap(),
1032 None
1033 );
1034 }
1035
1036 #[cfg(unix)]
1037 #[test]
1038 fn non_unicode_host_override_is_rejected() {
1039 use std::os::unix::ffi::OsStringExt;
1040
1041 let error =
1042 host_override_from_lookup("TEST_HOST", |_| Some(OsString::from_vec(vec![0xff])))
1043 .unwrap_err();
1044 assert_eq!(error.to_string(), "TEST_HOST must contain valid Unicode");
1045 }
1046}