From 59d8939c8c1301670adcbff6031febc270c8f45b Mon Sep 17 00:00:00 2001 From: "alpi.tolvanen" Date: Wed, 11 Nov 2026 13:18:11 +0200 Subject: [PATCH] Announce only running interfaces, in a stable order The unicast locators in SPDP/SEDP announcements listed every local IPv4 address, including interfaces that cannot pass traffic (an idle docker0 bridge, an unplugged NIC, Jetson's l4tbr0), in random order because netdev collects interfaces through a HashMap. Fast DDS uses only the first 4 unicast locators by default. On a host with more addresses, whether the reachable address made it into the first 4 was decided by chance at every process start, so discovery with Fast DDS peers worked or failed seemingly at random. - Sort the enumerated interfaces by index and address. - Do not announce addresses of non-running (IFF_RUNNING unset) interfaces. Loopback is always kept for same-host peers, and an explicit only_networks selection is honored as before. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/network/util.rs | 51 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/src/network/util.rs b/src/network/util.rs index 7752daf6..bd441acc 100644 --- a/src/network/util.rs +++ b/src/network/util.rs @@ -29,6 +29,10 @@ pub(crate) struct IfAddr { pub is_loopback: bool, /// Whether the interface is multicast-capable. pub is_multicast: bool, + /// Whether the interface can currently pass traffic (Unix `IFF_RUNNING`: + /// administratively up *and* has carrier). E.g. a container bridge without + /// members or an unplugged NIC keeps its address but is not running. + pub is_running: bool, /// IPv4 subnet mask for `ip` (only populated for IPv4 addresses). Used to /// decide whether a peer is on the same subnet (i.e. reachable in one hop). pub netmask: Option, @@ -49,6 +53,12 @@ pub(crate) struct IfAddr { /// the existing locator/multicast/ifindex helpers keep working unchanged, while /// the new fields (`netmask`, `mtu`) enable per-peer path-MTU resolution. /// +/// The result is sorted by interface index and address. `netdev` returns the +/// interfaces in random (`HashMap`) order, which would make the order of our +/// announced locators differ between processes and runs. That matters because +/// some implementations use only the first few locators (Fast DDS keeps 4 by +/// default). +/// /// This is infallible in practice (netdev returns an empty list rather than an /// error), but the signature is kept as `io::Result` for API compatibility with /// the previous per-platform implementations. @@ -59,6 +69,7 @@ fn enumerate_interfaces() -> io::Result> { let index = iface.index; let is_loopback = iface.is_loopback(); let is_multicast = iface.is_multicast(); + let is_running = iface.is_running(); let mtu = iface.mtu; for net in &iface.ipv4 { @@ -67,6 +78,7 @@ fn enumerate_interfaces() -> io::Result> { index, is_loopback, is_multicast, + is_running, netmask: Some(net.netmask()), mtu, }); @@ -77,6 +89,7 @@ fn enumerate_interfaces() -> io::Result> { index, is_loopback, is_multicast, + is_running, // IPv4-only subnet matching for now; IPv6 peers fall back to default. netmask: None, mtu, @@ -84,6 +97,7 @@ fn enumerate_interfaces() -> io::Result> { } } + result.sort_by_key(|ifa| (ifa.index, ifa.ip)); Ok(result) } @@ -215,7 +229,15 @@ fn get_local_unicast_locators_inner( // loopback -- would try to send there and get EAFNOSUPPORT per datagram. // Link-local IPv6 is doubly useless, as a `Locator` carries no scope id. .filter(|ifa| ifa.ip.is_ipv4()) - .filter(|ifa| only_networks.is_none_or(|nets| nets.contains(&ifa.ip))) + // Do not announce addresses of interfaces that cannot pass traffic, such + // as an idle `docker0`. Peers cannot reach us there, and implementations + // that use only the first few locators may drop the reachable ones. + // Loopback is kept for same-host peers, and an explicit `only_networks` + // selection is honored as is. + .filter(|ifa| match only_networks { + Some(nets) => nets.contains(&ifa.ip), + None => ifa.is_running || ifa.is_loopback, + }) .map(|ifa| Locator::from(SocketAddr::new(ifa.ip, port))) .collect() } @@ -308,6 +330,7 @@ mod tests { index, is_loopback, is_multicast, + is_running: true, netmask: None, mtu: None, } @@ -326,6 +349,7 @@ mod tests { index: 1, is_loopback, is_multicast: !is_loopback, + is_running: true, netmask: Some(Ipv4Addr::from(mask)), mtu: Some(mtu), } @@ -443,6 +467,31 @@ mod tests { ); } + #[test] + fn unicast_locators_exclude_non_running_interfaces() { + let lo = iface(IpAddr::V4(Ipv4Addr::LOCALHOST), 1, true, false); + let eth0 = iface(v4(192, 168, 0, 10), 2, false, true); + let docker0 = IfAddr { + is_running: false, + ..iface(v4(172, 17, 0, 1), 3, false, true) + }; + let ifaces = [lo, eth0, docker0]; + let locator = |ip| Locator::from(SocketAddr::new(ip, 7412)); + + assert_eq!( + get_local_unicast_locators_inner(&ifaces, 7412, None), + vec![ + locator(IpAddr::V4(Ipv4Addr::LOCALHOST)), + locator(v4(192, 168, 0, 10)) + ] + ); + // An explicit selection is honored even if the interface is not running. + assert_eq!( + get_local_unicast_locators_inner(&ifaces, 7412, Some(&[v4(172, 17, 0, 1)])), + vec![locator(v4(172, 17, 0, 1))] + ); + } + #[test] fn ifindex_map_prefers_ipv4_and_skips_index_zero() { let ifaces = vec![