Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions docs/fern/pages/reference/components/runtime-configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,12 @@ Unless a field is marked environment-only, it has both a CLI flag and an environ
<ParamField path="DYN_TCP_RPC_HOST" type="string" default="auto-detected local address">
Host for the TCP request-plane server. Accepts IPv4 and IPv6 literals, bracketed IPv6 literals,
wildcards, and interface names, including aliases such as `eth0:1`. If unset or empty, Dynamo
selects the first usable non-loopback IPv4 address, then IPv6, then IPv4 loopback, then IPv6
loopback. On Unix, automatic selection excludes interfaces that are down. Dynamo trims surrounding
whitespace from configured hosts; a value containing only whitespace still fails server startup.
selects a usable non-loopback IPv4 address, then IPv6, then IPv4 loopback, then IPv6 loopback.
Within each family, it prefers the address the operating system uses for outbound traffic (on
Linux, the preferred source address of the route to an external address) when an up interface
carries it, and otherwise the first usable address in operating-system enumeration order. On Unix,
automatic selection excludes interfaces that are down. Dynamo trims surrounding whitespace from
configured hosts; a value containing only whitespace still fails server startup.

For a named interface, Dynamo uses the same address selection order, restricted to that interface.
It selects the first usable address in each category in operating-system enumeration order. An
Expand Down Expand Up @@ -207,7 +210,8 @@ Unless a field is marked environment-only, it has both a CLI flag and an environ
For an interface name, Dynamo selects the first usable IPv4 address, then IPv6. Dynamo trims
whitespace and treats an empty value as unset. Unspecified addresses such as `0.0.0.0` and `::` are
invalid, including IPv4-mapped wildcards such as `::ffff:0.0.0.0`. When unset, Dynamo selects a
usable non-loopback IPv4 address first, then IPv6. On Unix, automatic selection excludes down
usable non-loopback IPv4 address first, then IPv6, preferring the operating system's routed
source address as described for `DYN_TCP_RPC_HOST`. On Unix, automatic selection excludes down
interfaces. If neither family has a usable non-loopback address, Dynamo selects IPv4 loopback,
then IPv6 loopback, and falls back to `127.0.0.1` if neither is found. Remote subscribers cannot
reach a loopback address. Automatic selection can choose an IPv4 link-local address or a bridge
Expand Down
86 changes: 76 additions & 10 deletions lib/runtime/src/utils/ip_resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,25 @@ fn local_candidates<R: IpResolver>(resolver: &R) -> Result<AddressCandidates, Ip
.list_up_afinet_netifas()
.map_err(IpResolutionError::InterfaceEnumeration)?;
let mut candidates = AddressCandidates::default();

// Prefer the source address the operating system routes outbound traffic from
// (on Linux, the preferred source of the route to an external address). Hosts
// often have node-local interfaces, such as a BMC Redfish host interface, that
// enumerate before the cluster interface. A routed address is used only if an
// up interface in this snapshot carries it; failed probes are ignored.
for routed in [resolver.local_ip(), resolver.local_ipv6()]
.into_iter()
.flatten()
{
let routed = routed.to_canonical();
if interfaces
.iter()
.any(|(_, address)| address.to_canonical() == routed)
{
candidates.consider(routed);
}
}

