//! Retrieve network data and modify interface state. //! //! This module contains the core logic of the `peach-network` and //! provides convenience wrappers for a range of `wpasupplicant` commands, //! many of which are ordinarily executed using `wpa_cli` (a WPA command line //! client). //! //! The `wpactrl` crate ([docs](https://docs.rs/wpactrl/0.3.1/wpactrl/)) //! is used to interact with the `wpasupplicant` process. //! //! Switching between client mode and access point mode is achieved by making //! system calls to systemd (via `systemctl`). Further networking functionality //! is provided by making system calls to retrieve interface state and write //! access point credentials to `wpa_supplicant-.conf`. use std::{ fs::OpenOptions, io::prelude::*, process::{Command, Stdio}, result::Result, str, }; use probes::network; #[cfg(feature = "miniserde_support")] use miniserde::{Deserialize, Serialize}; #[cfg(feature = "serde_support")] use serde::{Deserialize, Serialize}; use crate::error::NetworkError; use crate::utils; /// Network SSID. #[derive(Debug)] #[cfg_attr(feature = "miniserde_support", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))] pub struct Network { /// Service Set Identifier (SSID). pub ssid: String, } /// Access point data retrieved via scan. #[derive(Debug)] #[cfg_attr(feature = "miniserde_support", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))] pub struct Scan { /// Frequency. pub frequency: String, /// Protocol. pub protocol: String, /// Signal strength. pub signal_level: String, /// SSID. pub ssid: String, } /// Status data for a network interface. #[derive(Debug)] #[cfg_attr(feature = "miniserde_support", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))] pub struct Status { /// MAC address. pub address: Option, /// Basic Service Set Identifier (BSSID). pub bssid: Option, /// Frequency. pub freq: Option, /// Group cipher. pub group_cipher: Option, /// Local ID. pub id: Option, /// IP address. pub ip_address: Option, /// Key management. pub key_mgmt: Option, /// Mode. pub mode: Option, /// Pairwise cipher. pub pairwise_cipher: Option, /// SSID. pub ssid: Option, /// WPA state. pub wpa_state: Option, } impl Status { fn new() -> Status { Status { address: None, bssid: None, freq: None, group_cipher: None, id: None, ip_address: None, key_mgmt: None, mode: None, pairwise_cipher: None, ssid: None, wpa_state: None, } } } /// Received and transmitted network traffic (bytes). #[derive(Debug)] #[cfg_attr(feature = "miniserde_support", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))] pub struct Traffic { /// Total bytes received. pub received: u64, /// Total bytes transmitted. pub transmitted: u64, } /// SSID and password for a wireless access point. #[derive(Debug)] #[cfg_attr(feature = "miniserde_support", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))] pub struct WiFi { /// SSID. pub ssid: String, /// Password. pub pass: String, } /* GET - Methods for retrieving data */ /// Retrieve list of available wireless access points for a given network /// interface. /// /// # Arguments /// /// * `iface` - A string slice holding the name of a wireless network interface /// /// If the scan results include one or more access points for the given network /// interface, an `Ok` `Result` type is returned containing `Some(Vec)`. /// The vector of `Scan` structs contains data for the in-range access points. /// If no access points are found, a `None` type is returned in the `Result`. /// In the event of an error, a `NetworkError` is returned in the `Result`. pub fn available_networks(iface: &str) -> Result>, NetworkError> { let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?; wpa.request("SCAN")?; let networks = wpa.request("SCAN_RESULTS")?; let mut scan = Vec::new(); for network in networks.lines() { let v: Vec<&str> = network.split('\t').collect(); let len = v.len(); if len > 1 { let frequency = v[1].to_string(); let signal_level = v[2].to_string(); let flags = v[3].to_string(); let flags_vec: Vec<&str> = flags.split("][").collect(); let mut protocol = String::new(); // an open access point (no auth) will only have [ESS] in flags // we only want to return the auth / crypto flags if flags_vec[0] != "[ESS]" { // parse auth / crypto flag and assign it to protocol protocol.push_str(flags_vec[0].replace("[", "").replace("]", "").as_str()); } let ssid = v[4].to_string(); let response = Scan { frequency, protocol, signal_level, ssid, }; scan.push(response) } } if scan.is_empty() { Ok(None) } else { Ok(Some(scan)) } } /// Retrieve network identifier for the network specified by a given interface /// and SSID. /// /// # Arguments /// /// * `iface` - A string slice holding the name of a wireless network interface /// * `ssid` - A string slice holding the SSID of a wireless access point /// /// If the identifier corresponding to the given interface and SSID is /// found in the list of saved networks, an `Ok` `Result` type is returned /// containing `Some(String)` - where `String` is the network identifier. /// If no match is found, a `None` type is returned in the `Result`. In the /// event of an error, a `NetworkError` is returned in the `Result`. pub fn id(iface: &str, ssid: &str) -> Result, NetworkError> { let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?; let networks = wpa.request("LIST_NETWORKS")?; let mut id = Vec::new(); for network in networks.lines() { let v: Vec<&str> = network.split('\t').collect(); let len = v.len(); if len > 1 && v[1] == ssid { id.push(v[0].trim()) } } if id.is_empty() { Ok(None) } else { let network_id: String = id[0].to_string(); Ok(Some(network_id)) } } /// Retrieve IP address for a given interface. /// /// # Arguments /// /// * `iface` - A string slice holding the name of a wireless network interface /// /// If the given interface is found in the list of available interfaces, /// an `Ok` `Result` type is returned containing `Some(String)` - where `String` /// is the IP address of the interface. If no match is found, a `None` type is /// returned in the `Result`. In the event of an error, a `NetworkError` is /// returned in the `Result`. pub fn ip(iface: &str) -> Result, NetworkError> { let net_if: String = iface.to_string(); let ifaces = get_if_addrs::get_if_addrs().map_err(|source| NetworkError::NoIp { iface: net_if, source, })?; let ip = ifaces .iter() .find(|&i| i.name == iface) .map(|iface| iface.ip().to_string()); Ok(ip) } /// Retrieve average signal strength (dBm) for the network associated with /// a given interface. /// /// # Arguments /// /// * `iface` - A string slice holding the name of a wireless network interface /// /// If the signal strength is found for the given interface after polling, /// an `Ok` `Result` type is returned containing `Some(String)` - where `String` /// is the RSSI (Received Signal Strength Indicator) of the connection measured /// in dBm. If signal strength is not found, a `None` type is returned in the /// `Result`. In the event of an error, a `NetworkError` is returned in the /// `Result`. pub fn rssi(iface: &str) -> Result, NetworkError> { let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?; let status = wpa.request("SIGNAL_POLL")?; let rssi = utils::regex_finder(r"RSSI=(.*)\n", &status)?; if rssi.is_none() { let iface = iface.to_string(); Err(NetworkError::Rssi { iface }) } else { Ok(rssi) } } /// Retrieve average signal strength (%) for the network associated with /// a given interface. /// /// # Arguments /// /// * `iface` - A string slice holding the name of a wireless network interface /// /// If the signal strength is found for the given interface after polling, /// an `Ok` `Result` type is returned containing `Some(String)` - where `String` /// is the RSSI (Received Signal Strength Indicator) of the connection measured /// as a percentage. If signal strength is not found, a `None` type is returned /// in the `Result`. In the event of an error, a `NetworkError` is returned in /// the `Result`. pub fn rssi_percent(iface: &str) -> Result, NetworkError> { let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?; let status = wpa.request("SIGNAL_POLL")?; let rssi = utils::regex_finder(r"RSSI=(.*)\n", &status)?; match rssi { Some(rssi) => { // parse the string to a signed integer (for math) let rssi_parsed = rssi.parse::()?; // perform rssi (dBm) to quality (%) conversion let quality_percent = 2 * (rssi_parsed + 100); // convert signal quality integer to string let quality = quality_percent.to_string(); Ok(Some(quality)) } None => { let iface = iface.to_string(); Err(NetworkError::RssiPercent { iface }) } } } /// Retrieve list of all access points with credentials saved in the /// wpasupplicant configuration file. /// /// If the wpasupplicant configuration file contains credentials for one or /// more access points, an `Ok` `Result` type is returned containing /// `Some(Vec)`. The vector of `Network` structs contains the SSIDs /// of all saved networks. If no network credentials are found, a `None` type /// is returned in the `Result`. In the event of an error, a `NetworkError` is /// returned in the `Result`. pub fn saved_networks() -> Result>, NetworkError> { let mut wpa = wpactrl::WpaCtrl::builder().open()?; let networks = wpa.request("LIST_NETWORKS")?; let mut ssids = Vec::new(); for network in networks.lines() { let v: Vec<&str> = network.split('\t').collect(); let len = v.len(); if len > 1 { let ssid = v[1].trim().to_string(); let response = Network { ssid }; ssids.push(response) } } if ssids.is_empty() { Ok(None) } else { Ok(Some(ssids)) } } /// Retrieve SSID for the network associated with a given interface. /// /// # Arguments /// /// * `iface` - A string slice holding the name of a wireless network interface /// /// If the SSID is found in the status output for the given interface, /// an `Ok` `Result` type is returned containing `Some(String)` - where `String` /// is the SSID of the associated network. If SSID is not found, a `None` type /// is returned in the `Result`. In the event of an error, a `NetworkError` is /// returned in the `Result`. pub fn ssid(iface: &str) -> Result, NetworkError> { let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?; let status = wpa.request("STATUS")?; // pass the regex pattern and status output to the regex finder let ssid = utils::regex_finder(r"\nssid=(.*)\n", &status)?; Ok(ssid) } /// Retrieve state for a given interface. /// /// # Arguments /// /// * `iface` - A string slice holding the name of a wireless network interface /// /// If the state is found for the given interface, an `Ok` `Result` type is /// returned containing `Some(String)` - where `String` is the state of the /// network interface. If state is not found, a `None` type is returned in the /// `Result`. In the event of an error, a `NetworkError` is returned in the /// `Result`. pub fn state(iface: &str) -> Result, NetworkError> { // construct the interface operstate path let iface_path: String = format!("/sys/class/net/{}/operstate", iface); // execute the cat command and save output, catching any errors let output = Command::new("cat") .arg(iface_path) .output() .map_err(|source| NetworkError::NoState { iface: iface.to_string(), source, })?; if !output.stdout.is_empty() { // unwrap the command result and convert to String let mut state = String::from_utf8(output.stdout).unwrap(); // remove trailing newline character let len = state.len(); state.truncate(len - 1); return Ok(Some(state)); } Ok(None) } /// Retrieve status for a given interface. /// /// # Arguments /// /// * `iface` - A string slice holding the name of a wireless network interface /// /// If the status is found for the given interface, an `Ok` `Result` type is /// returned containing `Some(Status)` - where `Status` is a `struct` /// containing the aggregated interface data in named fields. If status is not /// found, a `None` type is returned in the `Result`. In the event of an error, /// a `NetworkError` is returned in the `Result`. pub fn status(iface: &str) -> Result, NetworkError> { let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?; let wpa_status = wpa.request("STATUS")?; // pass the regex pattern and status output to the regex finder let state = utils::regex_finder(r"wpa_state=(.*)\n", &wpa_status)?; // regex_finder returns an Option type, unwrap or replace None with ERROR let wpa_state = state.unwrap_or_else(|| "ERROR".to_string()); // create new Status object (all fields are None type by default) let mut status = Status::new(); // match on wpa_state and set Status fields accordingly match wpa_state.as_ref() { "ERROR" => status.wpa_state = Some("ERROR".to_string()), "UNKNOWN" => status.wpa_state = Some("UNKNOWN".to_string()), "INTERFACE_DISABLED" => status.wpa_state = Some("DISABLED".to_string()), "INACTIVE" => status.wpa_state = Some("INACTIVE".to_string()), "DISCONNECTED" => status.wpa_state = Some("DISCONNECTED".to_string()), "SCANNING" => status.wpa_state = Some("SCANNING".to_string()), "ASSOCIATING" => status.wpa_state = Some("ASSOCIATING".to_string()), "ASSOCIATED" => status.wpa_state = Some("ASSOCIATED".to_string()), "AUTHENTICATING" => status.wpa_state = Some("AUTHENTICATING".to_string()), "4WAY_HANDSHAKE" => status.wpa_state = Some("4WAY_HANDSHAKE".to_string()), "GROUP_HANDSHAKE" => status.wpa_state = Some("GROUP_HANDSHAKE".to_string()), // retrieve additional status fields only if wpa_state is COMPLETED "COMPLETED" => { status.address = utils::regex_finder(r"\naddress=(.*)\n", &wpa_status)?; status.bssid = utils::regex_finder(r"\nbssid=(.*)\n", &wpa_status)?; status.freq = utils::regex_finder(r"\nfreq=(.*)\n", &wpa_status)?; status.group_cipher = utils::regex_finder(r"\ngroup_cipher=(.*)\n", &wpa_status)?; status.id = utils::regex_finder(r"\nid=(.*)\n", &wpa_status)?; status.ip_address = utils::regex_finder(r"\nip_address=(.*)\n", &wpa_status)?; status.key_mgmt = utils::regex_finder(r"\nkey_mgmt=(.*)\n", &wpa_status)?; status.mode = utils::regex_finder(r"\nmode=(.*)\n", &wpa_status)?; status.pairwise_cipher = utils::regex_finder(r"\npairwise_cipher=(.*)\n", &wpa_status)?; status.ssid = utils::regex_finder(r"\nssid=(.*)\n", &wpa_status)?; status.wpa_state = utils::regex_finder(r"\nwpa_state=(.*)\n", &wpa_status)?; } _ => (), } Ok(Some(status)) } /// Retrieve network traffic statistics for a given interface. /// /// # Arguments /// /// * `iface` - A string slice holding the name of a wireless network interface /// /// If the network traffic statistics are found for the given interface, an `Ok` /// `Result` type is returned containing `Some(Traffic)`. The `Traffic` `struct` /// includes fields for received and transmitted network data statistics. If /// network traffic statistics are not found for the given interface, a `None` /// type is returned in the `Result`. In the event of an error, a `NetworkError` /// is returned in the `Result`. pub fn traffic(iface: &str) -> Result, NetworkError> { let network = network::read().map_err(|source| NetworkError::NoTraffic { iface: iface.to_string(), source, })?; // iterate through interfaces returned in network data for (interface, traffic) in network.interfaces { if interface == iface { let received = traffic.received; let transmitted = traffic.transmitted; let traffic = Traffic { received, transmitted, }; return Ok(Some(traffic)); } } Ok(None) } /* SET - Methods for modifying state */ /// Start network interface service. /// /// A `systemctl `command is invoked which starts the service for the given /// network interface. If the command executes successfully, an `Ok` `Result` /// type is returned. In the event of an error, a `NetworkError` is returned /// in the `Result`. pub fn start_iface_service(iface: String) -> Result<(), NetworkError> { let iface_service = format!("wpa_supplicant@{}.service", &iface); // start the interface service Command::new("sudo") .arg("/usr/bin/systemctl") .arg("start") .arg(iface_service) .output() .map_err(|source| NetworkError::StartInterface { source, iface })?; Ok(()) } /// Add network credentials for a given wireless access point. /// /// # Arguments /// /// * `wlan_iface` - A local wireless interface. /// * `wifi` - An instance of the `WiFi` `struct` with fields `ssid` and `pass` /// /// If configuration parameters are successfully generated from the provided /// SSID and password and appended to `wpa_supplicant-.conf` (where /// `` is the provided interface parameter), an `Ok` `Result` type /// is returned. In the event of an error, a `NetworkError` is returned in the /// `Result`. pub fn add(wlan_iface: String, wifi: &WiFi) -> Result<(), NetworkError> { // generate configuration based on provided ssid & password let output = Command::new("wpa_passphrase") .arg(&wifi.ssid) .arg(&wifi.pass) .stdout(Stdio::piped()) .output() .map_err(|source| NetworkError::GenWpaPassphrase { ssid: wifi.ssid.to_string(), source, })?; // prepend newline to wpa_details to safeguard against malformed supplicant let mut wpa_details = "\n".as_bytes().to_vec(); wpa_details.extend(&*(output.stdout)); let wlan_config = format!("/etc/wpa_supplicant/wpa_supplicant-{}.conf", wlan_iface); // append wpa_passphrase output to wpa_supplicant-.conf if successful if output.status.success() { // open file in append mode let file = OpenOptions::new().append(true).open(wlan_config); let _file = match file { // if file exists & open succeeds, write wifi configuration Ok(mut f) => f.write(&wpa_details), // TODO: handle this better: create file if not found // & seed with 'ctrl_interace' & 'update_config' settings // config file could also be copied from peach/config fs location Err(e) => panic!("Failed to write to file: {}", e), }; Ok(()) } else { let err_msg = String::from_utf8_lossy(&output.stdout); Err(NetworkError::GenWpaPassphraseWarning { ssid: wifi.ssid.to_string(), err_msg: err_msg.to_string(), }) } } /// Deploy an access point if the wireless interface is `up` without an active /// connection. /// /// The status of the wireless service and the state of the wireless interface /// are checked. If the service is active but the interface is down (ie. not /// currently connected to an access point), then the access point is activated /// by calling the `activate_ap()` function. pub fn check_iface(wlan_iface: String, ap_iface: String) -> Result<(), NetworkError> { let wpa_service = format!("wpa_supplicant@{}.service", &wlan_iface); // returns 0 if the service is currently active let wlan_status = Command::new("/usr/bin/systemctl") .arg("is-active") .arg(wpa_service) .status() .map_err(NetworkError::WlanState)?; // returns the current state of the wlan interface let iface_state = state(&wlan_iface)?; // returns down if the interface is not currently connected to an ap let wlan_state = match iface_state { Some(state) => state, None => "error".to_string(), }; // if wlan is active but not connected, start the ap service if wlan_status.success() && wlan_state == "down" { start_iface_service(ap_iface)? } Ok(()) } /// Connect with an access point for a given network identifier and interface. /// Results in connections with other access points being disabled. /// /// # Arguments /// /// * `id` - A string slice holding the network identifier of an access point /// * `iface` - A string slice holding the name of a wireless network interface /// /// If the network connection is successfully activated for the access point /// represented by the given network identifier on the given wireless interface, /// an `Ok` `Result`type is returned. In the event of an error, a `NetworkError` /// is returned in the `Result`. pub fn connect(id: &str, iface: &str) -> Result<(), NetworkError> { let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?; let select = format!("SELECT {}", id); wpa.request(&select)?; Ok(()) } /// Delete network credentials for a given network identifier and interface. /// /// # Arguments /// /// * `id` - A string slice holding the network identifier of an access point /// * `iface` - A string slice holding the name of a wireless network interface /// /// If the network configuration parameters are successfully deleted for /// the access point represented by the given network identifier, an `Ok` /// `Result`type is returned. In the event of an error, a `NetworkError` is /// returned in the `Result`. pub fn delete(id: &str, iface: &str) -> Result<(), NetworkError> { let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?; let remove = format!("REMOVE_NETWORK {}", id); wpa.request(&remove)?; Ok(()) } /// Disable network connection for a given network identifier and interface. /// /// # Arguments /// /// * `id` - A string slice holding the network identifier of an access point /// * `iface` - A string slice holding the name of a wireless network interface /// /// If the network connection is successfully disabled for the access point /// represented by the given network identifier, an `Ok` `Result`type is /// returned. In the event of an error, a `NetworkError` is returned in the /// `Result`. pub fn disable(id: &str, iface: &str) -> Result<(), NetworkError> { let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?; let disable = format!("DISABLE_NETWORK {}", id); wpa.request(&disable)?; Ok(()) } /// Disconnect network connection for a given wireless interface. /// /// # Arguments /// /// * `iface` - A string slice holding the name of a wireless network interface /// /// If the network connection is successfully disconnected for the given /// wireless interface, an `Ok` `Result` type is returned. In the event of an /// error, a `NetworkError` is returned in the `Result`. pub fn disconnect(iface: &str) -> Result<(), NetworkError> { let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?; let disconnect = "DISCONNECT".to_string(); wpa.request(&disconnect)?; Ok(()) } /// Modify password for a given network identifier and interface. /// /// # Arguments /// /// * `id` - A string slice holding the network identifier of an access point /// * `iface` - A string slice holding the name of a wireless network interface /// * `pass` - A string slice holding the password for a wireless access point /// /// If the password is successfully updated for the access point represented by /// the given network identifier, an `Ok` `Result` type is returned. In the /// event of an error, a `NetworkError` is returned in the `Result`. pub fn modify(id: &str, iface: &str, pass: &str) -> Result<(), NetworkError> { let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?; let new_pass = format!("NEW_PASSWORD {} {}", id, pass); wpa.request(&new_pass)?; Ok(()) } /// Reassociate with an access point for a given wireless interface. /// /// # Arguments /// /// * `iface` - A string slice holding the name of a wireless network interface /// /// If the network connection is successfully reassociated for the given /// wireless interface, an `Ok` `Result` type is returned. In the event of an /// error, a `NetworkError` is returned in the `Result`. pub fn reassociate(iface: &str) -> Result<(), NetworkError> { let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?; wpa.request("REASSOCIATE")?; Ok(()) } /// Reconfigure `wpa_supplicant` by forcing a reread of the configuration file. /// /// If the reconfigure command is successfully executed, indicating a reread /// of the `wpa_supplicant.conf` file by the `wpa_supplicant` process, an `Ok` /// `Result` type is returned. In the event of an error, a `NetworkError` is /// returned in the `Result`. pub fn reconfigure() -> Result<(), NetworkError> { let mut wpa = wpactrl::WpaCtrl::builder().open()?; wpa.request("RECONFIGURE")?; Ok(()) } /// Reconnect network connection for a given wireless interface. /// /// # Arguments /// /// * `iface` - A string slice holding the name of a wireless network interface /// /// If the network connection is successfully disconnected and reconnected for /// the given wireless interface, an `Ok` `Result` type is returned. In the /// event of an error, a `NetworkError` is returned in the `Result`. pub fn reconnect(iface: &str) -> Result<(), NetworkError> { let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?; wpa.request("DISCONNECT")?; wpa.request("RECONNECT")?; Ok(()) } /// Save configuration updates to the `wpa_supplicant` configuration file. /// /// If wireless network configuration updates are successfully save to the /// `wpa_supplicant.conf` file, an `Ok` `Result` type is returned. In the /// event of an error, a `NetworkError` is returned in the `Result`. pub fn save() -> Result<(), NetworkError> { let mut wpa = wpactrl::WpaCtrl::builder().open()?; wpa.request("SAVE_CONFIG")?; Ok(()) }