Features: - Introduce a Backend trait + System impl so flow/status/watch can be unit tested against a fake; add 11 connect-state-machine tests. - Captive-portal detection: status::connectivity returns Online/Portal/Offline; surfaced in status, JSON, connect notes, and a dedicated watch state. - `status --json` for bars/scripts; `profile add`/`profile remove`; detect now scores by number of in-range markers. Robustness: - Pin LC_ALL=C/LANG=C on child processes for locale-independent parsing. - Atomic config/state writes (temp + rename); 0600 config never world-readable. - Transient PSK file written to $XDG_RUNTIME_DIR when available. Fixes (from prior audit): - Feed Wi-Fi PSK to nmcli via stdin/passwd-file, never argv. - mask() no longer panics on multi-byte passwords. - Connectivity check requires HTTP 204 (no captive-portal false positives). - nmcli NAME,TYPE parsing handles escaped colons. - Strip CIDR suffix from displayed IP; PKGBUILD/Cargo version aligned (2.1.0).
80 lines
2.8 KiB
Rust
80 lines
2.8 KiB
Rust
//! The seam between breadcrumbs' decision logic and the outside world.
|
|
//!
|
|
//! Every interaction with NetworkManager, Tailscale, connectivity probes,
|
|
//! notifications and logging goes through the [`Backend`] trait. Production code
|
|
//! uses [`System`], which delegates to the `nm`/`tailscale`/`status`/`notify`
|
|
//! modules that shell out. Tests inject a fake so the connect state machine
|
|
//! (`flow`) and watch classifier can be exercised without touching the host.
|
|
|
|
use std::collections::HashSet;
|
|
|
|
use crate::config::{Config, NetworkDef};
|
|
use crate::nm;
|
|
use crate::notify::{self, Urgency};
|
|
use crate::status::{self, Connectivity};
|
|
use crate::tailscale::{self, TsHealth};
|
|
|
|
pub trait Backend {
|
|
fn wifi_interface(&self) -> Option<String>;
|
|
fn radio_on(&self);
|
|
fn rescan(&self, iface: &str, ssids: &[String]);
|
|
fn visible_ssids(&self, iface: &str) -> HashSet<String>;
|
|
fn active_ssid(&self, iface: &str) -> Option<String>;
|
|
fn ipv4(&self, iface: &str) -> Option<String>;
|
|
fn device_connected(&self, iface: &str) -> bool;
|
|
fn connect(&self, iface: &str, net: &NetworkDef, wait: u32, dns: &str) -> Result<(), String>;
|
|
fn tailscale_installed(&self) -> bool;
|
|
fn ensure_exit_node(&self, node: &str) -> TsHealth;
|
|
fn tailscale_check(&self, node: &str) -> TsHealth;
|
|
fn connectivity(&self, cfg: &Config) -> Connectivity;
|
|
fn notify(&self, summary: &str, body: &str, urgency: Urgency);
|
|
fn log(&self, line: &str);
|
|
}
|
|
|
|
/// The real backend: every method delegates to the system-facing modules.
|
|
pub struct System;
|
|
|
|
impl Backend for System {
|
|
fn wifi_interface(&self) -> Option<String> {
|
|
nm::wifi_interface()
|
|
}
|
|
fn radio_on(&self) {
|
|
nm::radio_on()
|
|
}
|
|
fn rescan(&self, iface: &str, ssids: &[String]) {
|
|
nm::rescan(iface, ssids)
|
|
}
|
|
fn visible_ssids(&self, iface: &str) -> HashSet<String> {
|
|
nm::visible_ssids(iface)
|
|
}
|
|
fn active_ssid(&self, iface: &str) -> Option<String> {
|
|
nm::active_ssid(iface)
|
|
}
|
|
fn ipv4(&self, iface: &str) -> Option<String> {
|
|
status::ipv4(iface)
|
|
}
|
|
fn device_connected(&self, iface: &str) -> bool {
|
|
nm::device_connected(iface)
|
|
}
|
|
fn connect(&self, iface: &str, net: &NetworkDef, wait: u32, dns: &str) -> Result<(), String> {
|
|
nm::connect_verbose(iface, net, wait, dns)
|
|
}
|
|
fn tailscale_installed(&self) -> bool {
|
|
tailscale::installed()
|
|
}
|
|
fn ensure_exit_node(&self, node: &str) -> TsHealth {
|
|
tailscale::ensure_exit_node(node)
|
|
}
|
|
fn tailscale_check(&self, node: &str) -> TsHealth {
|
|
tailscale::check(node)
|
|
}
|
|
fn connectivity(&self, cfg: &Config) -> Connectivity {
|
|
status::connectivity(cfg)
|
|
}
|
|
fn notify(&self, summary: &str, body: &str, urgency: Urgency) {
|
|
notify::notify(summary, body, urgency)
|
|
}
|
|
fn log(&self, line: &str) {
|
|
notify::log(line)
|
|
}
|
|
}
|