for (_, address) in interfaces {
candidates.consider(address);
}
Expand Down Expand Up @@ -473,6 +492,7 @@ pub(crate) mod test_support {

#[derive(Clone, Copy)]
pub(crate) enum ProbeOutcome {
Found(&'static str),
NotFound,
Strategy(&'static str),
Platform(&'static str),
Expand All @@ -481,6 +501,7 @@ pub(crate) mod test_support {
impl ProbeOutcome {
fn result(self) -> Result<IpAddr, Error> {
match self {
Self::Found(address) => Ok(address.parse().expect("valid test address")),
Self::NotFound => Err(Error::LocalIpAddressNotFound),
Self::Strategy(message) => Err(Error::StrategyError(message.to_string())),
Self::Platform(platform) => Err(Error::PlatformNotSupported(platform.to_string())),
Expand All @@ -498,8 +519,6 @@ pub(crate) mod test_support {
pub(crate) interfaces: Vec<(&'static str, IpAddr)>,
pub(crate) down_interfaces: Vec<&'static str>,
pub(crate) interface_error: Option<ProbeOutcome>,
pub(crate) ipv4_calls: Cell<usize>,
pub(crate) ipv6_calls: Cell<usize>,
pub(crate) interface_calls: Cell<usize>,
}

Expand All @@ -514,8 +533,6 @@ pub(crate) mod test_support {
],
down_interfaces: Vec::new(),
interface_error: None,
ipv4_calls: Cell::new(0),
ipv6_calls: Cell::new(0),
interface_calls: Cell::new(0),
}
}
Expand All @@ -527,12 +544,10 @@ pub(crate) mod test_support {

impl IpResolver for StubResolver {
fn local_ip(&self) -> Result<IpAddr, Error> {
self.ipv4_calls.set(self.ipv4_calls.get() + 1);
self.ipv4.result()
}

fn local_ipv6(&self) -> Result<IpAddr, Error> {
self.ipv6_calls.set(self.ipv6_calls.get() + 1);
self.ipv6.result()
}

Expand Down Expand Up @@ -563,7 +578,7 @@ pub(crate) mod test_support {
mod tests {
use super::test_support::{ProbeOutcome, StubResolver};
use super::*;
use ProbeOutcome::{NotFound, Platform, Strategy};
use ProbeOutcome::{Found, NotFound, Platform, Strategy};

fn ip(address: &str) -> IpAddr {
address.parse().unwrap()
Expand All @@ -577,7 +592,7 @@ mod tests {
}

#[test]
fn ipv6_only_inventory_uses_global_ipv6_without_family_probes() {
fn ipv6_only_inventory_uses_global_ipv6_when_family_probes_fail() {
let mut resolver = StubResolver::new(Strategy("IPv4 failed"), NotFound);
resolver.interfaces = vec![
("lo", ip("127.0.0.1")),
Expand All @@ -588,11 +603,62 @@ mod tests {
let resolved = resolve_local_host(&resolver).unwrap();
assert_eq!(resolved.bind_ip(), ip("2001:db8::5"));
assert!(!resolved.used_loopback_fallback());
assert_eq!(resolver.ipv4_calls.get(), 0);
assert_eq!(resolver.ipv6_calls.get(), 0);
assert_eq!(resolver.interface_calls.get(), 1);
}

#[test]
fn routed_address_is_preferred_over_earlier_interfaces() {
// A node-local BMC interface enumerates before the cluster interface.
Comment thread
nvpohanh marked this conversation as resolved.
Resolved
let mut resolver = StubResolver::new(Found("10.3.10.72"), Found("2001:db8::72"));
resolver.interfaces = vec![
("lo", ip("127.0.0.1")),
("bmc_redfish0", ip("10.0.1.2")),
("bmc_redfish0", ip("fd00::2")),
("eth0", ip("10.3.10.72")),
("eth0", ip("2001:db8::72")),
];

assert_eq!(
resolve_local_host(&resolver).unwrap().advertise_ip(),
ip("10.3.10.72")
);
let wildcard = resolve_host_or_interface("0.0.0.0", &resolver).unwrap();
assert_eq!(wildcard.bind_ip(), ip("0.0.0.0"));
assert_eq!(wildcard.advertise_ip(), ip("10.3.10.72"));
assert_eq!(
resolve_advertise_ip_for_bind(ip("0.0.0.0"), &resolver).unwrap(),
ip("10.3.10.72")
);
assert_eq!(
resolve_advertise_ip_for_bind(ip("::"), &resolver).unwrap(),
ip("2001:db8::72")
);
assert_eq!(
resolve_host_or_interface("bmc_redfish0", &resolver)
.unwrap()
.advertise_ip(),
ip("10.0.1.2")
);
}

#[test]
fn routed_address_outside_up_snapshot_is_ignored() {
for routed in ["198.51.100.1", "192.0.2.99"] {
Comment thread
nvpohanh marked this conversation as resolved.
Resolved
let mut resolver = StubResolver::new(Found(routed), NotFound);
resolver.interfaces = vec![
("lo", ip("127.0.0.1")),
("stale0", ip("198.51.100.1")),
("eth0", ip("192.0.2.10")),
];
resolver.down_interfaces.push("stale0");

assert_eq!(
resolve_local_host(&resolver).unwrap().advertise_ip(),
ip("192.0.2.10")
);
}
}

#[cfg(unix)]
#[test]
fn system_inventory_keeps_only_up_ip_addresses() {
Expand Down
Loading