// SPDX-License-Identifier: Apache-2.0 // Copyright Pionix GmbH and Contributors to EVerest #include "WiFiSetup.hpp" #include #include #include #include #include #include #include using namespace everest::run_application; /** * @file * @brief wpa_cli command failure detection * * `wpa_cli` sets an exit code of 0 unless the command is malformed. * Failures are presented via text to stdout. * Hence checking for failure to remove a network would mean checking * the output for OK or FAIL. * * This is common across all calls to `wpa_cli`. */ namespace { inline int hex_digit_to_nibble(std::uint8_t c) { int result{-1}; if ((c >= '0') && (c <= '9')) { result = static_cast(c - '0'); } else if ((c >= 'a') && (c <= 'f')) { result = static_cast(c - 'a') + 10; } else if ((c >= 'A') && (c <= 'F')) { result = static_cast(c - 'A') + 10; } return result; } inline int hex_to_int(std::uint8_t high, std::uint8_t low) { int result{-1}; const auto h = hex_digit_to_nibble(high); const auto l = hex_digit_to_nibble(low); if ((h != -1) && (l != -1)) { const auto hc = static_cast(h) << 4U; const auto lc = static_cast(l); result = static_cast(hc | lc); } return result; } inline std::uint8_t nibble_to_hex_digit(std::uint8_t c) { std::uint8_t result{}; c &= 0x0fU; if (c <= 9) { result = c + '0'; } else { result = (c - 10) + 'a'; } return result; } inline void int_to_hex(std::uint8_t& high, std::uint8_t& low, std::uint8_t c) { high = nibble_to_hex_digit(c >> 4U); low = nibble_to_hex_digit(c); } } // namespace namespace module { constexpr const char* wpa_cli = "/usr/sbin/wpa_cli"; constexpr const int not_connected_rssi = -100; // -100 dBm is the minimum for wifi bool WpaCliSetup::do_scan(const std::string& interface) { if (!is_wifi_interface(interface)) { return false; } auto output = run_application(wpa_cli, {"-i", interface, "scan"}); return output.exit_code == 0; } WpaCliSetup::WifiScanList WpaCliSetup::do_scan_results(const std::string& interface) { WifiScanList result = {}; auto output = run_application(wpa_cli, {"-i", interface, "scan_results"}); if (output.exit_code == 0) { auto scan_results = output.split_output; if (scan_results.size() >= 2) { // skip header for (auto scan_results_it = std::next(scan_results.begin()); scan_results_it != scan_results.end(); ++scan_results_it) { std::vector columns; std::istringstream stream(*scan_results_it); for (std::string value; std::getline(stream, value, '\t');) { columns.push_back(std::move(value)); } if (columns.size() >= 5) { WifiScan info; info.bssid = columns[0]; info.ssid = columns[4]; info.frequency = std::stoi(columns[1]); info.signal_level = std::stoi(columns[2]); info.flags = std::move(parse_flags(columns[3])); result.push_back(std::move(info)); } } } } return result; } WpaCliSetup::Status WpaCliSetup::do_status(const std::string& interface) { Status result = {}; if (is_wifi_interface(interface)) { auto output = run_application(wpa_cli, {"-i", interface, "status"}); if (output.exit_code == 0) { auto scan_results = output.split_output; for (auto& scan_result : scan_results) { std::vector columns; std::istringstream ss(scan_result); for (std::string value; std::getline(ss, value, '=');) { columns.push_back(std::move(value)); } if (columns.size() == 2) { result[columns[0]] = columns[1]; } } } } return result; } WpaCliSetup::Poll WpaCliSetup::do_signal_poll(const std::string& interface) { Poll result = {}; if (is_wifi_interface(interface)) { auto output = run_application(wpa_cli, {"-i", interface, "signal_poll"}); if (output.exit_code == 0) { auto scan_results = output.split_output; for (auto& scan_result : scan_results) { std::vector columns; std::istringstream ss(scan_result); for (std::string value; std::getline(ss, value, '=');) { columns.push_back(std::move(value)); } if (columns.size() == 2) { result[columns[0]] = columns[1]; } } } } return result; } WpaCliSetup::flags_t WpaCliSetup::parse_flags(const std::string& flags) { const std::regex flags_regex("\\[(.*?)\\]"); flags_t parsed_flags; for (auto it = std::sregex_iterator(flags.begin(), flags.end(), flags_regex); it != std::sregex_iterator(); ++it) { parsed_flags.push_back((*it).str(1)); } return parsed_flags; } int WpaCliSetup::add_network(const std::string& interface) { if (!is_wifi_interface(interface)) { return -1; } auto output = run_application(wpa_cli, {"-i", interface, "add_network"}); if ((output.exit_code != 0) || (output.split_output.size() != 1)) { return -1; } return std::stoi(output.split_output.at(0)); } bool WpaCliSetup::set_network(const std::string& interface, int network_id, const std::string& ssid, const std::string& psk, network_security_t mode, bool hidden) { /* * configuring a network needs: * - ssid "" * - psk "" or ABCDEF0123456789... (for WPA2) * - sae_password "" (for WPA3) * - key_mgmt NONE (for open networks) * - scan_ssid 1 (for hidden networks) * * Support for WPA3 requires: * - key_mgmt WPA-PSK WPA-PSK-SHA256 SAE or SAE if WPA3 only * - sae_password replaces psk, WPA3 doesn't support PreSharedKey (64 hex digits) * the passphrase is required * - for interworking WPA2 and WPA3 the passphrase is needed * - psk with hex digits (PreSharedKey) doesn't work */ /* * From wpa_supplicant/hostapd * ieee80211w: Whether management frame protection (MFP) is enabled * 0 = disabled (default) * 1 = optional * 2 = required * The most common configuration options for this based on the PMF (protected * management frames) certification program are: * PMF enabled: ieee80211w=1 and wpa_key_mgmt=WPA-EAP WPA-EAP-SHA256 * PMF required: ieee80211w=2 and wpa_key_mgmt=WPA-EAP-SHA256 * (and similarly for WPA-PSK and WPA-PSK-SHA256 if WPA2-Personal is used) * WPA3-Personal-only mode: ieee80211w=2 and wpa_key_mgmt=SAE */ constexpr std::uint8_t wpa2_psk_size = 64U; if (!is_wifi_interface(interface)) { return false; } if (psk.empty()) { // force security mode to none mode = network_security_t::none; } const char* key_mgt = nullptr; const char* psk_name = nullptr; const char* ieee80211w = nullptr; switch (mode) { case network_security_t::none: key_mgt = "NONE"; break; case network_security_t::wpa2_only: key_mgt = "WPA-PSK"; psk_name = "psk"; break; case network_security_t::wpa3_only: key_mgt = "SAE"; psk_name = "sae_password"; ieee80211w = "2"; break; case network_security_t::wpa2_and_wpa3: default: if (psk.size() == wpa2_psk_size) { // WPA3 doesn't support PSK (hex digits), it needs a passphrase key_mgt = "WPA-PSK"; psk_name = "psk"; } else if (psk.size() > wpa2_psk_size) { // WPA2 doesn't support passphrases > 63 characters key_mgt = "SAE"; psk_name = "sae_password"; ieee80211w = "2"; } else { key_mgt = "WPA-PSK WPA-PSK-SHA256 SAE"; psk_name = "psk"; ieee80211w = "1"; } break; } auto network_id_string = std::to_string(network_id); // de-escaping SSID strings in wpa_supplicant is not reliable. // hence providing the SSID as a string of hex digits auto ssid_parameter = ssid_to_hex(ssid); auto output = run_application(wpa_cli, {"-i", interface, "set_network", network_id_string, "ssid", ssid_parameter}); if ((output.exit_code == 0) && (psk_name != nullptr)) { output = run_application(wpa_cli, {"-i", interface, "set_network", network_id_string, psk_name, psk}); } if (output.exit_code == 0) { output = run_application(wpa_cli, {"-i", interface, "set_network", network_id_string, "key_mgmt", key_mgt}); } if ((output.exit_code == 0) && (ieee80211w != nullptr)) { output = run_application(wpa_cli, {"-i", interface, "set_network", network_id_string, "ieee80211w", ieee80211w}); } if (hidden && (output.exit_code == 0)) { output = run_application(wpa_cli, {"-i", interface, "set_network", network_id_string, "scan_ssid", "1"}); } return output.exit_code == 0; } bool WpaCliSetup::enable_network(const std::string& interface, int network_id) { if (!is_wifi_interface(interface)) { return false; } auto network_id_string = std::to_string(network_id); auto output = run_application(wpa_cli, {"-i", interface, "enable_network", network_id_string}); return output.exit_code == 0; } bool WpaCliSetup::disable_network(const std::string& interface, int network_id) { if (!is_wifi_interface(interface)) { return false; } auto network_id_string = std::to_string(network_id); auto output = run_application(wpa_cli, {"-i", interface, "disable_network", network_id_string}); return output.exit_code == 0; } bool WpaCliSetup::select_network(const std::string& interface, int network_id) { if (!is_wifi_interface(interface)) { return false; } auto network_id_string = std::to_string(network_id); auto output = run_application(wpa_cli, {"-i", interface, "select_network", network_id_string}); return output.exit_code == 0; } bool WpaCliSetup::remove_network(const std::string& interface, int network_id) { if (!is_wifi_interface(interface)) { return false; } auto network_id_string = std::to_string(network_id); auto output = run_application(wpa_cli, {"-i", interface, "remove_network", network_id_string}); return output.exit_code == 0; } bool WpaCliSetup::save_config(const std::string& interface) { if (!is_wifi_interface(interface)) { return false; } auto output = run_application(wpa_cli, {"-i", interface, "save_config"}); return output.exit_code == 0; } WpaCliSetup::WifiScanList WpaCliSetup::scan_wifi(const std::string& interface) { WifiScanList result = {}; if (do_scan(interface)) { // FIXME: is there a proper signal to check if the scan is ready? Maybe in the socket based interface std::this_thread::sleep_for(std::chrono::seconds(3)); result = std::move(do_scan_results(interface)); } return result; } WpaCliSetup::WifiNetworkList WpaCliSetup::list_networks(const std::string& interface) { WifiNetworkList result = {}; if (is_wifi_interface(interface)) { auto output = run_application(wpa_cli, {"-i", interface, "list_networks"}); if (output.exit_code == 0) { auto scan_results = output.split_output; if (scan_results.size() >= 2) { // skip header for (auto scan_results_it = std::next(scan_results.begin()); scan_results_it != scan_results.end(); ++scan_results_it) { std::vector columns; std::istringstream ss(*scan_results_it); for (std::string value; std::getline(ss, value, '\t');) { columns.push_back(std::move(value)); } if (columns.size() >= 2) { WifiNetwork info; info.network_id = std::stoi(columns[0]); info.ssid = columns[1]; result.push_back(std::move(info)); } } } } } return result; } WpaCliSetup::WifiNetworkStatusList WpaCliSetup::list_networks_status(const std::string& interface) { WifiNetworkStatusList result = {}; if (is_wifi_interface(interface)) { auto network_list = list_networks(interface); auto status_map = do_status(interface); int connected_rssi = not_connected_rssi; // signal_poll raises errors when not connected if (status_map["wpa_state"] == "COMPLETED") { auto signal_map = do_signal_poll(interface); if (auto it = signal_map.find("RSSI"); it != signal_map.end()) { connected_rssi = std::stoi(it->second); } } for (auto& i : network_list) { WifiNetworkStatus net; net.interface = interface; net.network_id = i.network_id; net.ssid = i.ssid; net.connected = false; net.signal_level = not_connected_rssi; auto id_it = status_map.find("id"); auto ssid_it = status_map.find("ssid"); if ((id_it != status_map.end()) && (ssid_it != status_map.end()) && (std::stoi(id_it->second) == i.network_id) && (ssid_it->second == i.ssid)) { net.connected = true; net.signal_level = connected_rssi; } result.push_back(net); } } return result; } bool WpaCliSetup::is_wifi_interface(const std::string& interface) { // check if /sys/class/net//wireless exists auto path = std::filesystem::path("/sys/class/net"); path /= interface; path /= "wireless"; return std::filesystem::exists(path); } int Ssid::standard(std::uint8_t c) { int output{-1}; if (c == '\\') { handler = &Ssid::backslash; } else { output = static_cast(c); } return output; } int Ssid::backslash(std::uint8_t c) { int output{static_cast(c)}; handler = &Ssid::standard; switch (c) { case '"': case '\\': break; case 'n': output = '\n'; break; case 'r': output = '\r'; break; case 't': output = '\t'; break; case 'e': output = '\033'; break; case 'x': handler = &Ssid::hex_1; output = -1; break; default: // malformed error = true; output = -1; break; } return output; } int Ssid::hex_1(std::uint8_t c) { tmp = static_cast(c); handler = &Ssid::hex_2; return -1; } int Ssid::hex_2(std::uint8_t c) { int output{-1}; handler = &Ssid::standard; const auto res = hex_to_int(tmp, c); if (res != -1) { output = res; } else { error = true; // malformed } return output; } std::string Ssid::to_hex(const std::string& ssid) { std::ostringstream ss; handler = &Ssid::standard; error = false; for (const auto& c : ssid) { const auto output = std::invoke(handler, this, static_cast(c)); if (error) { break; } if (output != -1) { ss << std::hex << std::setw(2) << std::setfill('0') << output; } } return (error) ? std::string{} : ss.str(); } void Ssid::encode(int c, std::ostringstream& ss) { if ((c < 0) || (c > 255)) { error = true; } else { switch (c) { case '"': ss << R"(\")"; break; case '\\': ss << R"(\\)"; break; case '\033': ss << R"(\e)"; break; case '\n': ss << R"(\n)"; break; case '\r': ss << R"(\r)"; break; case '\t': ss << R"(\t)"; break; default: { if ((c >= 32) && (c <= 126)) { ss << static_cast(c); } else { std::uint8_t high{}; std::uint8_t low{}; int_to_hex(high, low, c); ss << R"(\x)" << static_cast(high) << static_cast(low); } break; } } } } std::string Ssid::from_hex(const std::string& hex) { std::ostringstream ss; bool high{true}; error = (hex.size() % 2) != 0; for (const auto& c : hex) { if (error) { break; } if (high) { tmp = static_cast(c); high = false; } else { const auto output = hex_to_int(tmp, static_cast(c)); encode(output, ss); high = true; } } return (error) ? std::string{} : ss.str(); } std::string WpaCliSetup::hex_to_ssid(const std::string& hex) { Ssid converter; return converter.from_hex(hex); } std::string WpaCliSetup::ssid_to_hex(const std::string& ssid) { Ssid converter; return converter.to_hex(ssid); } } // namespace module