minimoon_sync_server/
network.rs1use 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}