use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::time::Duration;
const PUBLIC_IP_TIMEOUT: Duration = Duration::from_secs(3);
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct HostIps {
pub local_ipv4: Option<Ipv4Addr>,
pub local_ipv6: Option<Ipv6Addr>,
pub public_ipv4: Option<Ipv4Addr>,
pub public_ipv6: Option<Ipv6Addr>,
}
impl HostIps {
pub async fn detect() -> Self {
let local_ipv4 = detect_local_ipv4();
let local_ipv6 = detect_local_ipv6();
let (public_ipv4, public_ipv6) = tokio::join!(detect_public_ipv4(), detect_public_ipv6());
Self {
local_ipv4,
local_ipv6,
public_ipv4,
public_ipv6,
}
}
}
fn detect_local_ipv4() -> Option<Ipv4Addr> {
match local_ip_address::local_ip() {
Ok(IpAddr::V4(addr)) if is_routable_ipv4(&addr) => Some(addr),
Ok(addr) => {
tracing::debug!(addr = %addr, "local_ip() returned non-routable or non-v4 address");
None
}
Err(e) => {
tracing::debug!(error = %e, "local IPv4 detection failed");
None
}
}
}
fn detect_local_ipv6() -> Option<Ipv6Addr> {
match local_ip_address::local_ipv6() {
Ok(IpAddr::V6(addr)) if is_routable_ipv6(&addr) => Some(addr),
Ok(addr) => {
tracing::debug!(addr = %addr, "local_ipv6() returned non-routable or non-v6 address");
None
}
Err(e) => {
tracing::debug!(error = %e, "local IPv6 detection failed");
None
}
}
}
async fn detect_public_ipv4() -> Option<Ipv4Addr> {
match tokio::time::timeout(PUBLIC_IP_TIMEOUT, public_ip::addr_v4()).await {
Ok(Some(addr)) => Some(addr),
Ok(None) => {
tracing::debug!("public IPv4 resolvers returned no address");
None
}
Err(_) => {
tracing::debug!(
timeout_ms = PUBLIC_IP_TIMEOUT.as_millis() as u64,
"public IPv4 resolution timed out"
);
None
}
}
}
async fn detect_public_ipv6() -> Option<Ipv6Addr> {
match tokio::time::timeout(PUBLIC_IP_TIMEOUT, public_ip::addr_v6()).await {
Ok(Some(addr)) => Some(addr),
Ok(None) => {
tracing::debug!("public IPv6 resolvers returned no address");
None
}
Err(_) => {
tracing::debug!(
timeout_ms = PUBLIC_IP_TIMEOUT.as_millis() as u64,
"public IPv6 resolution timed out"
);
None
}
}
}
fn is_routable_ipv4(addr: &Ipv4Addr) -> bool {
!addr.is_loopback() && !addr.is_unspecified()
}
fn is_routable_ipv6(addr: &Ipv6Addr) -> bool {
!addr.is_loopback() && !addr.is_unspecified() && !is_unicast_link_local(addr)
}
fn is_unicast_link_local(addr: &Ipv6Addr) -> bool {
(addr.segments()[0] & 0xffc0) == 0xfe80
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn loopback_ipv4_is_not_routable() {
assert!(!is_routable_ipv4(&Ipv4Addr::new(127, 0, 0, 1)));
}
#[test]
fn unspecified_ipv4_is_not_routable() {
assert!(!is_routable_ipv4(&Ipv4Addr::UNSPECIFIED));
}
#[test]
fn private_ipv4_is_routable() {
assert!(is_routable_ipv4(&Ipv4Addr::new(192, 168, 1, 42)));
assert!(is_routable_ipv4(&Ipv4Addr::new(10, 0, 0, 1)));
assert!(is_routable_ipv4(&Ipv4Addr::new(172, 16, 0, 1)));
}
#[test]
fn link_local_ipv6_is_not_routable() {
let fe80 = "fe80::1".parse::<Ipv6Addr>().unwrap();
assert!(!is_routable_ipv6(&fe80));
}
#[test]
fn loopback_ipv6_is_not_routable() {
assert!(!is_routable_ipv6(&Ipv6Addr::LOCALHOST));
}
#[test]
fn global_ipv6_is_routable() {
let global = "2001:db8::1".parse::<Ipv6Addr>().unwrap();
assert!(is_routable_ipv6(&global));
}
#[test]
fn unique_local_ipv6_is_routable() {
let ula = "fc00::1".parse::<Ipv6Addr>().unwrap();
assert!(is_routable_ipv6(&ula));
}
#[test]
fn unicast_link_local_check_matches_rfc_prefix() {
for seg in [0xfe80, 0xfe81, 0xfebf] {
let addr = Ipv6Addr::new(seg, 0, 0, 0, 0, 0, 0, 1);
assert!(
is_unicast_link_local(&addr),
"{} should be link-local",
addr
);
}
for seg in [0xfec0, 0xff00, 0x2001] {
let addr = Ipv6Addr::new(seg, 0, 0, 0, 0, 0, 0, 1);
assert!(
!is_unicast_link_local(&addr),
"{} should not be link-local",
addr
);
}
}
#[tokio::test]
async fn detect_completes_and_returns_struct() {
let ips = HostIps::detect().await;
let _ = ips.local_ipv4;
let _ = ips.local_ipv6;
let _ = ips.public_ipv4;
let _ = ips.public_ipv6;
}
}