Skip to main content

minimoon_sync_server/
network.rs

1use anyhow::{anyhow, Context, Result};
2use std::net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket};
3
4#[cfg(unix)]
5use std::{cmp::Reverse, ffi::CStr, ptr};
6
7const LISTEN_PORT: u16 = 41324;
8
9pub fn listen_port() -> u16 {
10    LISTEN_PORT
11}
12
13pub fn preferred_bind_addr() -> Result<SocketAddr> {
14    preferred_bind_addr_for_port(LISTEN_PORT)
15}
16
17pub fn preferred_bind_addr_for_port(port: u16) -> Result<SocketAddr> {
18    Ok(SocketAddr::from((preferred_bind_ip()?, port)))
19}
20
21pub fn preferred_bind_ip() -> Result<Ipv4Addr> {
22    #[cfg(unix)]
23    if let Some(ip) = find_preferred_private_ipv4()? {
24        return Ok(ip);
25    }
26
27    if let Some(ip) = find_routed_private_ipv4()? {
28        return Ok(ip);
29    }
30
31    Err(anyhow!("No private LAN IPv4 address is available"))
32}
33
34fn find_routed_private_ipv4() -> Result<Option<Ipv4Addr>> {
35    let socket = UdpSocket::bind("0.0.0.0:0").context("failed to create route lookup socket")?;
36    socket
37        .connect("8.8.8.8:80")
38        .context("failed to determine default IPv4 route")?;
39
40    match socket
41        .local_addr()
42        .context("failed to read routed local address")?
43        .ip()
44    {
45        IpAddr::V4(ip) if is_usable_private_ipv4(ip) => Ok(Some(ip)),
46        _ => Ok(None),
47    }
48}
49
50#[cfg(unix)]
51fn find_preferred_private_ipv4() -> Result<Option<Ipv4Addr>> {
52    let mut interfaces = ptr::null_mut();
53    if unsafe { libc::getifaddrs(&mut interfaces) } != 0 {
54        return Err(std::io::Error::last_os_error()).context("getifaddrs failed");
55    }
56
57    let result = unsafe { collect_preferred_private_ipv4(interfaces) };
58    unsafe { libc::freeifaddrs(interfaces) };
59    result
60}
61
62#[cfg(unix)]
63unsafe fn collect_preferred_private_ipv4(
64    interfaces: *mut libc::ifaddrs,
65) -> Result<Option<Ipv4Addr>> {
66    let mut candidates = Vec::new();
67    let mut current = interfaces;
68
69    while !current.is_null() {
70        let interface = &*current;
71
72        if interface.ifa_addr.is_null() || interface.ifa_name.is_null() {
73            current = interface.ifa_next;
74            continue;
75        }
76
77        let flags = interface.ifa_flags as i32;
78        if flags & libc::IFF_UP == 0
79            || flags & libc::IFF_LOOPBACK != 0
80            || flags & libc::IFF_POINTOPOINT != 0
81        {
82            current = interface.ifa_next;
83            continue;
84        }
85
86        if (*interface.ifa_addr).sa_family as i32 != libc::AF_INET {
87            current = interface.ifa_next;
88            continue;
89        }
90
91        let name = CStr::from_ptr(interface.ifa_name)
92            .to_string_lossy()
93            .into_owned();
94        if is_excluded_interface(&name) {
95            current = interface.ifa_next;
96            continue;
97        }
98
99        let sockaddr = *(interface.ifa_addr as *const libc::sockaddr_in);
100        let ip = Ipv4Addr::from(u32::from_be(sockaddr.sin_addr.s_addr));
101        if !is_usable_private_ipv4(ip) {
102            current = interface.ifa_next;
103            continue;
104        }
105
106        candidates.push((candidate_priority(&name), ip));
107        current = interface.ifa_next;
108    }
109
110    candidates.sort_by_key(|(priority, ip)| (Reverse(*priority), Reverse(u32::from(*ip))));
111    Ok(candidates.first().map(|(_, ip)| *ip))
112}
113
114#[cfg(unix)]
115fn is_excluded_interface(name: &str) -> bool {
116    let lowered = name.to_ascii_lowercase();
117    [
118        "lo", "utun", "tun", "tap", "ppp", "ipsec", "bridge", "awdl", "llw", "docker", "veth",
119        "vmnet",
120    ]
121    .iter()
122    .any(|prefix| lowered.starts_with(prefix))
123}
124
125#[cfg(unix)]
126fn candidate_priority(name: &str) -> i32 {
127    let lowered = name.to_ascii_lowercase();
128    if lowered.starts_with("en") || lowered.starts_with("eth") {
129        4
130    } else if lowered.starts_with("wlan") || lowered.starts_with("wl") || lowered.starts_with("wi")
131    {
132        3
133    } else if lowered.starts_with("bridge") || lowered.starts_with("br") {
134        1
135    } else {
136        2
137    }
138}
139
140fn is_usable_private_ipv4(ip: Ipv4Addr) -> bool {
141    ip.is_private()
142        && !ip.is_loopback()
143        && !ip.is_link_local()
144        && !ip.is_broadcast()
145        && !ip.is_documentation()
146        && !ip.is_unspecified()
147}
148
149#[cfg(test)]
150mod tests {
151    use super::{find_routed_private_ipv4, is_usable_private_ipv4};
152    use std::net::Ipv4Addr;
153
154    #[test]
155    fn accepts_private_lan_addresses() {
156        assert!(is_usable_private_ipv4(Ipv4Addr::new(192, 168, 1, 44)));
157        assert!(is_usable_private_ipv4(Ipv4Addr::new(10, 0, 0, 5)));
158    }
159
160    #[test]
161    fn rejects_non_lan_addresses() {
162        assert!(!is_usable_private_ipv4(Ipv4Addr::new(84, 24, 1, 9)));
163        assert!(!is_usable_private_ipv4(Ipv4Addr::new(127, 0, 0, 1)));
164        assert!(!is_usable_private_ipv4(Ipv4Addr::new(169, 254, 1, 2)));
165    }
166
167    #[cfg(unix)]
168    #[test]
169    fn prioritizes_primary_lan_interfaces() {
170        use super::candidate_priority;
171
172        assert!(candidate_priority("en0") > candidate_priority("bridge0"));
173        assert!(candidate_priority("eth0") > candidate_priority("br0"));
174    }
175
176    #[test]
177    fn routed_private_ipv4_lookup_does_not_panic() {
178        let _ = find_routed_private_ipv4();
179    }
180}