/* * \brief Startup Wifi driver * \author Josef Soentgen * \date 2018-07-31 * * This wifi driver front end uses the CTRL interface of the wpa_supplicant via * a Genode specific backend, which merely wraps a shared memory buffer, to * manage the supplicant. * * Depending on the 'wifi_config' ROM content it will instruct the supplicant * to enable, disable and connect to wireless networks. Commands and their * corresponding execute result are handled by the '_cmd_handler' dispatcher. * This handler drives the front end's state-machine. Any different type of * action can only be initiated from the 'IDLE' state. Unsolicited events, e.g. * a scan-results-available event, may influence the current state. Config * updates are deferred in case the current state is not 'IDLE'. * * brain-dump * ========== * * config update overview: * [[block any new update]] > [mark stale] > [rm stale] > [add new] > [update new] > [[unblock update]] * * add new network: * [[new ap]] > [ssid] > bssid? + [bssid] > [psk] > auto? + [enable] > new ap? + [[new ap]] * * update network: * [[update ap] > bssid? + [bssid] > psk? + [psk] > auto? + [enable] > update ap? + [[update ap]] * * remove network: * [[mark stale]] > [remove network] > stale? + [remove network] */ /* * Copyright (C) 2018 Genode Labs GmbH * * This file is distributed under the terms of the GNU General Public License * version 2. */ #ifndef _WIFI_FRONTEND_H_ #define _WIFI_FRONTEND_H_ /* Genode includes */ #include #include #include #include #include #include #include #include #include #include /* rep includes */ #include #include /* local includes */ #include /* declare manually as it is a internal hack^Winterface */ extern void wifi_kick_socketcall(); namespace Wifi { struct Frontend; } /* keep ordered! */ static struct Recv_msg_table { char const *string; size_t len; } recv_table[] = { { "OK", 2 }, { "FAIL", 4 }, { "CTRL-EVENT-SCAN-RESULTS", 23 }, { "CTRL-EVENT-CONNECTED", 20 }, { "CTRL-EVENT-DISCONNECTED", 23 }, { "SME: Trying to authenticate", 27 }, { "CTRL-EVENT-NETWORK-NOT-FOUND", 28 }, }; enum Rmi { OK = 0, FAIL, SCAN_RESULTS, CONNECTED, DISCONNECTED, SME_AUTH, NOT_FOUND, }; static inline bool check_recv_msg(char const *msg, Recv_msg_table const &entry) { return Genode::strcmp(entry.string, msg, entry.len) == 0; } static bool cmd_successful(char const *msg) { return check_recv_msg(msg, recv_table[OK]); } static bool cmd_fail(char const *msg) { return check_recv_msg(msg, recv_table[FAIL]); } static bool results_available(char const *msg) { return check_recv_msg(msg, recv_table[SCAN_RESULTS]); } static bool connecting_to_network(char const *msg) { return check_recv_msg(msg, recv_table[SME_AUTH]); } static bool network_not_found(char const *msg) { return check_recv_msg(msg, recv_table[NOT_FOUND]); } static bool scan_results(char const *msg) { return Genode::strcmp("bssid", msg, 5) == 0; } static bool list_network_results(char const *msg) { return Genode::strcmp("network", msg, 7) == 0; } /* * Central network data structure */ struct Accesspoint : Genode::Interface { using Bssid = Genode::String<17+1>; using Freq = Genode::String< 4+1>; using Prot = Genode::String< 7+1>; using Ssid = Genode::String<32+1>; using Pass = Genode::String<63+1>; /* * Accesspoint information fields used by the front end */ Bssid bssid { }; Freq freq { }; Prot prot { }; Ssid ssid { }; Pass pass { }; unsigned signal { 0 }; /* * CTRL interface fields * * The 'enabled' field is set to true if ENABLE_NETWORK * was successfully executed. The network itself might * get disabled by wpa_supplicant itself in case it cannot * connect to the network, which will _not_ be reflected * here. */ int id { -1 }; bool enabled { false }; /* * Internal configuration fields */ bool auto_connect { false }; bool update { false }; bool stale { false }; bool explicit_scan { false }; /** * Default constructor */ Accesspoint() { } /** * Constructor that initializes information fields */ Accesspoint(char const *bssid, char const *freq, char const *prot, char const *ssid, unsigned signal) : bssid(bssid), freq(freq), prot(prot), ssid(ssid), signal(signal) { } void invalidate() { ssid = Ssid(); bssid = Bssid(); } bool valid() const { return ssid.length() > 1; } bool bssid_valid() const { return bssid.length() > 1; } bool wpa() const { return prot != "NONE"; } bool stored() const { return id != -1; } }; template static void for_each_line(char const *msg, FUNC const &func) { char line_buffer[1024]; size_t cur = 0; while (msg[cur] != 0) { size_t until = Util::next_char(msg, cur, '\n'); Genode::memcpy(line_buffer, &msg[cur], until); line_buffer[until] = 0; cur += until + 1; func(line_buffer); } } template static void for_each_result_line(char const *msg, FUNC const &func) { char line_buffer[1024]; size_t cur = 0; /* skip headline */ size_t until = Util::next_char(msg, cur, '\n'); cur += until + 1; while (msg[cur] != 0) { until = Util::next_char(msg, cur, '\n'); Genode::memcpy(line_buffer, &msg[cur], until); line_buffer[until] = 0; cur += until + 1; char const *s[5] = { }; for (size_t c = 0, i = 0; i < 5; i++) { size_t pos = Util::next_char(line_buffer, c, '\t'); line_buffer[c+pos] = 0; s[i] = (char const*)&line_buffer[c]; c += pos + 1; } bool const is_wpa1 = Util::string_contains((char const*)s[3], "WPA"); bool const is_wpa2 = Util::string_contains((char const*)s[3], "WPA2"); unsigned signal = Util::approximate_quality(s[2]); char const *prot = is_wpa1 ? "WPA" : "NONE"; prot = is_wpa2 ? "WPA2" : prot; Accesspoint ap(s[0], s[1], prot, s[4], signal); func(ap); } } /* * Wifi driver front end */ struct Wifi::Frontend { /* accesspoint */ Genode::Heap _ap_allocator; using Accesspoint_r = Genode::Registered; Genode::Registry _aps { }; Accesspoint *_lookup_ap_by_ssid(Accesspoint::Ssid const &ssid) { Accesspoint *p = nullptr; _aps.for_each([&] (Accesspoint &ap) { if (ap.valid() && ap.ssid == ssid) { p = ≈ } }); return p; } Accesspoint *_lookup_ap_by_bssid(Accesspoint::Bssid const &bssid) { Accesspoint *p = nullptr; _aps.for_each([&] (Accesspoint &ap) { if (ap.valid() && ap.bssid == bssid) { p = ≈ } }); return p; } Accesspoint *_alloc_ap() { return new (&_ap_allocator) Accesspoint_r(_aps); } void _free_ap(Accesspoint &ap) { Genode::destroy(&_ap_allocator, &ap); } template void _for_each_ap(FUNC const &func) { _aps.for_each([&] (Accesspoint &ap) { func(ap); }); } unsigned _count_to_be_enabled() { unsigned count = 0; auto enable = [&](Accesspoint const &ap) { count += ap.auto_connect; }; _for_each_ap(enable); return count; } unsigned _count_enabled() { unsigned count = 0; auto enabled = [&](Accesspoint const &ap) { count += ap.enabled; }; _for_each_ap(enabled); return count; } unsigned _count_stored() { unsigned count = 0; auto enabled = [&](Accesspoint const &ap) { count += ap.stored(); }; _for_each_ap(enabled); return count; } /* remaining stuff */ Msg_buffer _msg; Genode::Lock _notify_lock { Genode::Lock::UNLOCKED }; void _notify_lock_lock() { _notify_lock.lock(); } void _notify_lock_unlock() { _notify_lock.unlock(); } bool _rfkilled { false }; Genode::Signal_handler _rfkill_handler; void _handle_rfkill() { _rfkilled = wifi_get_rfkill(); /* re-enable scan timer */ if (!_rfkilled) { _scan_timer.sigh(_scan_timer_sigh); _arm_scan_timer(false); } else { _scan_timer.sigh(Genode::Signal_context_capability()); } if (_rfkilled && _state != State::IDLE) { Genode::warning("rfkilled in state ", state_strings(_state)); } } /* config */ Genode::Attached_rom_dataspace _config_rom; Genode::Signal_handler _config_sigh; bool _verbose { false }; bool _verbose_state { false }; bool _use_11n { true }; bool _deferred_config_update { false }; bool _single_autoconnect { false }; unsigned _connected_scan_interval { 30 }; unsigned _scan_interval { 5 }; void _config_update(bool signal) { _config_rom.update(); if (!_config_rom.valid()) { return; } Genode::Xml_node config = _config_rom.xml(); _verbose = config.attribute_value("verbose", _verbose); _verbose_state = config.attribute_value("verbose_state", _verbose_state); /* only evaluated at start-up */ _use_11n = config.attribute_value("use_11n", _use_11n); unsigned connected_scan_interval = Util::check_time(config.attribute_value("connected_scan_interval", _connected_scan_interval), 0, 15*60); unsigned scan_interval = Util::check_time(config.attribute_value("scan_interval", _scan_interval), 5, 15*60); if ( connected_scan_interval > _connected_scan_interval || scan_interval > _scan_interval) { _arm_scan_timer(_connected_ap.bssid_valid()); } _connected_scan_interval = connected_scan_interval; _scan_interval = scan_interval; /* * Always handle rfkill, regardless in which state we are currently in. * When we come back from rfkill, will most certainly will be IDLE anyway. */ if (config.has_attribute("rfkill")) { bool const blocked = config.attribute_value("rfkill", false); wifi_set_rfkill(blocked); /* * In case we get blocked set rfkilled immediately to prevent * any further scanning operation. The actual value will be set * by the singal handler but is not expected to be any different * as the rfkill call is not supposed to fail. */ if (blocked && !_rfkilled) { _rfkilled = true; Genode::Reporter::Xml_generator xml(*_state_reporter, [&] () { xml.node("accesspoint", [&] () { xml.attribute("state", "disconnected"); xml.attribute("rfkilled", _rfkilled); }); }); _connected_ap.invalidate(); } } /* * Block any further config updates until we have finished applying * the current one. */ if (_state != State::IDLE) { Genode::warning("deferring config update (", state_strings(_state), ")"); _deferred_config_update = true; return; } bool single_autoconnect = false; /* update AP list */ auto parse = [&] ( Genode::Xml_node node) { Accesspoint ap; ap.ssid = node.attribute_value("ssid", Accesspoint::Ssid()); ap.bssid = node.attribute_value("bssid", Accesspoint::Bssid()); size_t const ssid_len = ap.ssid.length() - 1; if (ssid_len == 0 || ssid_len > 32) { Genode::warning("ignoring accesspoint with invalid ssid"); return; } Accesspoint *p = _lookup_ap_by_ssid(ap.ssid); if (p) { if (_verbose) { Genode::log("Update: '", p->ssid, "'"); } /* mark for updating */ p->update = true; } else { p = _alloc_ap(); if (!p) { Genode::warning("could not add accesspoint, no slots left"); return; } } ap.pass = node.attribute_value("passphrase", Accesspoint::Pass("")); ap.prot = node.attribute_value("protection", Accesspoint::Prot("NONE")); ap.auto_connect = node.attribute_value("auto_connect", true); ap.explicit_scan = node.attribute_value("explicit_scan", false); if (ap.wpa()) { size_t const psk_len = ap.pass.length() - 1; if (psk_len < 8 || psk_len > 63) { Genode::warning("ignoring accesspoint '", ap.ssid, "' with invalid pass"); return; } } /* check if updating is really necessary */ if (p->update) { p->update = ((ap.bssid.length() > 1 && ap.bssid != p->bssid) || ap.pass != p->pass || ap.prot != p->prot || ap.auto_connect != p->auto_connect); } /* TODO add better way to check validity */ if (ap.bssid.length() == 17 + 1) { p->bssid = ap.bssid; } p->ssid = ap.ssid; p->prot = ap.prot; p->pass = ap.pass; p->auto_connect = ap.auto_connect; p->explicit_scan = ap.explicit_scan; single_autoconnect |= (p->update || p->auto_connect) && !_connected_ap.valid(); }; config.for_each_sub_node("network", parse); /* * To accomodate a management component that only deals * with on network, e.g. the sculpt_manager, generate a * fake connecting event. Either a connected or disconnected * event will bring us to square one. */ if (signal && _count_to_be_enabled() == 1 && single_autoconnect && !_rfkilled) { auto lookup = [&] (Accesspoint const &ap) { if (!ap.auto_connect) { return; } if (_verbose) { Genode::log("Single autoconnect event for '", ap.ssid, "'"); } try { Genode::Reporter::Xml_generator xml(*_state_reporter, [&] () { xml.node("accesspoint", [&] () { xml.attribute("ssid", ap.ssid); xml.attribute("state", "connecting"); }); }); _single_autoconnect = true; } catch (...) { } }; _for_each_ap(lookup); } /* * Marking removes stale APs first and triggers adding of * new ones afterwards. */ _mark_stale_aps(config); } void _handle_config_update() { _config_update(true); } /* state */ Accesspoint *_processed_ap { nullptr }; Accesspoint _connected_ap { }; enum State { IDLE = 0x00, SCAN = 0x01, NETWORK = 0x02, CONNECT = 0x03, STATUS = 0x04, INFO = 0x05, INITIATE_SCAN = 0x00|SCAN, PENDING_RESULTS = 0x10|SCAN, ADD_NETWORK = 0x00|NETWORK, FILL_NETWORK_SSID = 0x10|NETWORK, FILL_NETWORK_BSSID = 0x20|NETWORK, FILL_NETWORK_PSK = 0x30|NETWORK, REMOVE_NETWORK = 0x40|NETWORK, ENABLE_NETWORK = 0x50|NETWORK, DISABLE_NETWORK = 0x60|NETWORK, DISCONNECT_NETWORK = 0x70|NETWORK, LIST_NETWORKS = 0x80|NETWORK, CONNECTING = 0x00|CONNECT, CONNECTED = 0x10|CONNECT, DISCONNECTED = 0x20|CONNECT, }; State _state { State::IDLE }; char const *state_strings(State state) { switch (state) { case IDLE: return "idle"; case INITIATE_SCAN: return "initiate scan"; case PENDING_RESULTS: return "pending results"; case ADD_NETWORK: return "add network"; case FILL_NETWORK_SSID: return "fill network ssid"; case FILL_NETWORK_BSSID: return "fill network bssid"; case FILL_NETWORK_PSK: return "fill network pass"; case REMOVE_NETWORK: return "remove network"; case ENABLE_NETWORK: return "enable network"; case DISABLE_NETWORK: return "disable network"; case CONNECTING: return "connecting"; case CONNECTED: return "connected"; case DISCONNECTED: return "disconnected"; case STATUS: return "status"; case LIST_NETWORKS: return "list networks"; case INFO: return "info"; default: return "unknown"; }; } void _state_transition(State ¤t, State next) { if (_verbose_state) { using namespace Genode; log("Transition: ", state_strings(current), " -> ", state_strings(next)); } current = next; } using Cmd_str = Genode::String; void _submit_cmd(Cmd_str const &str) { Genode::memset(_msg.send, 0, sizeof(_msg.send)); Genode::memcpy(_msg.send, str.string(), str.length()); ++_msg.send_id; wpa_ctrl_set_fd(); /* * We might have to pull the socketcall task out of poll_all() * because otherwise we might be late and wpa_supplicant has * already removed all scan results due to BSS age settings. */ wifi_kick_socketcall(); } /* scan */ Timer::Connection _scan_timer; Genode::Signal_handler _scan_timer_sigh; void _handle_scan_timer() { /* * If we are blocked or currently trying to join a network * suspend scanning. */ if (_rfkilled || _connecting.length() > 1) { if (_verbose) { Genode::log("Suspend scan timer"); } return; } /* scanning was disabled, ignore current request */ if (!_arm_scan_timer(_connected_ap.bssid_valid())) { if (_verbose) { Genode::log("Scanning disabled, ignore current scan request"); } return; } /* skip as we will be scheduled some time soon(tm) anyway */ if (_state != State::IDLE) { if (_verbose) { Genode::log("Not idle, ignore scan request, state: ", Genode::Hex((unsigned)_state)); } return; } /* left one attempt out */ if (_scan_busy) { if (_verbose) { Genode::log("Scan already pending, ignore scan request"); } _scan_busy = false; return; } enum { SSID_ARG_LEN = 6 + 64, /* " ssid " + "a5a5a5a5..." */ }; /* send buffer - 'SCAN ' + stuff */ char ssid_buffer[sizeof(Msg_buffer::send)-16] = { }; size_t buffer_pos = 0; auto valid_ssid = [&] (Accesspoint const &ap) { if (buffer_pos + SSID_ARG_LEN >= sizeof(ssid_buffer)) { return; } if (!ap.explicit_scan) { return; } char ssid_hex[64+1] = { }; char const *ssid = ap.ssid.string(); for (size_t i = 0; i < ap.ssid.length() - 1; i++) { Util::byte2hex((ssid_hex + i * 2), ssid[i]); } Genode::String tmp(" ssid ", (char const*)ssid_hex); size_t const tmp_len = tmp.length() - 1; Genode::memcpy((ssid_buffer + buffer_pos), tmp.string(), tmp_len); buffer_pos += tmp_len; }; _for_each_ap(valid_ssid); _state_transition(_state, State::INITIATE_SCAN); _submit_cmd(Cmd_str("SCAN", (char const*)ssid_buffer)); } bool _arm_scan_timer(bool connected) { unsigned const sec = connected ? _connected_scan_interval : _scan_interval; if (!sec) { return false; } if (_verbose) { Genode::log("Arm ", connected ? "connected " : "", "scan: ", sec, " sec"); } _scan_timer.trigger_once(sec * (1000 * 1000)); return true; } Genode::Constructible _ap_reporter; void _generate_scan_results_report(char const *msg) { unsigned count_lines = 0; for_each_line(msg, [&] (char const*) { count_lines++; }); if (!count_lines) { if (_verbose) { Genode::log("Scan results empty"); } return; } bool connecting_attempt = false; try { _ap_reporter->generate([&] (Genode::Xml_generator &xml) { for_each_result_line(msg, [&] (Accesspoint const &ap) { /* ignore potentially empty ssids */ if (ap.ssid == "") { return; } xml.node("accesspoint", [&]() { xml.attribute("ssid", ap.ssid); xml.attribute("bssid", ap.bssid); xml.attribute("freq", ap.freq); xml.attribute("quality", ap.signal); if (ap.wpa()) { xml.attribute("protection", ap.prot); } }); auto check_existence = [&] (Accesspoint &lap) { connecting_attempt |= (lap.ssid == ap.ssid) && ap.auto_connect; }; _for_each_ap(check_existence); }); }); } catch (...) { /* silently omit report */ } try { if (!_connected_ap.bssid_valid() && connecting_attempt) { Genode::Reporter::Xml_generator xml(*_state_reporter, [&] () { xml.node("accesspoint", [&] () { xml.attribute("state", "connecting"); }); }); } } catch (...) { /* silently omit state */ } } /* network commands */ void _mark_stale_aps(Genode::Xml_node const &config) { auto mark_stale = [&] (Accesspoint &ap) { ap.stale = true; config.for_each_sub_node("network", [&] ( Genode::Xml_node node) { Accesspoint::Ssid ssid = node.attribute_value("ssid", Accesspoint::Ssid("")); if (ap.ssid == ssid) { ap.stale = false; } }); }; _for_each_ap(mark_stale); _remove_stale_aps(); } void _remove_stale_aps() { if (_state != State::IDLE) { Genode::warning("cannot remove stale APs in non-idle state " "(", state_strings(_state), ")"); return; } if (_processed_ap) { return; } _aps.for_each([&] (Accesspoint &ap) { if (!_processed_ap && ap.valid() && ap.stale) { _processed_ap = ≈ } }); if (!_processed_ap) { /* TODO move State transition somewhere more sane */ _state_transition(_state, State::IDLE); _add_new_aps(); return; } if (_verbose) { Genode::log("Remove network: '", _processed_ap->ssid, "'"); } _state_transition(_state, State::REMOVE_NETWORK); _submit_cmd(Cmd_str("REMOVE_NETWORK ", _processed_ap->id)); } void _update_aps() { if (_state != State::IDLE) { Genode::warning("cannot enable network in non-idle state"); return; } if (_processed_ap) { return; } _aps.for_each([&] (Accesspoint &ap) { if (!_processed_ap && ap.stored() && ap.update) { _processed_ap = ≈ } }); if (!_processed_ap) { return; } if (_verbose) { Genode::log("Update network: '", _processed_ap->ssid, "'"); } /* re-use state to change PSK */ _state_transition(_state, State::FILL_NETWORK_PSK); _network_set_psk(); } void _add_new_aps() { if (_state != State::IDLE) { Genode::warning("cannot enable network in non-idle state"); return; } if (_processed_ap) { return; } _aps.for_each([&] (Accesspoint &ap) { if (!_processed_ap && ap.valid() && !ap.stored()) { _processed_ap = ≈ } }); if (!_processed_ap) { /* XXX move State transition somewhere more sane */ _state_transition(_state, State::IDLE); _update_aps(); return; } if (_verbose) { Genode::log("Add network: '", _processed_ap->ssid, "'"); } _state_transition(_state, State::ADD_NETWORK); _submit_cmd(Cmd_str("ADD_NETWORK")); } void _network_enable() { if (_state != State::IDLE) { Genode::warning("cannot enable network in non-idle state"); return; } if (_processed_ap) { return; } _aps.for_each([&] (Accesspoint &ap) { if ( !_processed_ap && ap.valid() && !ap.enabled && ap.auto_connect) { _processed_ap = ≈ } }); if (!_processed_ap) { return; } if (_verbose) { Genode::log("Enable network: '", _processed_ap->ssid, "'"); } _state_transition(_state, State::ENABLE_NETWORK); _submit_cmd(Cmd_str("ENABLE_NETWORK ", _processed_ap->id)); } void _network_disable() { if (_state != State::IDLE) { Genode::warning("cannot enable network in non-idle state"); return; } if (_processed_ap) { return; } _aps.for_each([&] (Accesspoint &ap) { if ( !_processed_ap && ap.valid() && ap.enabled && ap.auto_connect) { _processed_ap = ≈ } }); if (!_processed_ap) { /* XXX move State transition somewhere more sane */ _state_transition(_state, State::IDLE); _add_new_aps(); return; } if (_verbose) { Genode::log("Disable network: '", _processed_ap->ssid, "'"); } _state_transition(_state, State::DISABLE_NETWORK); _submit_cmd(Cmd_str("DISABLE_NETWORK ", _processed_ap->id)); } void _network_disconnect() { _state_transition(_state, State::DISCONNECT_NETWORK); _submit_cmd(Cmd_str("DISCONNECT")); } void _network_set_ssid(char const *msg) { long id = -1; Genode::ascii_to(msg, id); _processed_ap->id = id; _submit_cmd(Cmd_str("SET_NETWORK ", _processed_ap->id, " ssid \"", _processed_ap->ssid, "\"")); } void _network_set_bssid() { bool const valid = _processed_ap->bssid.length() == 17 + 1; char const *bssid = valid ? _processed_ap->bssid.string() : ""; _submit_cmd(Cmd_str("SET_NETWORK ", _processed_ap->id, " bssid \"", bssid, "\"")); } void _network_set_psk() { if (_processed_ap->wpa()) { _submit_cmd(Cmd_str("SET_NETWORK ", _processed_ap->id, " psk \"", _processed_ap->pass, "\"")); } else { _submit_cmd(Cmd_str("SET_NETWORK ", _processed_ap->id, " key_mgmt NONE")); } } /* result handling */ bool _scan_busy { false }; void _handle_scan_results(State state, char const *msg) { switch (state) { case State::INITIATE_SCAN: if (!cmd_successful(msg)) { _scan_busy = Genode::strcmp(msg, "FAIL-BUSY"); if (!_scan_busy) { Genode::warning("could not initiate scan: ", msg); } } _state_transition(_state, State::IDLE); break; case State::PENDING_RESULTS: if (scan_results(msg)) { _state_transition(_state, State::IDLE); _generate_scan_results_report(msg); } break; default: Genode::warning("unknown SCAN state: ", msg); break; } } void _handle_network_results(State state, char const *msg) { bool successfully = false; switch (state) { case State::ADD_NETWORK: if (cmd_fail(msg)) { Genode::error("could not add network: ", msg); _state_transition(_state, State::IDLE); } else { _state_transition(_state, State::FILL_NETWORK_SSID); _network_set_ssid(msg); successfully = true; } break; case State::REMOVE_NETWORK: _state_transition(_state, State::IDLE); if (cmd_fail(msg)) { Genode::error("could not remove network: ", msg); } else { _free_ap(*_processed_ap); /* trigger the next round */ _processed_ap = nullptr; _remove_stale_aps(); successfully = true; } break; case State::FILL_NETWORK_SSID: _state_transition(_state, State::IDLE); if (!cmd_successful(msg)) { Genode::error("could not set ssid for network: ", msg); _state_transition(_state, State::IDLE); } else { _state_transition(_state, State::FILL_NETWORK_BSSID); _network_set_bssid(); successfully = true; } break; case State::FILL_NETWORK_BSSID: _state_transition(_state, State::IDLE); if (!cmd_successful(msg)) { Genode::error("could not set bssid for network: ", msg); _state_transition(_state, State::IDLE); } else { _state_transition(_state, State::FILL_NETWORK_PSK); _network_set_psk(); successfully = true; } break; case State::FILL_NETWORK_PSK: _state_transition(_state, State::IDLE); if (!cmd_successful(msg)) { Genode::error("could not set passphrase for network: ", msg); } else { /* * Disable network to trick wpa_supplicant into reloading * the settings. */ if (_processed_ap->update) { _processed_ap->enabled = false; _state_transition(_state, State::DISABLE_NETWORK); _submit_cmd(Cmd_str("DISABLE_NETWORK ", _processed_ap->id)); } else if (_processed_ap->auto_connect) { _state_transition(_state, State::ENABLE_NETWORK); _submit_cmd(Cmd_str("ENABLE_NETWORK ", _processed_ap->id)); } else { /* trigger the next round */ _processed_ap = nullptr; _add_new_aps(); } successfully = true; } break; case State::ENABLE_NETWORK: _state_transition(_state, State::IDLE); if (!cmd_successful(msg)) { Genode::error("could not enable network: ", msg); } else { _processed_ap->enabled = true; /* trigger the next round */ _processed_ap = nullptr; _add_new_aps(); successfully = true; } break; case State::DISABLE_NETWORK: _state_transition(_state, State::IDLE); if (!cmd_successful(msg)) { Genode::error("could not disable network: ", msg); } else { /* * Updated settings are applied, enable the network * anew an try again. */ if (_processed_ap->update) { _processed_ap->update = false; if (_processed_ap->auto_connect) { _state_transition(_state, State::ENABLE_NETWORK); _submit_cmd(Cmd_str("ENABLE_NETWORK ", _processed_ap->id)); } } else { _processed_ap->enabled = false; /* trigger the next round */ _processed_ap = nullptr; _network_disable(); } successfully = true; } break; case State::DISCONNECT_NETWORK: _state_transition(_state, State::IDLE); if (!cmd_successful(msg)) { Genode::error("could not disconnect from network: ", msg); } else { _network_disable(); successfully = true; } break; case State::LIST_NETWORKS: _state_transition(_state, State::IDLE); if (list_network_results(msg)) { Genode::error("List networks:\n", msg); } break; default: Genode::warning("unknown network state: ", msg); break; } /* * If some step failed we have to generate a fake * disconnect event. */ if (_single_autoconnect && !successfully) { try { Genode::Reporter::Xml_generator xml(*_state_reporter, [&] () { xml.node("accesspoint", [&] () { xml.attribute("state", "disconnected"); xml.attribute("rfkilled", _rfkilled); xml.attribute("config_error", true); }); }); _single_autoconnect = false; } catch (...) { } } } void _handle_status_result(State state, char const *msg) { _state_transition(_state, State::IDLE); /* * Querying the status might have failed but we already sent * out a rudimentary report, just stop here. */ if (0 == msg[0]) { return; } Accesspoint ap { }; auto fill_ap = [&] (char const *line) { if (Genode::strcmp(line, "ssid=", 5) == 0) { ap.ssid = Accesspoint::Ssid(line+5); } else if (Genode::strcmp(line, "bssid=", 6) == 0) { ap.bssid = Accesspoint::Bssid(line+6); } else if (Genode::strcmp(line, "freq=", 5) == 0) { ap.freq = Accesspoint::Freq(line+5); } }; for_each_line(msg, fill_ap); if (!ap.ssid.valid()) { Genode::error("Cannot query SSID :-("); return; } Accesspoint *p = _lookup_ap_by_ssid(ap.ssid); if (p) { p->bssid = ap.bssid; p->freq = ap.freq; } _connected_ap.ssid = ap.ssid; Genode::Reporter::Xml_generator xml(*_state_reporter, [&] () { xml.node("accesspoint", [&] () { xml.attribute("ssid", ap.ssid); xml.attribute("bssid", ap.bssid); xml.attribute("freq", ap.freq); xml.attribute("state", "connected"); }); }); } void _handle_info_result(State state, char const *msg) { _state_transition(_state, State::IDLE); if (!_connected_event && !_disconnected_event) { return; } /* * It might happen that the supplicant already flushed * its internal BSS information and cannot help us out. * Since we already sent out a rudimentary report, just * stop here. */ if (0 == msg[0]) { return; } Accesspoint ap { }; auto fill_ap = [&] (char const *line) { if (Genode::strcmp(line, "ssid=", 5) == 0) { ap.ssid = Accesspoint::Ssid(line+5); } else if (Genode::strcmp(line, "bssid=", 6) == 0) { ap.bssid = Accesspoint::Bssid(line+6); } else if (Genode::strcmp(line, "freq=", 5) == 0) { ap.freq = Accesspoint::Freq(line+5); } }; for_each_line(msg, fill_ap); /* * When the config is changed while we are still connecting and * for some reasons the accesspoint does not get disabled * a connected event could arrive and we will get a nullptr... */ Accesspoint *p = _lookup_ap_by_ssid(ap.ssid); /* * ... but we still generate a report and let the management * component deal with it. */ Genode::Reporter::Xml_generator xml(*_state_reporter, [&] () { xml.node("accesspoint", [&] () { xml.attribute("ssid", ap.ssid); xml.attribute("bssid", ap.bssid); xml.attribute("freq", ap.freq); xml.attribute("state", _connected_event ? "connected" : "disconnected"); if (!_connected_event) { xml.attribute("rfkilled", _rfkilled); xml.attribute("auth_failure", _disconnected_fail); } }); }); if (_disconnected_fail) { /* * Being able to remove a failed network from the internal * state of the supplicant relies on a sucessful BSS request. * In case that failes the supplicant will try to join the * network again and again... */ if (!p || _processed_ap) { Genode::error("cannot disabled failed network"); } else { _processed_ap = p; _state_transition(_state, State::DISABLE_NETWORK); _submit_cmd(Cmd_str("DISABLE_NETWORK ", p->id)); } } else if (_connected_event) { /* * In case the BSS cmd did not return a valid SSID, which * was observed only with hidden networks so far, check the * current status. */ if (!p) { _state_transition(_state, State::STATUS); _submit_cmd(Cmd_str("STATUS")); } else { p->bssid = ap.bssid; p->freq = ap.freq; } _connected_ap = ap; } } /* connection state */ Genode::Constructible _state_reporter { }; Accesspoint::Bssid _connecting { }; Accesspoint::Bssid const _extract_bssid(char const *msg, State state) { char bssid[32] = { }; /* by the power of wc -c, I have the start pos... */ enum { BSSID_CONNECT = 37, BSSID_DISCONNECT = 30, BSSID_CONNECTING = 33, }; bool const connected = state == State::CONNECTED; bool const connecting = state == State::CONNECTING; size_t const len = 17; size_t const start = connected ? BSSID_CONNECT : connecting ? BSSID_CONNECTING : BSSID_DISCONNECT; Genode::memcpy(bssid, msg + start, len); return Accesspoint::Bssid((char const*)bssid); } bool _auth_failure(char const *msg) { enum { REASON_OFFSET = 55, }; unsigned reason = 0; Genode::ascii_to((msg + REASON_OFFSET), reason); switch (reason) { case 2: /* prev auth no longer valid */ case 15: /* 4-way handshake timeout/failed */ return true; default: return false; } } /* events */ bool _connected_event { false }; bool _disconnected_event { false }; bool _disconnected_fail { false }; enum { MAX_ATTEMPTS = 3, }; unsigned _scan_attempts { 0 }; Accesspoint::Bssid _pending_bssid { }; void _handle_connection_events(char const *msg) { bool const connected = check_recv_msg(msg, recv_table[Rmi::CONNECTED]); bool const disconnected = check_recv_msg(msg, recv_table[Rmi::DISCONNECTED]); bool const auth_failed = disconnected && _auth_failure(msg); State state = connected ? State::CONNECTED : State::DISCONNECTED; Accesspoint::Bssid const &bssid = _extract_bssid(msg, state); /* * Always reset the "global" connection state first */ _connected_ap.invalidate(); if (connected) { _connected_ap.bssid = bssid; } if (connected || disconnected) { _connecting = Accesspoint::Bssid(); } /* * Save local connection state here for later re-use when * the BSS information are handled. */ _connected_event = connected; _disconnected_event = disconnected; _disconnected_fail = auth_failed; if (!_rfkilled) { /* * As we only received the BSSID, try to gather more information * so we may generate a more thorough follow-up state report. */ if (_state != State::IDLE) { _pending_bssid = bssid; } else { _state_transition(_state, State::INFO); _submit_cmd(Cmd_str("BSS ", bssid)); } _arm_scan_timer(connected); } /* * Generate the first rudimentary report whose missing information * are (potentially) filled in later (see above). */ Genode::Reporter::Xml_generator xml(*_state_reporter, [&] () { xml.node("accesspoint", [&] () { xml.attribute("bssid", bssid); xml.attribute("state", connected ? "connected" : "disconnected"); if (disconnected) { xml.attribute("rfkilled", _rfkilled); if (auth_failed) { xml.attribute("auth_failure", auth_failed); } } }); }); /* reset */ _single_autoconnect = false; } Genode::Signal_handler _events_handler; unsigned _last_event_id { 0 }; void _handle_events() { char const *msg = reinterpret_cast(_msg.event); unsigned const event_id = _msg.event_id; /* return early */ if (_last_event_id == event_id) { _notify_lock_unlock(); return; } if (results_available(msg)) { /* * We might have to pull the socketcall task out of poll_all() * because otherwise we might be late and wpa_supplicant has * already removed all scan results due to BSS age settings. */ wifi_kick_socketcall(); if (_state == State::IDLE) { _state_transition(_state, State::PENDING_RESULTS); _submit_cmd(Cmd_str("SCAN_RESULTS")); } } else if (connecting_to_network(msg)) { if (!_single_autoconnect) { Accesspoint::Bssid const &bssid = _extract_bssid(msg, State::CONNECTING); _connecting = bssid; Genode::Reporter::Xml_generator xml(*_state_reporter, [&] () { xml.node("accesspoint", [&] () { xml.attribute("bssid", bssid); xml.attribute("state", "connecting"); }); }); } } else if (network_not_found(msg)) { /* always try to update the accesspoint list */ if (_state == State::IDLE) { _state_transition(_state, State::PENDING_RESULTS); _submit_cmd(Cmd_str("SCAN_RESULTS")); } if (_single_autoconnect && ++_scan_attempts >= MAX_ATTEMPTS) { _scan_attempts = 0; _single_autoconnect = false; Genode::Reporter::Xml_generator xml(*_state_reporter, [&] () { xml.node("accesspoint", [&] () { xml.attribute("state", "disconnected"); xml.attribute("rfkilled", _rfkilled); xml.attribute("not_found", true); }); }); } } else { _handle_connection_events(msg); } _notify_lock_unlock(); } Genode::Signal_handler _cmd_handler; unsigned _last_recv_id { 0 }; void _handle_cmds() { char const *msg = reinterpret_cast(_msg.recv); unsigned const recv_id = _msg.recv_id; /* return early */ if (_last_recv_id == recv_id) { _notify_lock_unlock(); return; } _last_recv_id = recv_id; switch (_state & 0xf) { case State::SCAN: _handle_scan_results(_state, msg); break; case State::NETWORK: _handle_network_results(_state, msg); break; case State::STATUS: _handle_status_result(_state, msg); break; case State::INFO: _handle_info_result(_state, msg); break; case State::IDLE: default: break; } _notify_lock_unlock(); if (_verbose_state) { Genode::log("State:", " connected: ", _connected_ap.bssid_valid(), " connecting: ", _connecting.length() > 1, " enabled: ", _count_enabled(), " stored: ", _count_stored(), ""); } if (_state == State::IDLE && _deferred_config_update) { _deferred_config_update = false; _handle_config_update(); } if (_state == State::IDLE && _pending_bssid.length() > 1) { _state_transition(_state, State::INFO); _submit_cmd(Cmd_str("BSS ", _pending_bssid)); _pending_bssid = Accesspoint::Bssid(); } } /** * Constructor */ Frontend(Genode::Env &env) : _ap_allocator(env.ram(), env.rm()), _rfkill_handler(env.ep(), *this, &Wifi::Frontend::_handle_rfkill), _config_rom(env, "wifi_config"), _config_sigh(env.ep(), *this, &Wifi::Frontend::_handle_config_update), _scan_timer(env), _scan_timer_sigh(env.ep(), *this, &Wifi::Frontend::_handle_scan_timer), _events_handler(env.ep(), *this, &Wifi::Frontend::_handle_events), _cmd_handler(env.ep(), *this, &Wifi::Frontend::_handle_cmds) { _config_rom.sigh(_config_sigh); _scan_timer.sigh(_scan_timer_sigh); try { _ap_reporter.construct(env, "accesspoints", "accesspoints"); _ap_reporter->generate([&] (Genode::Xml_generator &xml) { }); } catch (...) { Genode::warning("no Report session available, scan results will " "not be reported"); } try { _state_reporter.construct(env, "state"); _state_reporter->enabled(true); Genode::Reporter::Xml_generator xml(*_state_reporter, [&] () { xml.node("accesspoint", [&] () { xml.attribute("state", "disconnected"); xml.attribute("rfkilled", _rfkilled); }); }); } catch (...) { Genode::warning("no Report session available, connectivity will " "not be reported"); } /* read in list of APs */ _config_update(false); /* kick-off initial scanning */ _handle_scan_timer(); } /** * Return if 11n operation is enabled */ bool use_11n() const { return _use_11n; } /** * Get RFKILL signal capability * * Used by the wifi_drv to notify front end. */ Genode::Signal_context_capability rfkill_sigh() { return _rfkill_handler; } /** * Get result signal capability * * Used by the wpa_supplicant to notify front end after processing * a command. */ Genode::Signal_context_capability result_sigh() { return _cmd_handler; } /** * Get event signal capability * * Used by the wpa_supplicant to notify front whenever a event * was triggered. */ Genode::Signal_context_capability event_sigh() { return _events_handler; } /** * Block until events were handled by the front end * * Used by the wpa_supplicant to wait for the front end. */ void block_for_processing() { _notify_lock_lock(); } /** * Return shared memory message buffer * * Used for communication between front end and wpa_supplicant. */ Msg_buffer &msg_buffer() { return _msg; } }; #endif /* _WIFI_FRONTEND_H_ */