For a fun side project recently I needed to put a linux computer’s wifi card into “access point” mode. I was working from Rust and so, even though I could probably have just called out to the nmcli command line tool, I wanted to try to do the whole thing from within my binary. Fortunately, NetworkManager has a D-Bus API that exposes basically all the configuration options available. Also fortunately, Rust has an extensive and fairly well documented D-Bus crate called zbus. Unfortunately, there are a couple crucial points where the documentation is a bit vague, so I thought I’d write up the process for anyone else who ran into the same hiccups as I did.

First, zbus has an excellent binary called zbus-xmlgen that uses D-Bus’ introspection ability to auto-generate code that can be used to interface with a given D-Bus endpoint. The zbus book (which is definitely worth reading if you’re using zbus) has a section about the tool. To get the generated code I needed for creating a new connection, which NetworkManager includes in its Settings interface, I ran the following command:

$ zbus-xmlgen system org.freedesktop.NetworkManager /org/freedesktop/NetworkManager/Settings

This produced a Rust file called settings.rs the content of which can be seen here.

The first challenge arose here. Even though the generated code includes a function called add_connection, which is what we’re looking to do, the function takes as its argument a HashMap (what you would call a dictionary in Python or a map in Go) with strings as the keys and zbus::zvariant::Value as a value. The generated code doesn’t say (and really has no way of knowing) what should go into that HashMap because that isn’t defined by the xml returned by D-Bus’ introspection. Instead, we need to go to NetworkManager’s documentation (specifically the D-Bus API Reference). There we can quickly find the definition of the add_connection function and see that the argument is of type a{sa{sv}} and must contain the “[c]onnection settings and properties.” Perhaps not as much clarity as we might like. There is a very handy list of all the different configuration settings in the NetworkManager documentation, but it still doesn’t quite answer the question of what to pass to add_connection.

For that, you need to turn to one more resource: the example code. They don’t have Rust examples, but the Python one gives enough details to figure out that the function needs to be passed a dictionary (or a HashMap in our case) full of other dictionaries that correspond to the configuration settings we’re looking to set from the list above.

So, with that knowledge, we can turn back to the generated code from zbus. One of the cool things about the code generated by zbus-xmlgen is that you can modify it. In particular, as they discuss in the book, you can add types to replace the rather generic arguments that the code generates. This is really useful for making sure that you send in compliant arguments and avoid runtime errors later. There are also special derive macros for serializing structs into D-Bus dictionaries, SerializeDict and DeserializeDict. I played around with these for a while before I unfortunately discovered that they don’t work on nested structs yet. You have to use HashMaps for now. Still, if you can use it for your purposes, I definitely recommend it.

There is one other change I would recommend making, and that is in the definition of the function add_connection. The generated code suggests that the argument should be of type HashMap<&str, &Value>, but I recommend changing it to HashMap<&str, Value> (note that Value is not borrowed in the latter) unless you have a very good reason to want to keep ownership of the items you are passing in. This avoids having to annotate lifetimes and the change doesn’t seem to bother the generated code one bit.

So, instead of using a struct, we need to assemble the HashMaps by hand, just as they do in the Python example. It looks like this in the end:

/// conn is an established connection to the D-Bus system bus
pub async fn launch_hotspot(conn: &zbus::Connection) -> Result<(), Error> {
    let nw_settings_proxy = crate::net::settings::SettingsProxy::new(&conn)
        .await
        .expect("Couldn't find Network Manager settings over DBus.");
    let mut connection: HashMap<&str, Value> = HashMap::new();
    connection.insert("type", "802-11-wireless".into());
    // You will want to generate your own UUID
    connection.insert("uuid", "1abe6bff-ce58-488d-b16c-bf0770ca70ec".into()); 
    connection.insert("id", "Any String Here".into());
    let mut wireless_settings: HashMap<&str, Value> = HashMap::new();
    wireless_settings.insert("ssid", "Your SSID Here".as_bytes().into());
    wireless_settings.insert("mode", "ap".into());
    wireless_settings.insert("band", "bg".into());
    wireless_settings.insert("channel", 6u32.into()); // Pick your favorite WiFi channel
    let mut ipv4: HashMap<&str, Value> = HashMap::new();
    ipv4.insert("method", "shared".into());

    let mut connection_config: HashMap<&str, HashMap<&str, Value>> = HashMap::new();

    connection_config.insert("connection", connection);
    connection_config.insert("802-11-wireless", wireless_settings);
    connection_config.insert("ipv4", ipv4);

    let ap_connection = match nw_settings_proxy
        .add_connection(connection_config)
        .await
    {
        Ok(c) => c,
        Err(e) => panic!("{}", e),
    };
    return Ok(());
}

With all that work done, we can now order NetworkManager to move the WiFi card into AP mode! Be sure to do something with the possible error returned by add_connection, since there are a number of ways the call can go wrong. In particular, the NetworkManager API is restricted to being modified by root, so your binary will need to be running as root or you will get an error. You’ll also get an error if you haven’t constructed the HashMap properly.

There are all kinds of other things you can do with D-Bus as well, including interacting with systemd to launch or stop services, and with the Notifications daemon to display desktop notifications. Enjoy!