#[cfg(test)]
mod tests;
use std::cell::RefCell;
use std::cmp::Ordering;
use std::collections::btree_map::Entry;
use std::collections::{BTreeMap, HashMap};
use std::rc::Rc;
use std::sync::Once;
use crabshell_reactive::{ReadSignal, Signal};
use crabshell_ui::Cx;
use zbus::{Array, Connection, Message, ObjectPath, OwnedObjectPath, Variant};
use crate::Shared;
use crate::dbus::{self, CALL_TIMEOUT, Daemon, OBJECT_MANAGER, PROPERTIES, Props, set_changed};
const NM: &str = "org.freedesktop.NetworkManager";
const NM_PATH: &str = "/org/freedesktop/NetworkManager";
const ROOT: &str = "/org/freedesktop";
const DEVICE: &str = "org.freedesktop.NetworkManager.Device";
const WIRELESS: &str = "org.freedesktop.NetworkManager.Device.Wireless";
const ACCESS_POINT: &str = "org.freedesktop.NetworkManager.AccessPoint";
const ACTIVE: &str = "org.freedesktop.NetworkManager.Connection.Active";
const MAX_DEVICES: usize = 64;
const MAX_ACCESS_POINTS: usize = 512;
const MAX_SSID: usize = 32;
const LOOPBACK: u32 = 32;
const UNMANAGED: u32 = 10;
const ACTIVATED: u32 = 100;
const PRIVACY: u32 = 0x1;
const KEY_MGMT_KEYED: u32 = 0x100 | 0x200 | 0x400 | 0x2000;
const MAX_OBJECTS: usize = 4096;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum NetworkKind {
Ethernet,
Wifi,
Other,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum NetworkState {
Connected,
Connecting,
Disconnected,
Unavailable,
}
#[derive(Clone, Debug, PartialEq)]
pub struct NetworkDevice {
pub path: String,
pub interface: String,
pub kind: NetworkKind,
pub state: NetworkState,
pub connection: String,
}
impl NetworkDevice {
fn from_props(path: String, props: &Props, objects: &BTreeMap<String, Object>) -> NetworkDevice {
let connection = dbus::get::<&ObjectPath>(props, "ActiveConnection")
.and_then(|at| objects.get(at.as_str())?.active.as_ref())
.map_or(String::new(), |active| dbus::text(active, "Id"));
NetworkDevice {
path,
interface: dbus::text(props, "Interface"),
kind: match dbus::get::<u32>(props, "DeviceType") {
Some(1) => NetworkKind::Ethernet,
Some(2) => NetworkKind::Wifi,
_ => NetworkKind::Other,
},
state: match dbus::get::<u32>(props, "State") {
Some(ACTIVATED) => NetworkState::Connected,
Some(40..=90) => NetworkState::Connecting,
Some(30 | 110 | 120) => NetworkState::Disconnected,
_ => NetworkState::Unavailable,
},
connection,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct WifiNetwork {
pub ssid: String,
pub strength: u8,
pub secured: bool,
pub connected: bool,
pub device: String,
pub access_point: String,
}
#[derive(Clone)]
pub struct Network {
service: Option<Rc<NetworkService>>,
wifi_enabled: ReadSignal<bool>,
devices: ReadSignal<Vec<NetworkDevice>>,
wifi_networks: ReadSignal<Vec<WifiNetwork>>,
}
impl Network {
pub fn wifi_enabled(&self) -> ReadSignal<bool> {
self.wifi_enabled
}
pub fn devices(&self) -> ReadSignal<Vec<NetworkDevice>> {
self.devices
}
pub fn wifi_networks(&self) -> ReadSignal<Vec<WifiNetwork>> {
self.wifi_networks
}
pub fn set_wifi_enabled(&self, enabled: bool) {
let body = (NM, "WirelessEnabled", Variant::from(enabled));
self.call(NM_PATH, PROPERTIES, "Set", body);
}
pub fn scan(&self, device: &str) {
let options = HashMap::<&str, Variant<'static>>::new();
self.call(device, WIRELESS, "RequestScan", options);
}
pub fn connect(&self, network: &WifiNetwork, password: Option<&str>) {
let paths = (
ObjectPath::try_from(network.device.clone()),
ObjectPath::try_from(network.access_point.clone()),
);
let (Ok(device), Ok(at)) = paths else {
return tracing::info!(network.ssid, "no such Wi-Fi network; nothing sent");
};
let Some(password) = password else {
return self.activate(network, device, at);
};
let ssid = self
.service
.as_ref()
.and_then(|service| {
let state = service.state.borrow();
let object = state.as_ref()?.objects.get(&network.access_point)?;
Some(ssid(object.access_point.as_ref()?))
})
.unwrap_or_else(|| network.ssid.clone().into_bytes());
let wireless = HashMap::from([("ssid", Variant::from(ssid))]);
let security = HashMap::from([("psk", Variant::from(password.to_owned()))]);
let settings = HashMap::from([
("802-11-wireless", wireless),
("802-11-wireless-security", security),
]);
let body = (settings, device, at);
self.call(NM_PATH, NM, "AddAndActivateConnection", body);
}
fn activate(&self, network: &WifiNetwork, device: ObjectPath<'static>, at: ObjectPath<'static>) {
let Some(service) = &self.service else {
return;
};
let connection = service
.state
.borrow()
.as_ref()
.and_then(|s| s.connection.clone());
let Some(connection) = connection else {
return tracing::info!("NetworkManager isn't running; nothing sent");
};
let (shared, open) = (Rc::clone(&service.shared), !network.secured);
let ssid = network.ssid.clone().into_bytes();
let ssid = self
.service
.as_ref()
.and_then(|service| {
let state = service.state.borrow();
let object = state.as_ref()?.objects.get(&network.access_point)?;
Some(self::ssid(object.access_point.as_ref()?))
})
.unwrap_or(ssid);
service.shared.spawn(async move {
let pick = ObjectPath::from_static_str_unchecked("/");
let body = (pick, &device, &at);
let call = connection.call_method(Some(NM), NM_PATH, Some(NM), "ActivateConnection", &body);
let Err(error) = shared.timeout(CALL_TIMEOUT, call).await else {
return;
};
if !open {
return tracing::warn!(%error, "no saved connection for that network");
}
let wireless = HashMap::from([("ssid", Variant::from(ssid))]);
let settings = HashMap::from([("802-11-wireless", wireless)]);
let body = (settings, &device, &at);
let method = "AddAndActivateConnection";
let call = connection.call_method(Some(NM), NM_PATH, Some(NM), method, &body);
if let Err(error) = shared.timeout(CALL_TIMEOUT, call).await {
tracing::warn!(%error, method, "NetworkManager refused it");
}
});
}
pub fn disconnect(&self, device: &str) {
self.call(device, DEVICE, "Disconnect", ());
}
fn call<B>(&self, path: &str, interface: &'static str, method: &'static str, body: B)
where
B: serde::Serialize + zbus::DynamicType + 'static,
{
let Some(service) = &self.service else {
return;
};
let Some(connection) = service
.state
.borrow()
.as_ref()
.and_then(|state| state.connection.clone())
else {
return tracing::info!(method, "NetworkManager isn't running; nothing sent");
};
let (shared, path) = (Rc::clone(&service.shared), path.to_owned());
service.shared.spawn(async move {
let call = connection.call_method(Some(NM), path.as_str(), Some(interface), method, &body);
if let Err(error) = shared.timeout(CALL_TIMEOUT, call).await {
tracing::warn!(%error, path, method, "NetworkManager refused it");
}
});
}
}
pub(crate) fn network(cx: &Cx) -> Network {
let Some(service) = cx.service::<NetworkService>() else {
static LOGGED: Once = Once::new();
LOGGED.call_once(|| {
tracing::warn!("cx.network() needs the services the shell provides; it stays empty");
});
return Network {
service: None,
wifi_enabled: cx.signal(false).read_only(),
devices: cx.signal(Vec::new()).read_only(),
wifi_networks: cx.signal(Vec::new()).read_only(),
};
};
service.start()
}
pub(crate) struct NetworkService {
shared: Rc<Shared>,
state: RefCell<Option<State>>,
}
struct State {
wifi_enabled: Signal<bool>,
devices: Signal<Vec<NetworkDevice>>,
wifi_networks: Signal<Vec<WifiNetwork>>,
objects: BTreeMap<String, Object>,
connection: Option<Connection>,
generation: u64,
}
#[derive(Default)]
struct Object {
manager: Option<Props>,
device: Option<Props>,
wireless: Option<Props>,
access_point: Option<Props>,
active: Option<Props>,
}
impl Object {
fn slot(&mut self, interface: &str) -> Option<&mut Option<Props>> {
match interface {
NM => Some(&mut self.manager),
DEVICE => Some(&mut self.device),
WIRELESS => Some(&mut self.wireless),
ACCESS_POINT => Some(&mut self.access_point),
ACTIVE => Some(&mut self.active),
_ => None,
}
}
fn kept(&self) -> bool {
self.manager.is_some()
|| self.device.is_some()
|| self.access_point.is_some()
|| self.active.is_some()
}
}
fn by_number(a: &str, b: &str) -> Ordering {
(a.len(), a).cmp(&(b.len(), b))
}
fn ssid(props: &Props) -> Vec<u8> {
let bytes = dbus::get::<&Array>(props, "Ssid");
let bytes = bytes.iter().flat_map(|bytes| bytes.inner());
let bytes = bytes.map_while(|byte| match byte {
Variant::U8(byte) => Some(*byte),
_ => None,
});
bytes.take(MAX_SSID).collect()
}
fn paths<'a>(props: &'a Props, name: &str) -> impl Iterator<Item = &'a str> {
let paths = dbus::get::<&Array>(props, name);
paths
.into_iter()
.flat_map(|paths| paths.inner())
.filter_map(|path| match path {
Variant::ObjectPath(path) => Some(path.as_str()),
_ => None,
})
}
fn add(objects: &mut BTreeMap<String, Object>, path: String, interfaces: HashMap<String, Props>) {
if objects.len() >= MAX_OBJECTS && !objects.contains_key(&path) {
return tracing::debug!(path, "too many network objects; leaving one out");
}
let mut object = objects.remove(&path).unwrap_or_default();
for (interface, props) in interfaces {
if let Some(slot) = object.slot(&interface) {
*slot = Some(props);
}
}
if object.kept() {
objects.insert(path, object);
}
}
fn secured(point: &Props) -> bool {
let flags = |name| dbus::get::<u32>(point, name).unwrap_or(0);
let keyed = (flags("WpaFlags") | flags("RsnFlags")) & KEY_MGMT_KEYED != 0;
let wep = flags("Flags") & PRIVACY != 0 && flags("WpaFlags") == 0 && flags("RsnFlags") == 0;
keyed || wep
}
fn read(interface: &str, name: &str) -> bool {
match interface {
NM => name == "WirelessEnabled",
DEVICE => matches!(
name,
"Interface" | "DeviceType" | "State" | "ActiveConnection"
),
WIRELESS => matches!(name, "AccessPoints" | "ActiveAccessPoint"),
ACCESS_POINT => matches!(
name,
"Ssid" | "Strength" | "Flags" | "WpaFlags" | "RsnFlags"
),
ACTIVE => name == "Id",
_ => false,
}
}
fn wifi_networks(objects: &BTreeMap<String, Object>) -> Vec<WifiNetwork> {
let mut best = BTreeMap::<(&str, Vec<u8>), WifiNetwork>::new();
for (path, object) in objects {
let (Some(device), Some(wireless)) = (&object.device, &object.wireless) else {
continue;
};
let on = dbus::get::<u32>(device, "State") == Some(ACTIVATED);
let active = dbus::get::<&ObjectPath>(wireless, "ActiveAccessPoint");
let active = active.map(|at| at.as_str());
for at in paths(wireless, "AccessPoints") {
let Some(point) = objects
.get(at)
.and_then(|object| object.access_point.as_ref())
else {
continue;
};
let ssid = ssid(point);
if ssid.iter().all(|byte| *byte == 0) {
continue;
}
let network = WifiNetwork {
ssid: String::from_utf8_lossy(&ssid).into_owned(),
strength: dbus::get::<u8>(point, "Strength").unwrap_or(0).min(100),
secured: secured(point),
connected: on && active == Some(at),
device: path.clone(),
access_point: at.to_owned(),
};
match best.entry((path, ssid)) {
Entry::Vacant(entry) => {
entry.insert(network);
}
Entry::Occupied(mut entry) => {
let connected = entry.get().connected || network.connected;
if network.strength > entry.get().strength {
entry.insert(network);
}
entry.get_mut().connected = connected;
}
}
}
}
let mut networks = best.into_values().collect::<Vec<_>>();
networks.sort_by(|a, b| {
let ties = || (&a.ssid, &a.device).cmp(&(&b.ssid, &b.device));
b.strength.cmp(&a.strength).then_with(ties)
});
networks.truncate(MAX_ACCESS_POINTS);
networks
}
impl NetworkService {
pub(crate) fn new(shared: Rc<Shared>) -> NetworkService {
NetworkService {
shared,
state: RefCell::new(None),
}
}
fn start(self: &Rc<Self>) -> Network {
if let Some(state) = &*self.state.borrow() {
return Network {
service: Some(Rc::clone(self)),
wifi_enabled: state.wifi_enabled.read_only(),
devices: state.devices.read_only(),
wifi_networks: state.wifi_networks.read_only(),
};
}
let root = &self.shared.root;
let wifi_enabled = root.signal(false);
let (devices, wifi_networks) = (root.signal(Vec::new()), root.signal(Vec::new()));
*self.state.borrow_mut() = Some(State {
wifi_enabled,
devices,
wifi_networks,
objects: BTreeMap::new(),
connection: None,
generation: 0,
});
match self.shared.config.system_bus.clone() {
Some(address) => {
let follow = dbus::follow(Rc::clone(&self.shared), Rc::clone(self), address);
self.shared.spawn(follow);
}
None => tracing::info!("no system bus, so there is no networking"),
}
Network {
service: Some(Rc::clone(self)),
wifi_enabled: wifi_enabled.read_only(),
devices: devices.read_only(),
wifi_networks: wifi_networks.read_only(),
}
}
fn publish(&self) {
let shown = self.state.borrow().as_ref().map(|state| {
let objects = &state.objects;
let enabled = objects
.get(NM_PATH)
.and_then(|object| dbus::get(object.manager.as_ref()?, "WirelessEnabled"))
.unwrap_or(false);
let mut devices = objects
.iter()
.filter_map(|(path, object)| {
let props = object.device.as_ref()?;
let kind = dbus::get::<u32>(props, "DeviceType");
let state = dbus::get::<u32>(props, "State");
if kind == Some(LOOPBACK) || state == Some(UNMANAGED) {
return None;
}
Some(NetworkDevice::from_props(path.clone(), props, objects))
})
.collect::<Vec<_>>();
devices.sort_by(|a, b| by_number(&a.path, &b.path));
devices.truncate(MAX_DEVICES);
let shown = (enabled, devices, wifi_networks(objects));
(
state.wifi_enabled,
state.devices,
state.wifi_networks,
shown,
)
});
if let Some((enabled_signal, devices_signal, networks_signal, shown)) = shown {
let (enabled, devices, networks) = shown;
set_changed(enabled_signal, enabled);
set_changed(devices_signal, devices);
set_changed(networks_signal, networks);
}
}
}
impl Daemon for NetworkService {
const NAME: &'static str = NM;
fn rules() -> Vec<String> {
let mut rules = vec![format!(
"type='signal',sender='{NM}',path='{ROOT}',interface='{OBJECT_MANAGER}'"
)];
rules.extend(
[NM, DEVICE, WIRELESS, ACCESS_POINT, ACTIVE].map(|interface| {
format!(
"type='signal',sender='{NM}',interface='{PROPERTIES}',member='PropertiesChanged',\
arg0='{interface}'"
)
}),
);
rules
}
fn alive(&self) -> bool {
let state = self.state.borrow();
state
.as_ref()
.is_some_and(|state| state.devices.try_with(|_| ()).is_some())
}
fn clear(&self) -> u64 {
let generation = {
let mut state = self.state.borrow_mut();
let Some(state) = state.as_mut() else {
return 0;
};
state.objects.clear();
state.connection = None;
state.generation += 1;
state.generation
};
self.publish();
generation
}
async fn read(self: Rc<Self>, connection: Connection, generation: u64) {
let call = connection.call_method(
Some(NM),
ROOT,
Some(OBJECT_MANAGER),
"GetManagedObjects",
&(),
);
let read = async {
let reply = self.shared.timeout(CALL_TIMEOUT, call).await?;
reply
.body()
.deserialize::<HashMap<OwnedObjectPath, HashMap<String, Props>>>()
};
let managed = match read.await {
Ok(managed) => managed,
Err(error) => return tracing::warn!(%error, "couldn't read NetworkManager"),
};
let mut managed = managed
.into_iter()
.map(|(path, interfaces)| (path.as_str().to_owned(), interfaces))
.collect::<Vec<_>>();
managed.sort_by(|(a, _), (b, _)| by_number(a, b));
{
let mut state = self.state.borrow_mut();
let Some(state) = state
.as_mut()
.filter(|state| state.generation == generation)
else {
return;
};
state.objects.clear();
for (path, interfaces) in managed {
add(&mut state.objects, path, interfaces);
}
state.connection = Some(connection);
}
self.publish();
}
fn signal(self: &Rc<Self>, _: &Connection, message: &Message) {
if let Some((path, interface, changed, dropped)) = dbus::properties_changed(message) {
{
let mut state = self.state.borrow_mut();
let props = state
.as_mut()
.and_then(|state| state.objects.get_mut(&path))
.and_then(|object| object.slot(&interface)?.as_mut());
let Some(props) = props else {
return;
};
let shown = changed
.keys()
.chain(&dropped)
.any(|name| read(&interface, name));
dbus::apply(props, changed, dropped);
if !shown {
return;
}
}
return self.publish();
}
let header = message.header();
if header.interface().map(|interface| interface.as_str()) != Some(OBJECT_MANAGER) {
return;
}
let body = message.body();
{
let mut state = self.state.borrow_mut();
let Some(state) = state.as_mut() else {
return;
};
match header.member().map(|member| member.as_str()) {
Some("InterfacesAdded") => {
let Ok((path, interfaces)) =
body.deserialize::<(OwnedObjectPath, HashMap<String, Props>)>()
else {
return;
};
add(&mut state.objects, path.as_str().to_owned(), interfaces);
}
Some("InterfacesRemoved") => {
let Ok((path, interfaces)) = body.deserialize::<(OwnedObjectPath, Vec<String>)>() else {
return;
};
let Some(object) = state.objects.get_mut(path.as_str()) else {
return;
};
for interface in interfaces {
if let Some(slot) = object.slot(&interface) {
*slot = None;
}
}
if !object.kept() {
state.objects.remove(path.as_str());
}
}
_ => return,
}
}
self.publish();
}
}