minimoon-sync-server 0.3.0

Standalone LAN file sharing server for Minimoon Sync
Documentation
use anyhow::{anyhow, Context, Result};
use std::net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket};

#[cfg(unix)]
use std::{cmp::Reverse, ffi::CStr, ptr};

const LISTEN_PORT: u16 = 41324;

pub fn listen_port() -> u16 {
    LISTEN_PORT
}

pub fn preferred_bind_addr() -> Result<SocketAddr> {
    preferred_bind_addr_for_port(LISTEN_PORT)
}

pub fn preferred_bind_addr_for_port(port: u16) -> Result<SocketAddr> {
    Ok(SocketAddr::from((preferred_bind_ip()?, port)))
}

pub fn preferred_bind_ip() -> Result<Ipv4Addr> {
    #[cfg(unix)]
    if let Some(ip) = find_preferred_private_ipv4()? {
        return Ok(ip);
    }

    if let Some(ip) = find_routed_private_ipv4()? {
        return Ok(ip);
    }

    Err(anyhow!("No private LAN IPv4 address is available"))
}

fn find_routed_private_ipv4() -> Result<Option<Ipv4Addr>> {
    let socket = UdpSocket::bind("0.0.0.0:0").context("failed to create route lookup socket")?;
    socket
        .connect("8.8.8.8:80")
        .context("failed to determine default IPv4 route")?;

    match socket
        .local_addr()
        .context("failed to read routed local address")?
        .ip()
    {
        IpAddr::V4(ip) if is_usable_private_ipv4(ip) => Ok(Some(ip)),
        _ => Ok(None),
    }
}

#[cfg(unix)]
fn find_preferred_private_ipv4() -> Result<Option<Ipv4Addr>> {
    let mut interfaces = ptr::null_mut();
    if unsafe { libc::getifaddrs(&mut interfaces) } != 0 {
        return Err(std::io::Error::last_os_error()).context("getifaddrs failed");
    }

    let result = unsafe { collect_preferred_private_ipv4(interfaces) };
    unsafe { libc::freeifaddrs(interfaces) };
    result
}

#[cfg(unix)]
unsafe fn collect_preferred_private_ipv4(
    interfaces: *mut libc::ifaddrs,
) -> Result<Option<Ipv4Addr>> {
    let mut candidates = Vec::new();
    let mut current = interfaces;

    while !current.is_null() {
        let interface = &*current;

        if interface.ifa_addr.is_null() || interface.ifa_name.is_null() {
            current = interface.ifa_next;
            continue;
        }

        let flags = interface.ifa_flags as i32;
        if flags & libc::IFF_UP == 0
            || flags & libc::IFF_LOOPBACK != 0
            || flags & libc::IFF_POINTOPOINT != 0
        {
            current = interface.ifa_next;
            continue;
        }

        if (*interface.ifa_addr).sa_family as i32 != libc::AF_INET {
            current = interface.ifa_next;
            continue;
        }

        let name = CStr::from_ptr(interface.ifa_name)
            .to_string_lossy()
            .into_owned();
        if is_excluded_interface(&name) {
            current = interface.ifa_next;
            continue;
        }

        let sockaddr = *(interface.ifa_addr as *const libc::sockaddr_in);
        let ip = Ipv4Addr::from(u32::from_be(sockaddr.sin_addr.s_addr));
        if !is_usable_private_ipv4(ip) {
            current = interface.ifa_next;
            continue;
        }

        candidates.push((candidate_priority(&name), ip));
        current = interface.ifa_next;
    }

    candidates.sort_by_key(|(priority, ip)| (Reverse(*priority), Reverse(u32::from(*ip))));
    Ok(candidates.first().map(|(_, ip)| *ip))
}

#[cfg(unix)]
fn is_excluded_interface(name: &str) -> bool {
    let lowered = name.to_ascii_lowercase();
    [
        "lo", "utun", "tun", "tap", "ppp", "ipsec", "bridge", "awdl", "llw", "docker", "veth",
        "vmnet",
    ]
    .iter()
    .any(|prefix| lowered.starts_with(prefix))
}

#[cfg(unix)]
fn candidate_priority(name: &str) -> i32 {
    let lowered = name.to_ascii_lowercase();
    if lowered.starts_with("en") || lowered.starts_with("eth") {
        4
    } else if lowered.starts_with("wlan") || lowered.starts_with("wl") || lowered.starts_with("wi")
    {
        3
    } else if lowered.starts_with("bridge") || lowered.starts_with("br") {
        1
    } else {
        2
    }
}

fn is_usable_private_ipv4(ip: Ipv4Addr) -> bool {
    ip.is_private()
        && !ip.is_loopback()
        && !ip.is_link_local()
        && !ip.is_broadcast()
        && !ip.is_documentation()
        && !ip.is_unspecified()
}

#[cfg(test)]
mod tests {
    use super::{find_routed_private_ipv4, is_usable_private_ipv4};
    use std::net::Ipv4Addr;

    #[test]
    fn accepts_private_lan_addresses() {
        assert!(is_usable_private_ipv4(Ipv4Addr::new(192, 168, 1, 44)));
        assert!(is_usable_private_ipv4(Ipv4Addr::new(10, 0, 0, 5)));
    }

    #[test]
    fn rejects_non_lan_addresses() {
        assert!(!is_usable_private_ipv4(Ipv4Addr::new(84, 24, 1, 9)));
        assert!(!is_usable_private_ipv4(Ipv4Addr::new(127, 0, 0, 1)));
        assert!(!is_usable_private_ipv4(Ipv4Addr::new(169, 254, 1, 2)));
    }

    #[cfg(unix)]
    #[test]
    fn prioritizes_primary_lan_interfaces() {
        use super::candidate_priority;

        assert!(candidate_priority("en0") > candidate_priority("bridge0"));
        assert!(candidate_priority("eth0") > candidate_priority("br0"));
    }

    #[test]
    fn routed_private_ipv4_lookup_does_not_panic() {
        let _ = find_routed_private_ipv4();
    }
}