D-Bus is a message bus which handles inter-process communication, which can be run both on a system or an user level. It is used with a variety of systems and applications in Linux, most notably with systemd and NetworkManager (which we are going to be focusing on).
D-Bus is used by NetworkManager for managing network connections. Applications can interact with NetworkManager over D-Bus to list and connect to networks, create hotspots, and perform other network-related tasks.
zbus is a 100% Rust-native implementation of the D-Bus protocol, which takes advantage of rust's macros which makes interacting with D-Bus very easy. Rust's strong type system and ownership model also prevent common programming errors, making it suitable for writing robust system-level applications.
We're going to write a small axum web server which will be used to list ssid's, connect to a network and turn on a hotspot with the help of zbus.
cargo new db_nm cd db_nm
[dependencies] zbus = "2.0" zvariant = "2.8" anyhow = "1.0" tokio = { version = "1", features = ["full"] } axum = "0.7"
zbus-xmlgen --system org.freedesktop.NetworkManager /org/freedesktop/NetworkManager > src/network_manager.rs zbus-xmlgen --system org.freedesktop.NetworkManager /org/freedesktop/NetworkManager/Devices/1 > src/network_manager_device.rs zbus-xmlgen --system org.freedesktop.NetworkManager /org/freedesktop/NetworkManager/Devices/2 > src/network_manager_wired_device.rs zbus-xmlgen --system org.freedesktop.NetworkManager /org/freedesktop/NetworkManager/Devices/3 > src/network_manager_wireless_device.rs zbus-xmlgen --system org.freedesktop.NetworkManager /org/freedesktop/NetworkManager/AccessPoint > src/network_manager_access_point.rs zbus-xmlgen --system org.freedesktop.NetworkManager /org/freedesktop/NetworkManager/Settings > src/network_manager_settings.rs
/org/freedesktop/NetworkManager
/org/freedesktop/NetworkManager/Devices
/org/freedesktop/NetworkManager/AccessPoint
/org/freedesktop/NetworkManager/Settings
let app = Router::new() .route("/list_ssids", get(list_ssids)) .route("/connect", post(connect_to_network)) .route("/hotspot", post(turn_on_hotspot)); let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); axum::Server::bind(&addr).serve(app.into_make_service()).await.unwrap();
We've defined a router with 3 routes, list_ssids
, connect
and hotspot
, which will, spoiler alert, do what their methods describe they do.
I would like to define a custom type which will own things like the system connection and network manager proxy, as getting them repeatedly would cause additional overhead we do not want.
pub struct Wifi<'a> { network_proxy: NetworkManagerProxy<'a>, wifi_device: DeviceProxy<'a>, connection: Connection, }
First, we'll add a constructor for the Wifi
type and getters for the properties:
impl<'a> Wifi<'a> { pub fn get_wifi_device_proxy(&self) -> &DeviceProxy<'_> { &self.wifi_device_proxy } pub fn get_network_manager_proxy(&self) -> &NetworkManagerProxy<'_> { &self.network_manager_proxy } pub fn get_system_connection(&self) -> &Connection { &self.connection } pub async fn new() -> Result<Self> { let mut wifi_device_proxy = None; let mut wifi_device_proxy_path = None; let connection = Connection::system().await?; let network_manager_proxy = NetworkManagerProxy::new(&connection).await?; let devices = network_manager_proxy.get_devices().await?; for device in devices { let device_proxy = DeviceProxy::builder(&connection) .path(device.clone())? .build() .await?; if device_proxy.device_type().await == Ok(NM_DEVICE_TYPE_WIFI) { wifi_device_proxy = Some(device_proxy); wifi_device_proxy_path = Some(device); break; } } Ok(WifiSystem { connection, network_proxy, }) }
This method is designed to asynchronously initialize a Wifi
struct by connecting to the D-Bus system bus, interacting with NetworkManager to find a Wi-Fi device,
and storing its proxy for further operations. A couple of things to notice, with dbus you can have both system
and session
connection types.
System Bus: The system bus is used for system-wide messages and services. It's typically used by system daemons and services to communicate with each other and
with user applications (like the one we're using - with NetworkManager).
The session bus connection is specific to a user session. It's used for IPC between applications belonging to the same user session. Secondly, we notice a const called
NM_DEVICE_TYPE_WIFI
, which is used to compare against the device type returned by NetworkManager to determine if a device is a Wi-Fi device, its value is 2.
Now, in order to use a Wifi
object in our soon-to-be-created handlers, we could use the application state. We will need to instantiate one object and store it in our Router
object:
... let wifi = Arc::new(Wifi::new().await?); let app = Router::new() ... .route("/hotspot", post(turn_on_hotspot)); .with_state(wifi); ...
Now we will define our handlers:
async fn list_ssids(State(wifi): State<Arc<Wifi<'_>>>) -> Json<Vec<String>> { let ssids = wifi.get_access_points_ssids(&wifi).await; Json(ssids) } async fn get_access_points_ssids(&self) -> Vec<String> { let network_manager_proxy = self.get_network_manager_proxy(); match network_manager_proxy.get_devices().await { Ok(devices) => { let mut ssids = Vec::new(); for device in devices { let device_proxy = DeviceProxy::builder(self.get_system_connection()) .path(device.clone()) .build() .await .unwrap(); // You can handle errors appropriately if device_proxy.device_type().await == Ok(NM_DEVICE_TYPE_WIFI) { match WirelessProxy::builder(&self.get_system_connection()) .path(device.clone()) .build() .await { Ok(wireless_proxy) => { match wireless_proxy.get_access_points().await { Ok(access_points) => { for ap in access_points { let ap_proxy = AccessPointProxy::builder(self.get_system_connection()) .path(ap) .build() .await .unwrap(); match ap_proxy.ssid().await { Ok(ssid_bytes) => { if let Ok(ssid) = String::from_utf8(ssid_bytes) { ssids.push(ssid); } } Err(_) => continue, } } } Err(_) => continue, } } Err(_) => continue, } } } ssids } Err(_) => Vec::new(), } }
Next, in order to connect to a wifi network, we will need to figure out the wifi device and access point paths.
pub async fn get_device_and_ap_by_ssid( &self, target_ssid: String, ) -> Option<(AccessPointProxy<'_>, String)> { let network_manager_proxy = &self.get_network_manager_proxy(); match network_manager_proxy.get_devices().await { Ok(devices) => { for device in devices { let device_proxy = match DeviceProxy::builder(&self.get_system_connection()) .path(device.clone()) .build() .await { Ok(proxy) => proxy, Err(_) => continue, // Handle errors appropriately }; if device_proxy.device_type().await == Ok(NM_DEVICE_TYPE_WIFI) { match WirelessProxy::builder(&self.get_system_connection()) .path(device.clone()) .build() .await { Ok(wireless_proxy) => { match wireless_proxy.get_access_points().await { Ok(access_points) => { for ap_path in access_points { let ap_proxy = match AccessPointProxy::builder(&self.get_system_connection()) .path(ap_path.clone()) .build() .await { Ok(proxy) => proxy, Err(_) => continue, // Handle errors appropriately }; match ap_proxy.ssid().await { Ok(ssid_bytes) => { if let Ok(ssid) = String::from_utf8(ssid_bytes) { if ssid == target_ssid { return Some((ap_proxy, ap_path)); } } } Err(_) => continue, } } } Err(_) => continue, } } Err(_) => continue, } } } None } Err(_) => None, // Handle errors appropriately } }
and the connect handler:
#[derive(Deserialize)] struct ConnectRequest { ssid: String, password: Option<String>, } async fn connect( Json(request): Json<ConnectRequest>, State(wifi): State<Arc<Wifi<'_>>>, ) -> (StatusCode, String) { let network_manager_proxy = &wifi.get_network_manager_proxy(); let connection = wifi.get_system_connection(); match network_manager_proxy.get_devices().await { Ok(devices) => { for device in devices { let device_proxy = match DeviceProxy::builder(connection) .path(device.clone()) .build() .await { Ok(proxy) => proxy, Err(_) => continue, // Handle errors appropriately }; if device_proxy.device_type().await == Ok(NM_DEVICE_TYPE_WIFI) { // Assuming `connect_to_network` is a method to connect to a Wi-Fi network match wifi.connect_to_network(&device_proxy, &request.ssid, request.password.clone()).await { Ok(_) => return (StatusCode::OK, "Connected to Wi-Fi".to_string()), Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to connect: {}", e)), } } } (StatusCode::NOT_FOUND, "Wi-Fi device not found".to_string()) }, Err(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Failed to retrieve devices".to_string()), } }
and the hotspot handler, given the ssid and password, but first, we need to add a new method on the Wifi
impl block:
impl<'a> Wifi<'a> { pub async fn turn_on_hotspot(&self, ssid: &str, password: &str) -> Result<()> { let mut wireless_settings = HashMap::new(); wireless_settings.insert("ssid", ssid.as_bytes().to_vec().into()); wireless_settings.insert("mode", "ap".into()); // Access Point mode let mut security_settings = HashMap::new(); security_settings.insert("key-mgmt", "wpa-psk".into()); security_settings.insert("psk", password.into()); let mut ipv4_settings: HashMap<&str, Value<'_>> = HashMap::new(); ipv4_settings.insert("method", "shared".into()); // Shared method for internet sharing ipv4_settings.insert( "address-data", vec![HashMap::from([ ("address", Value::from("192.168.123.123")), ("prefix", 24u32.into()), // subnet mask 255.255.255.0 ])] .into(), ); ipv4_settings.insert("gateway", GATEWAY_IP.into()); let mut connection_settings: HashMap<&str, HashMap<&str, Value<'_>>> = HashMap::new(); connection_settings.insert("802-11-wireless", wireless_settings); connection_settings.insert("802-11-wireless-security", security_settings); connection_settings.insert("connection", { let mut cs = HashMap::new(); cs.insert("type", "802-11-wireless".into()); cs.insert("id", ssid.to_string().into()); cs.insert("autoconnect", false.into()); cs }); connection_settings.insert("ipv4", ipv4_settings); // Add the new connection using NetworkManager D-Bus Interface let settings_proxy = Proxy::new( &self.connection, "org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager/Settings", "org.freedesktop.NetworkManager.Settings", ) .await?; let new_connection_path = settings_proxy .call_method("AddConnection", &(connection_settings)) .await? .body::<zbus::zvariant::OwnedObjectPath>()?; // Activate the connection self.network_manager_proxy .call_method( "ActivateConnection", &( new_connection_path, zbus::zvariant::ObjectPath::try_from(self.wifi_device_proxy_path.as_str())?, zbus::zvariant::ObjectPath::try_from("/")?, ), ) .await?; println!("Hotspot created and activated"); Ok(()) } }
The turn_on_hotspot
method sets up configuration for the hotspot, including SSID, security settings, and IPv4 settings - we're
using a static ip address here, but you can always setup dhcp, if you do want to set up a static ip address - for a local network like a Wi-Fi hotspot, you should use IP
addresses from the private IP address ranges. After successfully adding the connection, it activates this new connection, effectively turning on the hotspot.
Here goes the handler:
async fn turn_on_hotspot(State(wifi): State<Arc<Wifi<'_>>>) -> Json<String> { let _ = wifi.turn_on_hotspot("ssid", "password").await.unwrap(); Json("done".to_owned()) }
I've found interacting with d-bus a way to dig deeper, understand system services and also tackle lower level development of applications that interact with these system components.