1use std::path::{Path, PathBuf};
24
25use crate::wfb_tables::{DENY_DRIVER_PREFIXES, DENY_VID, WFB_COMPATIBLE, WFB_COMPATIBLE_DRIVERS};
26
27#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct WirelessIface {
30 pub name: String,
31 pub driver: String,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
36pub enum ApIfaceError {
37 NoOnboardWifi,
40 ConfiguredIsRadio { iface: String, driver: String },
42 ConfiguredMissing { iface: String },
44}
45
46impl std::fmt::Display for ApIfaceError {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 match self {
49 Self::NoOnboardWifi => write!(
50 f,
51 "no onboard WiFi interface found; refusing to run the access \
52 point on the WFB radio"
53 ),
54 Self::ConfiguredIsRadio { iface, driver } => write!(
55 f,
56 "configured AP interface {iface} is the WFB radio (driver \
57 {driver}); refusing to take the aircraft's radio link"
58 ),
59 Self::ConfiguredMissing { iface } => {
60 write!(f, "configured AP interface {iface} is not present")
61 }
62 }
63 }
64}
65
66pub fn is_injection_driver(driver: &str) -> bool {
71 let d = driver.trim().to_ascii_lowercase();
72 WFB_COMPATIBLE_DRIVERS.iter().any(|k| *k == d)
73}
74
75pub fn is_wfb_adapter(driver: &str, vid_pid: Option<(u16, u16)>) -> bool {
81 let d = driver.trim().to_ascii_lowercase();
82 if DENY_DRIVER_PREFIXES.iter().any(|p| d.starts_with(p)) {
83 return false;
84 }
85 if let Some((vid, pid)) = vid_pid {
86 if DENY_VID.contains(&vid) {
87 return false;
88 }
89 if WFB_COMPATIBLE
90 .iter()
91 .any(|(v, p, _)| *v == vid && *p == pid)
92 {
93 return true;
94 }
95 }
96 is_injection_driver(&d)
97}
98
99pub fn usb_vid_pid_in(root: &Path, iface: &str) -> Option<(u16, u16)> {
105 const MAX_PARENT_HOPS: usize = 4;
106 let mut dir = std::fs::canonicalize(root.join(iface).join("device")).ok()?;
107 for _ in 0..=MAX_PARENT_HOPS {
108 if let (Ok(vid), Ok(pid)) = (
109 std::fs::read_to_string(dir.join("idVendor")),
110 std::fs::read_to_string(dir.join("idProduct")),
111 ) {
112 return Some((
113 u16::from_str_radix(vid.trim(), 16).ok()?,
114 u16::from_str_radix(pid.trim(), 16).ok()?,
115 ));
116 }
117 dir = dir.parent()?.to_path_buf();
118 }
119 None
120}
121
122pub fn usb_vid_pid(iface: &str) -> Option<(u16, u16)> {
124 usb_vid_pid_in(Path::new(NET_DIR), iface)
125}
126
127pub fn is_onboard_wifi_driver(driver: &str) -> bool {
134 let d = driver.trim().to_ascii_lowercase();
135 DENY_DRIVER_PREFIXES.iter().any(|p| d.starts_with(p))
136}
137
138pub fn choose_ap_interface(
145 ifaces: &[WirelessIface],
146 configured: &str,
147 radio_iface: Option<&str>,
148) -> Result<String, ApIfaceError> {
149 let is_radio = |c: &WirelessIface| {
150 is_injection_driver(&c.driver) || radio_iface.is_some_and(|r| r == c.name)
151 };
152
153 let configured = configured.trim();
154 if !configured.is_empty() {
155 return match ifaces.iter().find(|c| c.name == configured) {
156 Some(c) if is_radio(c) => Err(ApIfaceError::ConfiguredIsRadio {
157 iface: c.name.clone(),
158 driver: c.driver.clone(),
159 }),
160 Some(c) => Ok(c.name.clone()),
161 None => Err(ApIfaceError::ConfiguredMissing {
165 iface: configured.to_string(),
166 }),
167 };
168 }
169
170 ifaces
171 .iter()
172 .find(|c| is_onboard_wifi_driver(&c.driver) && !is_radio(c))
173 .map(|c| c.name.clone())
174 .ok_or(ApIfaceError::NoOnboardWifi)
175}
176
177const NET_DIR: &str = "/sys/class/net";
178
179pub fn driver_name_in(root: &Path, iface: &str) -> Option<String> {
181 let link = root.join(iface).join("device").join("driver");
182 let target = std::fs::read_link(&link).ok()?;
183 Some(target.file_name()?.to_string_lossy().into_owned())
184}
185
186pub fn is_wireless_in(root: &Path, iface: &str) -> bool {
188 root.join(iface).join("phy80211").exists() || root.join(iface).join("wireless").exists()
189}
190
191pub fn list_wireless_in(root: &Path) -> Vec<WirelessIface> {
197 let mut out: Vec<WirelessIface> = std::fs::read_dir(root)
198 .into_iter()
199 .flatten()
200 .flatten()
201 .filter_map(|e| {
202 let name = e.file_name().to_string_lossy().into_owned();
203 if name == "lo" || !is_wireless_in(root, &name) {
204 return None;
205 }
206 Some(WirelessIface {
207 driver: driver_name_in(root, &name)?,
208 name,
209 })
210 })
211 .collect();
212 out.sort_by(|a, b| a.name.cmp(&b.name));
213 out
214}
215
216pub fn list_wireless() -> Vec<WirelessIface> {
218 list_wireless_in(&PathBuf::from(NET_DIR))
219}
220
221pub const WFB_STATS_PATH: &str = "/run/ados/wfb-stats.json";
224
225pub fn radio_interface_from(path: &Path) -> Option<String> {
232 let txt = std::fs::read_to_string(path).ok()?;
233 let v: serde_json::Value = serde_json::from_str(&txt).ok()?;
234 let iface = v.get("interface")?.as_str()?.trim().to_string();
235 (!iface.is_empty()).then_some(iface)
236}
237
238pub fn radio_interface() -> Option<String> {
240 radio_interface_from(Path::new(WFB_STATS_PATH))
241}
242
243pub fn resolve_ap_interface(
245 configured: &str,
246 radio_iface: Option<&str>,
247) -> Result<String, ApIfaceError> {
248 choose_ap_interface(&list_wireless(), configured, radio_iface)
249}
250
251const NOT_WIRED_DRIVERS: &[&str] = &[
256 "g_ether",
257 "g_multi",
258 "rndis_host",
259 "ipheth",
260 "qmi_wwan",
261 "cdc_mbim",
262 "huawei_cdc_ncm",
263];
264
265pub fn is_wired_in(root: &Path, iface: &str) -> bool {
270 if iface == "lo" || is_wireless_in(root, iface) {
271 return false;
272 }
273 let dir = root.join(iface);
274 let ether = std::fs::read_to_string(dir.join("type"))
276 .map(|t| t.trim() == "1")
277 .unwrap_or(false);
278 if !ether {
279 return false;
280 }
281 let device = match std::fs::read_link(dir.join("device")) {
283 Ok(target) => target,
284 Err(_) => return false,
285 };
286 if device.to_string_lossy().contains("gadget") {
287 return false;
288 }
289 match driver_name_in(root, iface) {
290 Some(driver) => {
291 !NOT_WIRED_DRIVERS.contains(&driver.as_str()) && !driver.starts_with("usb_f_")
292 }
293 None => false,
294 }
295}
296
297pub fn list_wired_in(root: &Path) -> Vec<String> {
299 let mut out: Vec<String> = std::fs::read_dir(root)
300 .into_iter()
301 .flatten()
302 .flatten()
303 .map(|e| e.file_name().to_string_lossy().into_owned())
304 .filter(|name| is_wired_in(root, name))
305 .collect();
306 out.sort();
307 out
308}
309
310pub fn list_wired() -> Vec<String> {
312 list_wired_in(&PathBuf::from(NET_DIR))
313}
314
315pub fn mesh_capable_in(root: &Path) -> bool {
319 list_wireless_in(root)
320 .iter()
321 .filter(|w| is_wfb_adapter(&w.driver, usb_vid_pid_in(root, &w.name)))
322 .count()
323 >= 2
324}
325
326pub fn mesh_capable() -> bool {
328 mesh_capable_in(Path::new(NET_DIR))
329}
330
331const CELLULAR_DRIVERS: &[&str] = &[
333 "qmi_wwan",
334 "cdc_mbim",
335 "huawei_cdc_ncm",
336 "cdc_ncm",
337 "rndis_host",
338];
339
340const MODEM_USB_VIDS: &[u16] = &[0x1199, 0x2c7c, 0x12d1, 0x05c6, 0x1e0e, 0x2cb7, 0x1bc7];
342
343pub fn is_cellular_in(root: &Path, iface: &str) -> bool {
347 let Ok(device) = std::fs::read_link(root.join(iface).join("device")) else {
348 return false;
349 };
350 let path = device.to_string_lossy();
351 if path.contains("gadget") || path.contains("udc") {
352 return false;
353 }
354 match driver_name_in(root, iface).as_deref() {
355 Some(d) if CELLULAR_DRIVERS.contains(&d) => true,
356 Some("option") => {
357 usb_vid_pid_in(root, iface).is_some_and(|(vid, _)| MODEM_USB_VIDS.contains(&vid))
358 }
359 _ => false,
360 }
361}
362
363pub fn list_cellular_in(root: &Path) -> Vec<String> {
365 let mut out: Vec<String> = std::fs::read_dir(root)
366 .into_iter()
367 .flatten()
368 .flatten()
369 .map(|e| e.file_name().to_string_lossy().into_owned())
370 .filter(|name| is_cellular_in(root, name))
371 .collect();
372 out.sort();
373 out
374}
375
376pub fn list_cellular() -> Vec<String> {
378 list_cellular_in(&PathBuf::from(NET_DIR))
379}
380
381#[cfg(test)]
382#[path = "netif_tests.rs"]
383mod tests;