From 0b2ac8d103347ca6d735314a5e9f3571171536ef Mon Sep 17 00:00:00 2001 From: Lukas Rieger Date: Tue, 2 Dec 2025 12:53:31 +0100 Subject: [PATCH] [ROS1/TCPROS] Improve self-ip heuristics. When neither ROS_IP nor ROS_HOSTNAME are set, use a slightly smarter heuristic to try to find an IP address in the same subnet as the rosmaster. This only works if the rosmaster is specified as an IP address, not if it is a hostname. --- Cargo.lock | 20 ++++++++ roslibrust_ros1/Cargo.toml | 1 + roslibrust_ros1/src/node/handle.rs | 2 +- roslibrust_ros1/src/node/mod.rs | 75 ++++++++++++++++++++++++------ 4 files changed, 82 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e8262f95..03575021 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1602,6 +1602,16 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "if-addrs" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf39cc0423ee66021dc5eccface85580e4a001e0c5288bae8bea7ecb69225e90" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -3069,6 +3079,7 @@ dependencies = [ "byteorder", "gethostname", "hyper", + "if-addrs", "lazy_static", "log", "regex", @@ -4875,6 +4886,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.60.2" diff --git a/roslibrust_ros1/Cargo.toml b/roslibrust_ros1/Cargo.toml index f4ce6220..7a2a6911 100644 --- a/roslibrust_ros1/Cargo.toml +++ b/roslibrust_ros1/Cargo.toml @@ -32,6 +32,7 @@ regex = { version = "1.9" } byteorder = "1.4" thiserror = "2.0" anyhow = "1.0" +if-addrs = "0.14.0" [dev-dependencies] # Used for message definitions in tests diff --git a/roslibrust_ros1/src/node/handle.rs b/roslibrust_ros1/src/node/handle.rs index cc0be1b5..9bffa599 100644 --- a/roslibrust_ros1/src/node/handle.rs +++ b/roslibrust_ros1/src/node/handle.rs @@ -32,7 +32,7 @@ impl NodeHandle { let _ = Name::new("test").unwrap().resolve_to_global(&name); // Follow ROS rules and determine our IP and hostname - let (addr, hostname) = super::determine_addr().await?; + let (addr, hostname) = super::determine_addr(master_uri).await?; let node = Node::new(master_uri, &hostname, &name, addr).await?; let nh = NodeHandle { inner: node }; diff --git a/roslibrust_ros1/src/node/mod.rs b/roslibrust_ros1/src/node/mod.rs index 4f417656..8b876661 100644 --- a/roslibrust_ros1/src/node/mod.rs +++ b/roslibrust_ros1/src/node/mod.rs @@ -1,6 +1,7 @@ //! This module contains the top level Node and NodeHandle classes. //! These wrap the lower level management of a ROS Node connection into a higher level and thread safe API. +use if_addrs::Interface; use roslibrust_common::Error; use super::{names::InvalidNameError, RosMasterError}; @@ -29,8 +30,8 @@ pub struct ProtocolParams { /// Following ROS's idiomatic address rules uses ROS_HOSTNAME and ROS_IP to determine the address that server should be hosted at. /// Returns both the resolved IpAddress of the host (used for actually opening the socket), and the String "hostname" which should /// be used in the URI. -async fn determine_addr() -> Result<(Ipv4Addr, String), RosMasterError> { - // If ROS_IP is set that trumps anything else +async fn determine_addr(master_uri: &str) -> Result<(Ipv4Addr, String), RosMasterError> { + // If ROS_IP is set, that trumps anything else if let Ok(ip_str) = std::env::var("ROS_IP") { let ip = ip_str.parse().map_err(|e| { RosMasterError::HostIpResolutionFailure(format!( @@ -39,41 +40,85 @@ async fn determine_addr() -> Result<(Ipv4Addr, String), RosMasterError> { })?; return Ok((ip, ip_str)); } - // If ROS_HOSTNAME is set that is next highest precedent + // If ROS_HOSTNAME is set, that is next highest precedent if let Ok(name) = std::env::var("ROS_HOSTNAME") { let ip = hostname_to_ipv4(&name).await?; return Ok((ip, name)); } + // If neither env var is set, use the computers "hostname" let name = gethostname::gethostname(); let name = name.into_string().map_err(|e| { RosMasterError::HostIpResolutionFailure(format!("This host's hostname is a string that cannot be validly converted into a Rust type, and therefore we cannot convert it into an IpAddrv4: {e:?}")) })?; + + // Try to find an IP in the same subnet as the ROS master + if let Some(master_ip) = try_get_master_ip(master_uri) { + if let Ok(local_interfaces) = if_addrs::get_if_addrs() { + if let Some(ip) = try_find_addr_in_same_subnet(master_ip, &local_interfaces) { + return Ok((ip, name)); + } + } + } + + // Fallback to just use the first ip we can find let ip = hostname_to_ipv4(&name).await?; Ok((ip, name)) } +fn try_find_addr_in_same_subnet( + master_ip: Ipv4Addr, + local_interfaces: &Vec, +) -> Option { + for iface in local_interfaces { + if let if_addrs::IfAddr::V4(ifv4) = &iface.addr { + if is_in_same_subnet(ifv4.ip, master_ip, ifv4.netmask) { + return Some(ifv4.ip); + } + } + } + None +} + +fn try_get_master_ip(master_uri: &str) -> Option { + let s = master_uri + .strip_prefix("http://") + .or_else(|| master_uri.strip_prefix("https://")) + .unwrap_or(master_uri); + let host = s.split(':').next()?; + host.parse::().ok() +} + +fn is_in_same_subnet(ip1: Ipv4Addr, ip2: Ipv4Addr, mask: Ipv4Addr) -> bool { + let ip1_octets = ip1.octets(); + let ip2_octets = ip2.octets(); + let mask_octets = mask.octets(); + + for i in 0..4 { + if (ip1_octets[i] & mask_octets[i]) != (ip2_octets[i] & mask_octets[i]) { + return false; + } + } + true +} + /// Given a the name of a host use's std::net::ToSocketAddrs to perform a DNS lookup and return the resulting IP address. /// This function is intended to be used to determine the correct IP host the socket for the xmlrpc server on. async fn hostname_to_ipv4(name: &str) -> Result { let name_with_port = &format!("{name}:0"); - let mut i = tokio::net::lookup_host(name_with_port).await.map_err(|e| { + let i = tokio::net::lookup_host(name_with_port).await.map_err(|e| { RosMasterError::HostIpResolutionFailure(format!( "Failure while attempting to lookup ROS_HOSTNAME: {e:?}" )) })?; - if let Some(addr) = i.next() { - match addr.ip() { - IpAddr::V4(ip) => Ok(ip), - IpAddr::V6(ip) => { - Err(RosMasterError::HostIpResolutionFailure(format!("ROS_HOSTNAME resolved to an IPv6 address which is not support by ROS/roslibrust: {ip:?}"))) - } - } - } else { - Err(RosMasterError::HostIpResolutionFailure(format!( - "ROS_HOSTNAME did not resolve any address: {name:?}" - ))) + for addr in i { + if let IpAddr::V4(ip) = addr.ip() { + return Ok(ip); + } } + Err(RosMasterError::HostIpResolutionFailure(format!( + "ROS_HOSTNAME resolved to no IPv4 addresses: {name:?}" + ))) } #[derive(thiserror::Error, Debug)]