Wifi(Android): fixes errors when location permission is not granted

Security detection support is also added
This commit is contained in:
李通洲
2026-09-20 23:49:39 +08:00
parent 39564acde4
commit 0b76d33f07
2 changed files with 356 additions and 67 deletions
+6 -2
View File
@@ -52,9 +52,13 @@ Features:
* Running as `adb shell` or as root, `dumpsys display` is used as well, which is what covers Android 12 and older. * Running as `adb shell` or as root, `dumpsys display` is used as well, which is what covers Android 12 and older.
* The refresh rate is now the rate of the active display mode, and the HDR capability is read from the display itself, for every display rather than only for the built-in one. * The refresh rate is now the rate of the active display mode, and the HDR capability is read from the display itself, for every display rather than only for the built-in one.
* Improved WiFi detection on Android (Wifi, Android) * Improved WiFi detection on Android (Wifi, Android)
* The connection details are now read from the WiFi service over `/dev/binder` instead of `termux-api WifiConnectionInfo`, so no subprocess is spawned. The Termux:API app is still required, though: it is what carries the WiFi permission that the Termux app does not request itself, and uninstalling it turns this module off. * The connection details are now read from the WiFi service over `/dev/binder` instead of `termux-api WifiConnectionInfo`, so no subprocess is spawned.
* The interface name and its state, the connection state and the Wi-Fi standard are now reported as well. * The Termux:API app is still required, though. It is what carries the WiFi permission that the Termux app does not request itself, so uninstalling it turns this module off.
* A location permission is required as well, and Termux:API is what carries that one too. The WiFi service withholds the SSID and the BSSID from an app that does not hold one, and hands both over once it is granted; until then the signal, the rates, the frequency, the Wi-Fi standard and the security type are reported as usual, and the two names are reported as `<redacted>`. The grant has to be for all the time rather than only while the app is in use, as the service withholds the two names from an app that is not in the foreground either -- which is the state a run over `ssh` is in.
* The interface name and its state, the connection state, the Wi-Fi standard and the security type are now reported as well.
* The security type is reported from Android 12 on. The reply Android 11 sends ends before that field, so the `security` field stays empty there.
* This needs Android 11 (API 30), the release that moved the Wi-Fi framework into an APEX. There is no fallback, so Android 10 and older report an error instead. * This needs Android 11 (API 30), the release that moved the Wi-Fi framework into an APEX. There is no fallback, so Android 10 and older report an error instead.
* WiFi detection on Android is experimental. The reply the WiFi service hands an app drifts between Android releases and between vendors, and what it contains also depends on the permissions the calling app holds, so a device can report less than its connection has. Please report anything that looks wrong or missing.
* Improved COSMIC detection (DE / WM, Linux) * Improved COSMIC detection (DE / WM, Linux)
* The version is now read from the `COSMIC_VERSION` environment variable when it is set. * The version is now read from the `COSMIC_VERSION` environment variable when it is set.
* Improved accuracy and performance of process name detection in the Top module. (Top, macOS) * Improved accuracy and performance of process name detection in the Top module. (Top, macOS)
+350 -65
View File
@@ -51,6 +51,27 @@
// no route left in this file at all, and the error the caller sees is raised by the first call, before // no route left in this file at all, and the error the caller sees is raised by the first call, before
// the second one could have answered anything. // the second one could have answered anything.
// //
// Holding the Wi-Fi permission is not the same as being told everything, though, and a reply that
// comes back without the SSID is not a reply this module fails on. The service hands out a copy with
// the fields the caller may not see removed -- `WifiInfo.makeCopy` drops the SSID, writes
// `02:00:00:00:00:00` in place of the BSSID and the MAC address, and rewrites the network id to -1,
// while the signal, the rates, the frequency, the IPv4 address and the supplicant state are carried
// over untouched. It makes that copy for every caller that `enforceCanAccessScanResults` refuses,
// which was measured here as a caller whose location permission had lapsed, next to the shell, which
// holds one and reads the SSID. Nothing else about the reply moves, so the head is understood the
// same way and everything the reply still says is reported: a connection that cannot be named is
// still a connection, and the name is the only thing missing. The two names are reported as
// `<redacted>` -- the same word the macOS backend uses for the same situation -- rather than as
// nothing, because an empty SSID is what the module prints the interface state in place of, and the
// reply did carry a signal, a channel and a rate worth showing. The placeholders the service writes
// in their place are never reported as values. See parseConnectionInfo().
//
// The network id goes with them, and that one matters to this file: -1 is what it reads as "there is
// no connection", so a redacted reply would otherwise be reported as a disconnected one. What
// separates the two is the frequency -- a `WifiInfo` with no connection has none to write, so a
// frequency that passes the shape checks below is a connection -- and the supplicant state, which is
// not redacted and answers directly when it is there.
//
// The transaction code is read out of the device's own jar rather than carried as a table, so the // The transaction code is read out of the device's own jar rather than carried as a table, so the
// module is not tied to a list of releases. It does need the jar to exist, and Android 11 is the // module is not tied to a list of releases. It does need the jar to exist, and Android 11 is the
// release that moved the Wi-Fi framework into an APEX -- before that the class sat in framework.jar, // release that moved the Wi-Fi framework into an APEX -- before that the class sat in framework.jar,
@@ -93,16 +114,21 @@
// //
// What is deliberately left empty: // What is deliberately left empty:
// //
// * `security`. `WifiInfo` carries no security type on Android 11 at all, and on Android 16 the // * `security` on a release whose parcel carries no such field. Android 11 ends its parcel at the
// value the shell command prints is not in the parcel either -- `cmd wifi status` shows it only // passpoint id, so there is nothing to read and nothing is reported; Android 13 and later write
// because it reads the object in system_server. No other call an app can make returns it, so // one, and it is read. Where it sits is behind the information element list, which is
// the field stays empty rather than being guessed. // variable-length and changes with the access point -- 27 elements on the Android 16 device --
// so reaching it means walking that list in full. The walk is exact and its result is checked
// before it is believed. See findSecurityType().
// * The IPv4 address. It is in the parcel, but FFWifiConnection has no field for it and // * The IPv4 address. It is in the parcel, but FFWifiConnection has no field for it and
// `inf.description` is the interface name, as on every other platform. The LocalIP module // `inf.description` is the interface name, as on every other platform. The LocalIP module
// reports addresses. // reports addresses.
// * `inf.description` while the interface is not in the address list. Its name is not knowable // * `inf.description` while the interface is not in the address list. Its name is not knowable
// from anywhere else -- sysfs and /proc/net are EACCES -- so the field stays empty rather than // from anywhere else -- sysfs and /proc/net are EACCES -- so the field stays empty rather than
// naming an interface that was guessed at. `inf.status` is still reported, from the radio. // naming an interface that was guessed at. `inf.status` is still reported, from the radio.
// * `conn.ssid` and `conn.bssid` on a connection whose names the service withheld are reported as
// `<redacted>` instead, which is not empty: see the note above. They are left empty when there
// is no connection at all, where an absent name is the truth rather than a redaction.
#define FF_WIFI_ANDROID_SERVICE "wifi" #define FF_WIFI_ANDROID_SERVICE "wifi"
#define FF_WIFI_ANDROID_DESCRIPTOR "android.net.wifi.IWifiManager" #define FF_WIFI_ANDROID_DESCRIPTOR "android.net.wifi.IWifiManager"
@@ -118,8 +144,14 @@
// the two states are told apart by asking the service directly. The method takes no arguments and // the two states are told apart by asking the service directly. The method takes no arguments and
// answers with a `WifiManager.WIFI_STATE_*`. // answers with a `WifiManager.WIFI_STATE_*`.
#define FF_WIFI_ANDROID_GET_WIFI_ENABLED_STATE "TRANSACTION_getWifiEnabledState" #define FF_WIFI_ANDROID_GET_WIFI_ENABLED_STATE "TRANSACTION_getWifiEnabledState"
#define FF_WIFI_ANDROID_WIFI_STATE_DISABLED 1
#define FF_WIFI_ANDROID_WIFI_STATE_UNKNOWN 4 // `WifiManager.WIFI_STATE_*`. Only two of the five are acted on: everything else -- the enabled
// state and the two transitional ones -- means the radio is on.
typedef enum FFWifiAndroidRadioState : int32_t
{
FF_WIFI_ANDROID_WIFI_STATE_DISABLED = 1,
FF_WIFI_ANDROID_WIFI_STATE_UNKNOWN = 4,
} FFWifiAndroidRadioState;
// The other argument is the caller's package name. The shell UID owns exactly one package and the // The other argument is the caller's package name. The shell UID owns exactly one package and the
// service accepts that name, which is what `cmd` and `dumpsys` pass. The UID itself is // service accepts that name, which is what `cmd` and `dumpsys` pass. The UID itself is
@@ -172,19 +204,77 @@
#define FF_WIFI_ANDROID_SSID_LEGACY 0x0c // Android 11 and older #define FF_WIFI_ANDROID_SSID_LEGACY 0x0c // Android 11 and older
#define FF_WIFI_ANDROID_SSID_MAX_LENGTH 32 #define FF_WIFI_ANDROID_SSID_MAX_LENGTH 32
// `WifiInfo.DEFAULT_MAC_ADDRESS`: what the service writes in place of the BSSID and of the MAC
// address when the caller may not be told them, and also what it writes when it has neither to give.
// Neither reading is an address, so it is never reported as one -- the field stays empty instead.
#define FF_WIFI_ANDROID_DEFAULT_MAC_ADDRESS "02:00:00:00:00:00"
// A BSSID is written as a string of `hh:hh:hh:hh:hh:hh`, so it is this many characters long -- the
// placeholder above included. Its length word is what stands in for the SSID octets as the check
// that a reply carrying no SSID is still laid out the way this file reads it.
#define FF_WIFI_ANDROID_MAC_STRING_LENGTH 17
// What the two names are reported as when the service withheld them. The macOS backend spells the
// same situation with the same word, and it is a word rather than an empty string so that the module
// keeps printing the signal and the rate it did get instead of the interface state.
#define FF_WIFI_ANDROID_REDACTED "<redacted>"
// `WifiInfo` writes the Wi-Fi standard and then the two maximum-supported link speeds it can reach,
// so the standard is the first word of a run of three. What follows the run is the passpoint id.
#define FF_WIFI_ANDROID_STANDARD_RUN 12
// The longest a string16 this file is willing to step over can be. Only the passpoint id and the
// network key are stepped over by length, and neither is ever long; the bound is what keeps a
// misread length from walking the parser off the end of the reply.
#define FF_WIFI_ANDROID_STRING_MAX_LENGTH 64
// The bounds the information element list is walked under. A reply carries tens of elements, each
// tens of bytes, so these are far above anything real and only ever catch a misread.
#define FF_WIFI_ANDROID_IE_MAX_COUNT 256
#define FF_WIFI_ANDROID_IE_MAX_LENGTH 1024
// `WifiInfo.SecurityType`, which is what `writeToParcel` writes -- and NOT the same numbering as
// `WifiConfiguration.SecurityType`, which the two only share up to SAE. `Unknown` is what the field
// holds before the service sets it, and is reported as no security rather than as an open network.
typedef enum FFWifiAndroidSecurity : int32_t
{
FF_WIFI_ANDROID_SECURITY_UNKNOWN = -1,
FF_WIFI_ANDROID_SECURITY_OPEN = 0,
FF_WIFI_ANDROID_SECURITY_WEP = 1,
FF_WIFI_ANDROID_SECURITY_PSK = 2,
FF_WIFI_ANDROID_SECURITY_EAP = 3,
FF_WIFI_ANDROID_SECURITY_SAE = 4,
FF_WIFI_ANDROID_SECURITY_EAP_WPA3_ENTERPRISE_192_BIT = 5,
FF_WIFI_ANDROID_SECURITY_OWE = 6,
FF_WIFI_ANDROID_SECURITY_WAPI_PSK = 7,
FF_WIFI_ANDROID_SECURITY_WAPI_CERT = 8,
FF_WIFI_ANDROID_SECURITY_EAP_WPA3_ENTERPRISE = 9,
FF_WIFI_ANDROID_SECURITY_OSEN = 10,
FF_WIFI_ANDROID_SECURITY_PASSPOINT_R1_R2 = 11,
FF_WIFI_ANDROID_SECURITY_PASSPOINT_R3 = 12,
FF_WIFI_ANDROID_SECURITY_DPP = 13,
// The largest value this file knows of, which is what bounds the read below. AOSP has no such
// constant; a release that adds a security type past DPP is refused rather than reported.
FF_WIFI_ANDROID_SECURITY_MAX = FF_WIFI_ANDROID_SECURITY_DPP,
} FFWifiAndroidSecurity;
// `WifiInfo` writes the Wi-Fi standard as one of the `ScanResult.WIFI_STANDARD_*` values. It is // `WifiInfo` writes the Wi-Fi standard as one of the `ScanResult.WIFI_STANDARD_*` values. It is
// followed by the two maximum-supported link speeds, which is what makes it recognisable: an int // followed by the two maximum-supported link speeds, which is what makes it recognisable: an int
// naming a standard, then two plausible rates. The band is checked against it as well, so a value // naming a standard, then two plausible rates. The band is checked against it as well, so a value
// that only fits another band is rejected. A layout that is not recognised leaves `protocol` empty // that only fits another band is rejected. A layout that is not recognised leaves `protocol` empty
// instead of reporting the wrong standard. // instead of reporting the wrong standard.
#define FF_WIFI_ANDROID_STANDARD_LEGACY 1 typedef enum FFWifiAndroidStandard : int32_t
#define FF_WIFI_ANDROID_STANDARD_11A 2 {
#define FF_WIFI_ANDROID_STANDARD_11B 3 FF_WIFI_ANDROID_STANDARD_LEGACY = 1,
#define FF_WIFI_ANDROID_STANDARD_11N 4 FF_WIFI_ANDROID_STANDARD_11A = 2,
#define FF_WIFI_ANDROID_STANDARD_11AC 5 FF_WIFI_ANDROID_STANDARD_11B = 3,
#define FF_WIFI_ANDROID_STANDARD_11AX 6 FF_WIFI_ANDROID_STANDARD_11N = 4,
#define FF_WIFI_ANDROID_STANDARD_11AD 7 FF_WIFI_ANDROID_STANDARD_11AC = 5,
#define FF_WIFI_ANDROID_STANDARD_11BE 8 FF_WIFI_ANDROID_STANDARD_11AX = 6,
FF_WIFI_ANDROID_STANDARD_11AD = 7,
FF_WIFI_ANDROID_STANDARD_11BE = 8,
} FFWifiAndroidStandard;
#define FF_WIFI_ANDROID_RATE_MAX 20000 #define FF_WIFI_ANDROID_RATE_MAX 20000
// The whole WifiInfo parcel measured 1340 bytes on Android 16, most of it the MLO and ANQP tail. // The whole WifiInfo parcel measured 1340 bytes on Android 16, most of it the MLO and ANQP tail.
@@ -260,7 +350,7 @@ static bool getOwnPackage(char* buffer, size_t capacity) {
} }
static bool isMacAddress(const char* value, uint32_t length) { static bool isMacAddress(const char* value, uint32_t length) {
if (length != 17) { if (length != FF_WIFI_ANDROID_MAC_STRING_LENGTH) {
return false; return false;
} }
for (uint32_t i = 0; i < length; ++i) { for (uint32_t i = 0; i < length; ++i) {
@@ -300,12 +390,16 @@ typedef struct FFWifiAndroidConnection {
int32_t linkSpeed; int32_t linkSpeed;
int32_t txLinkSpeed; int32_t txLinkSpeed;
int32_t rxLinkSpeed; int32_t rxLinkSpeed;
int32_t standard; FFWifiAndroidStandard standard;
FFWifiAndroidSecurity security;
size_t stateEnd; size_t stateEnd;
size_t standardOffset; // where the Wi-Fi standard was found, which is the anchor for `security`
uint16_t frequency; uint16_t frequency;
bool up; bool up;
bool upKnown; // whether the state of the interface was established at all bool upKnown; // whether the state of the interface was established at all
bool connected; bool connected;
bool ssidAbsent; // whether the reply carried no SSID at all, rather than one that was read
bool bssidSeen; // whether the BSSID slot was passed already, placeholder or not
} FFWifiAndroidConnection; } FFWifiAndroidConnection;
// The Wi-Fi interface is the one the HAL names `wlan*`, and it is looked up rather than assumed so // The Wi-Fi interface is the one the HAL names `wlan*`, and it is looked up rather than assumed so
@@ -339,7 +433,7 @@ static void detectInterface(FFWifiAndroidConnection* connection) {
} }
} }
static const char* wifiStandardName(int32_t standard) { static const char* wifiStandardName(FFWifiAndroidStandard standard) {
switch (standard) { switch (standard) {
case FF_WIFI_ANDROID_STANDARD_LEGACY: return "802.11"; case FF_WIFI_ANDROID_STANDARD_LEGACY: return "802.11";
case FF_WIFI_ANDROID_STANDARD_11A: return "802.11a"; case FF_WIFI_ANDROID_STANDARD_11A: return "802.11a";
@@ -355,7 +449,7 @@ static const char* wifiStandardName(int32_t standard) {
// 11ac and 11ad live in bands the connection is not in when it reports a 2.4 GHz frequency, so a // 11ac and 11ad live in bands the connection is not in when it reports a 2.4 GHz frequency, so a
// candidate that disagrees with the band is a misread rather than a standard. // candidate that disagrees with the band is a misread rather than a standard.
static bool isStandardPlausible(int32_t standard, uint16_t frequency) { static bool isStandardPlausible(FFWifiAndroidStandard standard, uint16_t frequency) {
if (frequency < 3000 && (standard == FF_WIFI_ANDROID_STANDARD_11AC || standard == FF_WIFI_ANDROID_STANDARD_11AD)) { if (frequency < 3000 && (standard == FF_WIFI_ANDROID_STANDARD_11AC || standard == FF_WIFI_ANDROID_STANDARD_11AD)) {
return false; return false;
} }
@@ -369,7 +463,10 @@ static bool isStandardPlausible(int32_t standard, uint16_t frequency) {
// their distance from the head depends on how long the SSID is and on which release wrote the // their distance from the head depends on how long the SSID is and on which release wrote the
// parcel. They are recognised by shape instead of by offset: a 17 character `hh:hh:hh:hh:hh:hh`, and // parcel. They are recognised by shape instead of by offset: a 17 character `hh:hh:hh:hh:hh:hh`, and
// one of the thirteen state names. The BSSID is written before the MAC, so the first match is the // one of the thirteen state names. The BSSID is written before the MAC, so the first match is the
// one wanted. `from` is the end of the SSID octets, which is where the BSSID follows. // one wanted -- and that slot is settled by the first match even when it turns out to hold the
// placeholder rather than an address, since a second MAC-shaped string further down is the MAC
// address or a vendor's own copy of either, never the BSSID. `from` is the end of the SSID octets,
// or the word after the marker when the reply carried no SSID, which is where the BSSID follows.
static void findStrings(const uint8_t* data, size_t size, size_t from, FFWifiAndroidConnection* connection) { static void findStrings(const uint8_t* data, size_t size, size_t from, FFWifiAndroidConnection* connection) {
for (size_t offset = from; offset + 4 <= size; ++offset) { for (size_t offset = from; offset + 4 <= size; ++offset) {
const uint32_t length = ffBinderReadU32(data, size, offset); const uint32_t length = ffBinderReadU32(data, size, offset);
@@ -392,15 +489,20 @@ static void findStrings(const uint8_t* data, size_t size, size_t from, FFWifiAnd
} }
buffer[length] = '\0'; buffer[length] = '\0';
if (connection->bssid[0] == '\0' && isMacAddress(buffer, length)) { if (!connection->bssidSeen && isMacAddress(buffer, length)) {
memcpy(connection->bssid, buffer, length + 1); connection->bssidSeen = true;
// The placeholder means there is no address to report: it is what the service writes
// both for a caller it may not tell and for a connection that has none.
if (strcmp(buffer, FF_WIFI_ANDROID_DEFAULT_MAC_ADDRESS) != 0) {
memcpy(connection->bssid, buffer, length + 1);
}
} else if (connection->state[0] == '\0' && isSupplicantState(buffer, length)) { } else if (connection->state[0] == '\0' && isSupplicantState(buffer, length)) {
memcpy(connection->state, buffer, length + 1); memcpy(connection->state, buffer, length + 1);
// The state is the last string before the tail, so where it ends is where the tail // The state is the last string before the tail, so where it ends is where the tail
// starts. A Parcel pads a string to the next word, terminator included. // starts. A Parcel pads a string to the next word, terminator included.
connection->stateEnd = offset + 4 + ((length * 2 + 2 + 3) & ~3u); connection->stateEnd = offset + 4 + ((length * 2 + 2 + 3) & ~3u);
} }
if (connection->bssid[0] != '\0' && connection->state[0] != '\0') { if (connection->bssidSeen && connection->state[0] != '\0') {
return; return;
} }
} }
@@ -427,13 +529,139 @@ static void findWifiStandard(const uint8_t* data, size_t size, FFWifiAndroidConn
if (maxTx < 0 || maxTx > FF_WIFI_ANDROID_RATE_MAX || maxRx < 0 || maxRx > FF_WIFI_ANDROID_RATE_MAX) { if (maxTx < 0 || maxTx > FF_WIFI_ANDROID_RATE_MAX || maxRx < 0 || maxRx > FF_WIFI_ANDROID_RATE_MAX) {
continue; continue;
} }
connection->standard = standard; connection->standard = (FFWifiAndroidStandard) standard;
connection->standardOffset = offset;
FF_DEBUG("Wi-Fi standard %d at +0x%zx, maximum speeds %d/%d", standard, offset, maxTx, maxRx); FF_DEBUG("Wi-Fi standard %d at +0x%zx, maximum speeds %d/%d", standard, offset, maxTx, maxRx);
return; return;
} }
FF_DEBUG("No Wi-Fi standard matched after the supplicant state at +0x%zx", connection->stateEnd); FF_DEBUG("No Wi-Fi standard matched after the supplicant state at +0x%zx", connection->stateEnd);
} }
// The name the module prints for a `WifiInfo.SecurityType`. The spellings follow the Windows backend
// where the two overlap, except that Android collapses WPA-PSK and WPA2-PSK into one value, which is
// reported under the WPA2 name because that is what a value of this kind almost always describes.
// An open network is `Insecure`, as on every other backend.
static const char* wifiSecurityName(FFWifiAndroidSecurity security) {
switch (security) {
case FF_WIFI_ANDROID_SECURITY_OPEN: return "Insecure";
case FF_WIFI_ANDROID_SECURITY_WEP: return "WEP";
case FF_WIFI_ANDROID_SECURITY_PSK: return "WPA2-PSK";
case FF_WIFI_ANDROID_SECURITY_EAP: return "WPA2-ENT";
case FF_WIFI_ANDROID_SECURITY_SAE: return "WPA3-SAE";
case FF_WIFI_ANDROID_SECURITY_EAP_WPA3_ENTERPRISE_192_BIT: return "WPA3-ENT-192";
case FF_WIFI_ANDROID_SECURITY_OWE: return "OWE";
case FF_WIFI_ANDROID_SECURITY_WAPI_PSK: return "WAPI-PSK";
case FF_WIFI_ANDROID_SECURITY_WAPI_CERT: return "WAPI-CERT";
case FF_WIFI_ANDROID_SECURITY_EAP_WPA3_ENTERPRISE: return "WPA3-ENT";
case FF_WIFI_ANDROID_SECURITY_OSEN: return "OSEN";
case FF_WIFI_ANDROID_SECURITY_PASSPOINT_R1_R2: return "Passpoint R1/R2";
case FF_WIFI_ANDROID_SECURITY_PASSPOINT_R3: return "Passpoint R3";
case FF_WIFI_ANDROID_SECURITY_DPP: return "DPP";
default: return nullptr;
}
}
// The security type is three words behind the information element list, and that list is the one
// part of the tail whose length is not fixed: `writeTypedList` writes a count and then a presence
// word in front of every element, and an element writes its id, its extension id and its body as a
// length-prefixed byte array padded to a word. There is no way to step over it without reading it,
// so the walk is exact -- every length it passes is bounds-checked -- and what it lands on has to
// look like the run of three that AOSP writes there: an `isPrimary` marker, the security type, and a
// `restricted` flag, with the network key behind them. A reply that does not is one this file cannot
// place the field in, and the field is then left empty rather than read from a word that only
// happens to fit.
//
// Android 11 is the case that matters: its parcel ends at the passpoint id, so the walk runs off the
// end and is refused, which is the right answer -- there is no security type in that reply to report.
// The walk is also the reason the field cannot be found by searching: the list is full of words that
// would pass for a security type on their own (the Android 16 device carries 27 elements).
static bool findSecurityType(const uint8_t* data, size_t size, FFWifiAndroidConnection* connection) {
if (connection->standardOffset == 0) {
return false; // without the standard there is no anchor to walk from
}
// The two maximum-supported link speeds, then the passpoint id, which is null or a string16.
size_t offset = connection->standardOffset + FF_WIFI_ANDROID_STANDARD_RUN;
const int32_t passpoint = ffBinderReadI32(data, size, offset);
if (passpoint < 0) {
offset += 4; // a null string is a length of -1 and nothing behind it
} else if (passpoint <= FF_WIFI_ANDROID_STRING_MAX_LENGTH) {
offset += 4 + ((((size_t) passpoint * 2 + 2 + 3) & ~(size_t) 3));
} else {
return false;
}
offset += 4; // the subscription id, which takes no part in locating anything
const int32_t elements = ffBinderReadI32(data, size, offset);
offset += 4;
if (elements > 0) {
if (elements > FF_WIFI_ANDROID_IE_MAX_COUNT) {
return false;
}
for (int32_t i = 0; i < elements; ++i) {
if (offset + 16 > size) {
return false;
}
const int32_t present = ffBinderReadI32(data, size, offset);
offset += 4;
if (present == 0) {
continue; // a null element takes its presence word and nothing else
}
if (present != 1) {
return false;
}
// The element id, the extension id, then the body as a length-prefixed array.
const int32_t length = ffBinderReadI32(data, size, offset + 8);
offset += 12;
if (length < 0 || length > FF_WIFI_ANDROID_IE_MAX_LENGTH) {
return false;
}
offset += ((size_t) length + 3) & ~(size_t) 3;
}
}
// `isPrimary` is written from Android 12 on -- it is gated on `SdkLevel.isAtLeastS()`, which is
// what FF_ANDROID_API_AT_LEAST(31) answers -- and that is also the release that introduced the
// list above, so the two arrived together and neither is written before it.
if (FF_ANDROID_API_AT_LEAST(31)) {
if (offset + 4 > size) {
return false;
}
const int32_t isPrimary = ffBinderReadI32(data, size, offset);
if (isPrimary < -1 || isPrimary > 1) {
return false;
}
offset += 4;
}
if (offset + 4 > size) {
return false;
}
const int32_t security = ffBinderReadI32(data, size, offset);
if (security < FF_WIFI_ANDROID_SECURITY_UNKNOWN || security > FF_WIFI_ANDROID_SECURITY_MAX) {
return false;
}
// Android 13 put a `restricted` flag and the network key behind the security type, and Android
// 12 ends the parcel with the security type itself. Where the two are there they are checked,
// because they are the last thing that says the word above is the field it is taken for; where
// the reply ends first, the check has nothing to check and the word stands on its own.
if (offset + 12 <= size) {
const int32_t restricted = ffBinderReadI32(data, size, offset + 4);
const int32_t networkKey = ffBinderReadI32(data, size, offset + 8);
if (restricted < 0 || restricted > 1) {
return false;
}
if (networkKey < -1 || networkKey > FF_WIFI_ANDROID_STRING_MAX_LENGTH) {
return false;
}
}
if (security == FF_WIFI_ANDROID_SECURITY_UNKNOWN) {
return true; // the field is there and the service has not set it: nothing to report
}
connection->security = (FFWifiAndroidSecurity) security;
FF_DEBUG("Security type %d (%s) at +0x%zx", security, wifiSecurityName(security), offset);
return true;
}
#ifndef NDEBUG #ifndef NDEBUG
// The reply is read by shape, so a reply that fails every shape leaves nothing behind to diagnose // The reply is read by shape, so a reply that fails every shape leaves nothing behind to diagnose
// it with: the bytes are the only evidence. Each line carries the words next to their bytes, // it with: the bytes are the only evidence. Each line carries the words next to their bytes,
@@ -509,51 +737,86 @@ static const char* parseConnectionInfo(const uint8_t* data, size_t size, FFWifiA
} }
const uint32_t ssidBase = base + (hasIp == 1 ? FF_WIFI_ANDROID_FREQ_SSID_BASE : FF_WIFI_ANDROID_FREQ_SSID_BASE_NO_IP); const uint32_t ssidBase = base + (hasIp == 1 ? FF_WIFI_ANDROID_FREQ_SSID_BASE : FF_WIFI_ANDROID_FREQ_SSID_BASE_NO_IP);
const int32_t ssidPresent = ffBinderReadI32(data, size, ssidBase); const int32_t ssidPresent = ffBinderReadI32(data, size, ssidBase);
if (ssidPresent != 1) { if (ssidPresent != 0 && ssidPresent != 1) {
FF_DEBUG(" rejected: the SSID at +0x%02x is marked %d, not present", ssidBase, ssidPresent); FF_DEBUG(" rejected: the SSID at +0x%02x is marked %d, which is neither present nor absent", ssidBase, ssidPresent);
continue; // the SSID of a reported connection is never null
}
const uint32_t ssidLength = ffBinderReadU32(data, size, ssidBase + FF_WIFI_ANDROID_SSID_LENGTH);
if (ssidLength == 0 || ssidLength > FF_WIFI_ANDROID_SSID_MAX_LENGTH) {
FF_DEBUG(" rejected: the SSID length at +0x%02x is %u", ssidBase + FF_WIFI_ANDROID_SSID_LENGTH, ssidLength);
continue; continue;
} }
for (uint32_t i = 0; i < ARRAY_SIZE(ssidOffsets); ++i) { // Where the BSSID follows: after the octets when there is an SSID, and directly after the
const size_t ssidOffset = ssidBase + ssidOffsets[i]; // marker when there is not. A marker of 0 is not a layout to reject -- it is the service
if (ssidOffset + ssidLength > size) { // saying it wrote no SSID, which it does both for a caller it may not name the network to and
// for a connection that has no name to give. The rest of the reply is unaffected, so it is
// read and reported without the two names rather than as a reply that could not be read.
size_t stringsFrom = ssidBase + FF_WIFI_ANDROID_SSID_LENGTH;
if (ssidPresent == 0) {
// The BSSID is written directly after the marker, and it is always a 17 character
// address, the placeholder included, so its length word is what keeps this branch a
// shape check rather than an accept-anything: without it the marker alone would be the
// whole evidence that this candidate is the head of a connection.
if (ffBinderReadU32(data, size, stringsFrom) != FF_WIFI_ANDROID_MAC_STRING_LENGTH) {
FF_DEBUG(" rejected: the SSID at +0x%02x is marked absent and no BSSID follows it", ssidBase);
continue;
}
connection->ssidAbsent = true;
FF_DEBUG("The service wrote no SSID at +0x%02x: this caller may not be told it, or there is none", ssidBase);
} else {
const uint32_t ssidLength = ffBinderReadU32(data, size, ssidBase + FF_WIFI_ANDROID_SSID_LENGTH);
if (ssidLength == 0 || ssidLength > FF_WIFI_ANDROID_SSID_MAX_LENGTH) {
FF_DEBUG(" rejected: the SSID length at +0x%02x is %u", ssidBase + FF_WIFI_ANDROID_SSID_LENGTH, ssidLength);
continue; continue;
} }
// The SSID is a raw byte string, so a NUL in it means this was not the SSID after all. bool ssidFound = false;
if (memchr(data + ssidOffset, '\0', ssidLength) != nullptr) { for (uint32_t i = 0; i < ARRAY_SIZE(ssidOffsets) && !ssidFound; ++i) {
FF_DEBUG(" rejected: the %u SSID bytes at +0x%zx hold a NUL", ssidLength, ssidOffset); const size_t ssidOffset = ssidBase + ssidOffsets[i];
if (ssidOffset + ssidLength > size) {
continue;
}
// The SSID is a raw byte string, so a NUL in it means this was not the SSID after all.
if (memchr(data + ssidOffset, '\0', ssidLength) != nullptr) {
FF_DEBUG(" rejected: the %u SSID bytes at +0x%zx hold a NUL", ssidLength, ssidOffset);
continue;
}
memcpy(connection->ssid, data + ssidOffset, ssidLength);
connection->ssid[ssidLength] = '\0';
stringsFrom = ssidOffset + ssidLength;
ssidFound = true;
}
if (!ssidFound) {
continue; continue;
} }
memcpy(connection->ssid, data + ssidOffset, ssidLength);
connection->ssid[ssidLength] = '\0';
connection->frequency = frequency;
connection->rssi = ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_RSSI);
connection->linkSpeed = ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_LINK_SPEED);
connection->txLinkSpeed = ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_TX_LINK_SPEED);
connection->rxLinkSpeed = ffBinderReadI32(data, size, base - FF_WIFI_ANDROID_FREQ_RX_LINK_SPEED);
findStrings(data, size, ssidOffset + ssidLength, connection);
FF_DEBUG("SSID \"%s\" at +0x%zx, BSSID \"%s\", supplicant state \"%s\"",
connection->ssid, ssidOffset, connection->bssid, connection->state);
// The supplicant state is the authoritative signal; the network id is the fallback for a
// parcel whose state string was not recognised.
connection->connected = connection->state[0] != '\0'
? strcmp(connection->state, "COMPLETED") == 0
: ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_NET_ID) >= 0;
findWifiStandard(data, size, connection);
FF_DEBUG("Standard %d (%s), rx link speed %d, connected %s",
connection->standard, wifiStandardName(connection->standard) ?: "unknown", connection->rxLinkSpeed,
connection->connected ? "yes" : "no");
return nullptr;
} }
connection->frequency = frequency;
connection->rssi = ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_RSSI);
connection->linkSpeed = ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_LINK_SPEED);
connection->txLinkSpeed = ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_TX_LINK_SPEED);
connection->rxLinkSpeed = ffBinderReadI32(data, size, base - FF_WIFI_ANDROID_FREQ_RX_LINK_SPEED);
findStrings(data, size, stringsFrom, connection);
FF_DEBUG("SSID \"%s\", BSSID \"%s\", supplicant state \"%s\" (the strings start at +0x%zx)",
connection->ssid, connection->bssid, connection->state, stringsFrom);
// The supplicant state is the authoritative signal. The network id is the fallback for a
// parcel whose state string was not recognised -- but not when the service wrote no SSID,
// because it withholds the network id from that caller as well and the -1 it writes there
// would be read as "there is no connection". The head answers instead: a `WifiInfo` with no
// connection has no frequency to write, so one that is here and passed the checks above is a
// connection.
if (connection->state[0] != '\0') {
connection->connected = strcmp(connection->state, "COMPLETED") == 0;
} else {
connection->connected = connection->ssidAbsent
|| ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_NET_ID) >= 0;
}
findWifiStandard(data, size, connection);
if (!findSecurityType(data, size, connection)) {
FF_DEBUG("No security type could be located in this reply, so the field stays empty");
}
FF_DEBUG("Standard %d (%s), rx link speed %d, connected %s",
connection->standard, wifiStandardName(connection->standard) ?: "unknown", connection->rxLinkSpeed,
connection->connected ? "yes" : "no");
return nullptr;
} }
// Nothing in the head matched, but an unassociated connection has nothing in the head to match: // Nothing in the head matched, but an unassociated connection has nothing in the head to match:
@@ -584,6 +847,19 @@ static double wifiAndroidRate(int32_t specific, int32_t generic) {
return rate > 0 && rate <= FF_WIFI_ANDROID_RATE_MAX ? (double) rate : -DBL_MAX; return rate > 0 && rate <= FF_WIFI_ANDROID_RATE_MAX ? (double) rate : -DBL_MAX;
} }
// The SSID and the BSSID as the module reports them: the value when the reply carried one, and
// `<redacted>` when the service withheld it. A withheld name is not reported as nothing, because an
// empty SSID is what the module prints the interface state in place of -- and the reply did carry a
// signal, a channel and a rate that are worth showing. `withheld` is only ever true for a connection:
// the placeholder the service writes in place of an address is also what an empty `WifiInfo` carries,
// but a reply with no connection never reaches the point where the names are set.
static const char* wifiName(const char* value, bool withheld) {
if (value[0] != '\0') {
return value;
}
return withheld ? FF_WIFI_ANDROID_REDACTED : "";
}
// Calls a method of the service that takes no arguments and answers with an int. The transaction // Calls a method of the service that takes no arguments and answers with an int. The transaction
// code comes out of the jar the same way `getConnectionInfo`'s does, so a release that does not // code comes out of the jar the same way `getConnectionInfo`'s does, so a release that does not
// declare the method answers UNKNOWN_TRANSACTION instead of a number that means something else. // declare the method answers UNKNOWN_TRANSACTION instead of a number that means something else.
@@ -663,7 +939,11 @@ static const char* detectWithBinder(FFlist* result) {
return "Wifi service rejected the request"; return "Wifi service rejected the request";
} }
FFWifiAndroidConnection connection = {}; FFWifiAndroidConnection connection = {
// Zero is a real security type -- an open network -- so the field is not left at what the
// zero initialisation would otherwise make of it.
.security = FF_WIFI_ANDROID_SECURITY_UNKNOWN,
};
detectInterface(&connection); detectInterface(&connection);
error = parseConnectionInfo(reply.data, reply.size, &connection); error = parseConnectionInfo(reply.data, reply.size, &connection);
if (error != nullptr) { if (error != nullptr) {
@@ -730,15 +1010,20 @@ static const char* detectWithBinder(FFlist* result) {
} }
item->conn.signalQuality = connection.rssi >= -50 ? 100 : connection.rssi <= -100 ? 0 item->conn.signalQuality = connection.rssi >= -50 ? 100 : connection.rssi <= -100 ? 0
: (connection.rssi + 100) * 2; : (connection.rssi + 100) * 2;
ffStrbufSetS(&item->conn.bssid, connection.bssid); ffStrbufSetS(&item->conn.bssid, wifiName(connection.bssid, connection.bssidSeen));
ffStrbufSetS(&item->conn.ssid, connection.ssid); ffStrbufSetS(&item->conn.ssid, wifiName(connection.ssid, connection.ssidAbsent));
const char* security = wifiSecurityName(connection.security);
if (security != nullptr) {
ffStrbufSetStatic(&item->conn.security, security);
}
item->conn.frequency = connection.frequency; item->conn.frequency = connection.frequency;
item->conn.txRate = wifiAndroidRate(connection.txLinkSpeed, connection.linkSpeed); item->conn.txRate = wifiAndroidRate(connection.txLinkSpeed, connection.linkSpeed);
item->conn.rxRate = wifiAndroidRate(connection.rxLinkSpeed, 0); item->conn.rxRate = wifiAndroidRate(connection.rxLinkSpeed, 0);
item->conn.channel = ffWifiFreqToChannel(connection.frequency); item->conn.channel = ffWifiFreqToChannel(connection.frequency);
FF_DEBUG("\"%s\" %s: \"%s\", \"%s\", %s, %u MHz (channel %u), signal %.0f, tx %d, rx %d", FF_DEBUG("\"%s\" %s: \"%s\", \"%s\", %s, %s, %u MHz (channel %u), signal %.0f, tx %d, rx %d",
connection.interface, item->inf.status.chars, item->conn.ssid.chars, item->conn.bssid.chars, connection.interface, item->inf.status.chars, item->conn.ssid.chars, item->conn.bssid.chars,
item->conn.protocol.length ? item->conn.protocol.chars : "(no standard)", connection.frequency, item->conn.protocol.length ? item->conn.protocol.chars : "(no standard)",
item->conn.security.length ? item->conn.security.chars : "(no security)", connection.frequency,
item->conn.channel, item->conn.signalQuality, connection.txLinkSpeed, connection.rxLinkSpeed); item->conn.channel, item->conn.signalQuality, connection.txLinkSpeed, connection.rxLinkSpeed);
return nullptr; return nullptr;
} }