use std::net::{Ipv4Addr, SocketAddrV4};
const PROBE_HOST: Ipv4Addr = Ipv4Addr::new(1, 1, 1, 1);
const PROBE_PORT: u16 = 53;
pub async fn detect_lan_ip() -> Option<Ipv4Addr> {
if let Some(ip) = probe_default_route().await {
if let Some(iface) = find_interface_for_ip(&ip) {
if !is_virtual_interface(&iface) {
return Some(ip);
}
} else {
if !ip.is_loopback() && !ip.is_link_local() {
return Some(ip);
}
}
}
fallback_interface_ip()
}
pub async fn detect_lan_ip_if_changed(last: Ipv4Addr) -> Option<Ipv4Addr> {
let current = detect_lan_ip().await?;
(current != last).then_some(current)
}
async fn probe_default_route() -> Option<Ipv4Addr> {
let sock = tokio::net::UdpSocket::bind("0.0.0.0:0").await.ok()?;
let dest = SocketAddrV4::new(PROBE_HOST, PROBE_PORT);
sock.connect(dest).await.ok()?;
let local = sock.local_addr().ok()?;
let ip = local.ip();
match ip {
std::net::IpAddr::V4(v4) if !v4.is_unspecified() && !v4.is_loopback() => Some(v4),
_ => None,
}
}
struct InterfaceInfo {
name: String,
ip: Ipv4Addr,
is_loopback: bool,
}
#[cfg(unix)]
fn find_interface_for_ip(target: &Ipv4Addr) -> Option<InterfaceInfo> {
let addrs = nix::ifaddrs::getifaddrs().ok()?;
for iface in addrs {
let flags = iface.flags;
let is_loopback = flags.contains(nix::net::if_::InterfaceFlags::IFF_LOOPBACK);
let ip = match iface
.address
.as_ref()
.and_then(|a| a.as_sockaddr_in().map(|sa| sa.ip()))
{
Some(ip) => ip,
None => continue,
};
if &ip == target {
return Some(InterfaceInfo {
name: iface.interface_name.clone(),
ip,
is_loopback,
});
}
}
None
}
#[cfg(unix)]
fn is_virtual_interface(iface: &InterfaceInfo) -> bool {
if iface.is_loopback {
return true;
}
if iface.ip.is_link_local() {
return true;
}
let name = iface.name.to_lowercase();
if name.starts_with("veth") || name.starts_with("br-") || name.starts_with("docker") {
return true;
}
if name.starts_with("virbr") {
return true;
}
if name.starts_with("vethernet") || name.starts_with("bridge") {
return true;
}
false
}
#[cfg(unix)]
fn fallback_interface_ip() -> Option<Ipv4Addr> {
let addrs = nix::ifaddrs::getifaddrs().ok()?;
for iface in addrs {
let flags = iface.flags;
if !flags.contains(nix::net::if_::InterfaceFlags::IFF_UP) {
continue;
}
if flags.contains(nix::net::if_::InterfaceFlags::IFF_LOOPBACK) {
continue;
}
let ip = match iface
.address
.as_ref()
.and_then(|a| a.as_sockaddr_in().map(|sa| sa.ip()))
{
Some(ip) => ip,
None => continue,
};
if ip.is_loopback() || ip.is_link_local() {
continue;
}
let info = InterfaceInfo {
name: iface.interface_name.clone(),
ip,
is_loopback: false,
};
if !is_virtual_interface(&info) {
return Some(ip);
}
}
None
}
#[cfg(not(unix))]
fn find_interface_for_ip(_target: &Ipv4Addr) -> Option<InterfaceInfo> {
None
}
#[cfg(not(unix))]
fn is_virtual_interface(_iface: &InterfaceInfo) -> bool {
false
}
#[cfg(not(unix))]
fn fallback_interface_ip() -> Option<Ipv4Addr> {
None
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(unix)]
#[test]
fn test_is_virtual_interface_loopback() {
let iface = InterfaceInfo {
name: "lo".to_string(),
ip: Ipv4Addr::new(127, 0, 0, 1),
is_loopback: true,
};
assert!(is_virtual_interface(&iface));
}
#[cfg(unix)]
#[test]
fn test_is_virtual_interface_docker() {
for name in &["veth1234", "br-abc", "docker0"] {
let iface = InterfaceInfo {
name: name.to_string(),
ip: Ipv4Addr::new(172, 17, 0, 1),
is_loopback: false,
};
assert!(
is_virtual_interface(&iface),
"expected {name} to be virtual"
);
}
}
#[cfg(unix)]
#[test]
fn test_is_virtual_interface_normal() {
let iface = InterfaceInfo {
name: "eth0".to_string(),
ip: Ipv4Addr::new(192, 168, 1, 42),
is_loopback: false,
};
assert!(!is_virtual_interface(&iface));
}
#[cfg(unix)]
#[test]
fn test_is_virtual_interface_link_local() {
let iface = InterfaceInfo {
name: "en0".to_string(),
ip: Ipv4Addr::new(169, 254, 1, 1),
is_loopback: false,
};
assert!(is_virtual_interface(&iface));
}
}