master ohmu/fastfetch-openbsd / src / detection / wifi / wifi_android.c
ohmu  ·  2026-10-15
   1#include "wifi.h"
   2
   3#include "common/android/api.h"
   4#include "common/android/binder.h"
   5#include "common/android/dex.h"
   6#include "common/android/package.h"
   7#include "common/debug.h"
   8#include "common/strutil.h"
   9
  10#include <ifaddrs.h>
  11#include <net/if.h>
  12#include <unistd.h>
  13
  14// Android gives an app two ways to see the current connection, and both are weaker than the desktop
  15// ones. `/system/bin/cmd wifi status` prints everything fastfetch wants, but it does not run in the
  16// calling process: `cmd` hands the arguments to the service over binder and the command body runs
  17// inside system_server, where it reads a locally built WifiInfo. An app can therefore not ask for
  18// it -- the call comes back as a silent exit status 255 with nothing on either stream, which is
  19// what makes it look like it worked. `dumpsys wifi` needs android.permission.DUMP, and
  20// /proc/net/wireless, /proc/net/dev and /sys/class/net/wlan0 are all EACCES. That leaves
  21// `IWifiManager.getConnectionInfo`, which needs ACCESS_WIFI_STATE.
  22//
  23// ACCESS_WIFI_STATE is a normal permission, so the user is never asked, but it is granted to a UID
  24// rather than to a package: the service checks the caller's UID, and a UID holds the permission as
  25// soon as any one of the packages sharing it requests it. The question is therefore not whether the
  26// host app declares it, but whether its UID ends up with it.
  27//
  28// Under Termux it does, and only because of a second app. `com.termux` requests no Wi-Fi permission at
  29// all, while `com.termux.api` -- Termux:API -- requests ACCESS_WIFI_STATE and shares `sharedUserId
  30// com.termux`, so it lands on the app's own UID. Uninstalling Termux:API takes the permission off
  31// the UID with it, and this module then reports "The Wifi service raised an exception" where it had
  32// reported the connection; reinstalling it brings the connection back. Both states were measured on
  33// the test device with Android 16 and the same binary. So Termux:API is still needed for this module -- as
  34// the thing that carries the permission, not as the `termux-api` command that used to be spawned. That
  35// command is gone from here because it could not do better than this does: whenever it was available,
  36// so is the permission, and whenever the permission is missing the command is missing too.
  37//
  38// What a UID that does not hold the permission is told, for both calls below. The service names the
  39// reason itself, and the message is 93 characters:
  40//
  41//     WifiService: Neither user <uid> nor current process has android.permission.ACCESS_WIFI_STATE.
  42//         at com.android.server.wifi.WifiServiceImpl.enforceAccessPermission(WifiServiceImpl.java:1541)
  43//         at com.android.server.wifi.WifiServiceImpl.getConnectionInfo(WifiServiceImpl.java:5537)
  44//
  45// It is the calling UID that is checked, not the package name in the request: the same call made as
  46// the shell, asking about a package it does not own, is refused with `Package com.termux does not
  47// belong to <shell uid>` instead. Nothing in this file can work around the permission, since the calling UID
  48// is the process itself.
  49//
  50// The second call sits behind the same check, which is what makes the permission a gate on the module
  51// rather than on one field: `getWifiEnabledState` from a UID that lacks it comes back with the
  52// identical exception and the same message, where a UID that holds it reads the state. Such a host has
  53// no route left in this file at all, and the error the caller sees is raised by the first call, before
  54// the second one could have answered anything.
  55//
  56// Holding the Wi-Fi permission is not the same as being told everything, though, and a reply that
  57// comes back without the SSID is not a reply this module fails on. The service hands out a copy with
  58// the fields the caller may not see removed -- `WifiInfo.makeCopy` drops the SSID, writes
  59// `02:00:00:00:00:00` in place of the BSSID and the MAC address, and rewrites the network id to -1,
  60// while the signal, the rates, the frequency, the IPv4 address and the supplicant state are carried
  61// over untouched. It makes that copy for every caller that `enforceCanAccessScanResults` refuses.
  62//
  63// One route to a withheld name is a permission that can lapse. That copy is gated on location
  64// permission -- `enforceCanAccessScanResults` asks for `ACCESS_FINE_LOCATION` -- which is a runtime
  65// permission the user can revoke at any time, so a caller that once read the SSID keeps its Wi-Fi
  66// permission and stops being told the name. Measured here as a caller whose location permission had
  67// lapsed, next to the shell, which holds one and reads the SSID. Nothing else about the reply moves,
  68// so the head is understood the same way and everything the reply still says is reported: a
  69// connection that cannot be named is still a connection, and the name is the only thing missing.
  70// The two names are reported as `<redacted>` -- the same word the macOS backend uses for the same
  71// situation -- rather than as nothing, because an empty SSID is what the module prints the interface
  72// state in place of, and the reply did carry a signal, a channel and a rate worth showing. The
  73// placeholders the service writes in their place are never reported as values. See
  74// parseConnectionInfo().
  75//
  76// A UID with no Wi-Fi permission at all takes a different route, and it is worth naming because it
  77// is the one that looks like this one from the outside. There the first call raises EX_SECURITY (-1)
  78// and the module fails outright: nothing is printed, `<redacted>` never appears, and the caller sees
  79// "The Wifi service raised an exception". The reason is that the caller's UID is what the service
  80// checks, so no later field of a reply that was never delivered can be recovered -- the two routes
  81// are not a fallback chain. There is no degraded answer to give here and no route left in this file:
  82// without the permission the service answers nothing at all, so the module cannot run rather than
  83// run worse. The file header above says which UID holds the permission and why.
  84//
  85// The network id goes with them, and that one matters to this file: -1 is what it reads as "there is
  86// no connection", so a redacted reply would otherwise be reported as a disconnected one. What
  87// separates the two is the frequency -- a `WifiInfo` with no connection has none to write, so a
  88// frequency that passes the shape checks below is a connection -- and the supplicant state, which is
  89// not redacted and answers directly when it is there.
  90//
  91// The transaction code is read out of the device's own jar rather than carried as a table, so the
  92// module is not tied to a list of releases. It does need the jar to exist, and Android 11 is the
  93// release that moved the Wi-Fi framework into an APEX -- before that the class sat in framework.jar,
  94// which is not read here. So Android 11 (API 30) is the floor, and an older release reports an error
  95// rather than a guess.
  96//
  97// The reply layout itself has been checked against the AOSP source as far back as Android 9, which
  98// is why nothing about it is release-gated: the head grew a link speed in Android 10 and a Wi-Fi
  99// standard in Android 11, and both are found by shape.
 100//
 101// Whether the radio is on is a second call, `IWifiManager.getWifiEnabledState`, which takes no
 102// arguments and answers with a `WifiManager.WIFI_STATE_*`. It is needed because the interface
 103// cannot answer for itself: bionic's `getifaddrs` lists only the interfaces that hold an address,
 104// so the flags that would say whether `wlan0` is up leave the list exactly when there is no
 105// association -- and nothing else in the reply separates a Wi-Fi that is off from one that is on
 106// with nothing behind it. Both answer with an empty `WifiInfo`, the same at every offset, down to
 107// the `02:00:00:00:00:00` placeholder it writes in place of a BSSID. The call is made only when the
 108// interface was not in the list, so a connected device -- where `IFF_UP` is both closer to hand and
 109// more accurate -- pays nothing for it.
 110//
 111// Three things about the call are positional rather than negotiated, and all three were measured
 112// across the Android 11, 13, 16 and 17 devices:
 113//
 114//   * The transaction code is a build-time constant of the `.aidl` (`getConnectionInfo` is 29 on
 115//     Android 11, 36 on Android 13 and 41 on Android 16 and 17) -- see common/android/dex.h.
 116//   * The reply layout drifts, and not only by release. Android 12 dropped the duplicate length word
 117//     that Android 11 and 10 wrote in front of the SSID octets, and the Android 17 device writes a
 118//     word of its own between the link speed and the transmit speed that the Android 11 and 13
 119//     devices leave out -- a vendor addition rather than a release difference, since AOSP's own
 120//     Android 16 writes the head the Android 11 way. Frequency is the only channel centre in the
 121//     head, so it is located by value, with the whole result validated before it is used, and the
 122//     two direction specific speeds are read as its neighbours. A wrong guess shows up as a
 123//     rejected layout, not as a plausible wrong number.
 124//   * The tail of the parcel, which holds the Wi-Fi standard, is not laid out the same way by every
 125//     vendor: the Android 11 device carries three words there that AOSP's own Android 11
 126//     does not. It is located by shape for that reason.
 127//
 128// BSSID and MAC are the one place the binder route beats `cmd wifi status`: MAC addresses are
 129// redacted for a caller targeting a recent SDK, and the shell targets the current one, so `cmd wifi
 130// status` prints an address with its middle octets redacted where this reads the whole one.
 131//
 132// What is deliberately left empty:
 133//
 134//   * `security` on a release whose parcel carries no such field. Android 11 ends its parcel at the
 135//     passpoint id, so there is nothing to read and nothing is reported; Android 13 and later write
 136//     one, and it is read. Where it sits is behind the information element list, which is
 137//     variable-length and changes with the access point -- 27 elements on the Android 16 device --
 138//     so reaching it means walking that list in full. The walk is exact and its result is checked
 139//     before it is believed. See findSecurityType().
 140//   * The IPv4 address. It is in the parcel, but FFWifiConnection has no field for it and
 141//     `inf.description` is the interface name, as on every other platform. The LocalIP module
 142//     reports addresses.
 143//   * `inf.description` while the interface is not in the address list. Its name is not knowable
 144//     from anywhere else -- sysfs and /proc/net are EACCES -- so the field stays empty rather than
 145//     naming an interface that was guessed at. `inf.status` is still reported, from the radio.
 146//   * `conn.ssid` and `conn.bssid` on a connection whose names the service withheld are reported as
 147//     `<redacted>` instead, which is not empty: see the note above. They are the two fields a
 148//     lapsed location permission takes away and nothing else. They are left empty when there is no
 149//     connection at all, where an absent name is the truth rather than a redaction.
 150
 151#define FF_WIFI_ANDROID_SERVICE "wifi"
 152#define FF_WIFI_ANDROID_DESCRIPTOR "android.net.wifi.IWifiManager"
 153
 154// Android 11 is the release that moved the Wi-Fi framework out of framework.jar and into an APEX,
 155// which is where this jar is. Nothing older carries it, so there is no second path to try: the
 156// class is either in the APEX or the release is too old.
 157#define FF_WIFI_ANDROID_JAR "/apex/com.android.wifi/javalib/framework-wifi.jar"
 158#define FF_WIFI_ANDROID_STUB "Landroid/net/wifi/IWifiManager$Stub;"
 159#define FF_WIFI_ANDROID_GET_CONNECTION_INFO "TRANSACTION_getConnectionInfo"
 160// Whether the radio is on is the one thing the connection does not say. An empty `WifiInfo` is what
 161// the service answers with both while Wi-Fi is off and while it is on with nothing associated, so
 162// the two states are told apart by asking the service directly. The method takes no arguments and
 163// answers with a `WifiManager.WIFI_STATE_*`.
 164#define FF_WIFI_ANDROID_GET_WIFI_ENABLED_STATE "TRANSACTION_getWifiEnabledState"
 165
 166// `WifiManager.WIFI_STATE_*`. Only two of the five are acted on: everything else -- the enabled
 167// state and the two transitional ones -- means the radio is on.
 168typedef enum FFWifiAndroidRadioState : int32_t
 169{
 170    FF_WIFI_ANDROID_WIFI_STATE_DISABLED = 1,
 171    FF_WIFI_ANDROID_WIFI_STATE_UNKNOWN = 4,
 172} FFWifiAndroidRadioState;
 173
 174// An AIDL reply opens with the exception code and, for a Parcelable return, a non-null marker;
 175// WifiInfo itself then starts at 8. The network id, the RSSI and the link speed are the first three
 176// fields `writeToParcel` writes, so nothing a release or a vendor adds behind them can move them --
 177// they have held those offsets from Android 9 to 17 and are read there. The two direction specific
 178// speeds sit behind them but are located from the frequency instead, because a vendor does write a
 179// word of its own in front of that pair: see the note at FF_WIFI_ANDROID_FREQ_TX_LINK_SPEED.
 180#define FF_WIFI_ANDROID_OFF_NET_ID 8
 181#define FF_WIFI_ANDROID_OFF_RSSI 12
 182#define FF_WIFI_ANDROID_OFF_LINK_SPEED 16
 183
 184// A `WifiInfo` with no connection to describe carries the sentinels for the two fields that are
 185// always at a fixed offset: -1 for a network id it does not have and -127 for a signal it cannot
 186// measure. Both were measured on the Android 16 device's reply, which is what the service answers
 187// while Wi-Fi is on with nothing associated -- the head holds no frequency and no SSID at all, only
 188// the "02:00:00:00:00:00" placeholder that `WifiInfo` writes in place of a BSSID.
 189#define FF_WIFI_ANDROID_NET_ID_NONE (-1)
 190#define FF_WIFI_ANDROID_RSSI_NONE (-127)
 191
 192// Frequency is looked for in the head, after the link speeds and before the address. See the note
 193// above for why it is not at a fixed offset.
 194#define FF_WIFI_ANDROID_FREQ_SCAN_FROM 0x14
 195#define FF_WIFI_ANDROID_FREQ_SCAN_TO 0x34
 196
 197// Distances from Frequency. AOSP writes an address as a flag byte followed by a byte array, and
 198// omits the array entirely when there is no IPv4 address -- an IPv6-only network does that -- which
 199// shifts everything after it by two words. The base of the SSID section is therefore derived from
 200// that flag rather than fixed, and only the SSID's own offset is left as a pair: Android 11 wrote
 201// the length twice in front of the octets and Android 12 dropped the duplicate.
 202//
 203// The SSID is the one field WifiInfo writes as raw bytes rather than as a UTF-16 string, so its
 204// length is a byte count and a NUL among the bytes means the offset was wrong.
 205#define FF_WIFI_ANDROID_FREQ_HAS_IP 0x04
 206#define FF_WIFI_ANDROID_FREQ_IP_LENGTH 0x08
 207#define FF_WIFI_ANDROID_FREQ_SSID_BASE 0x10
 208#define FF_WIFI_ANDROID_FREQ_SSID_BASE_NO_IP 0x08
 209#define FF_WIFI_ANDROID_SSID_LENGTH 0x04
 210
 211// The transmit speed and the receive speed are the two words in front of Frequency, in that order:
 212// `mTxLinkSpeed, mRxLinkSpeed, mFrequency` are written back to back by every head this file has
 213// been checked against, from Android 11 through 17. The pair is read from Frequency rather than
 214// stepped to from the link speed, because the Android 17 device writes a word of its own between
 215// the link speed and the transmit speed: its dex names that word `mCurrentLinkSpeed`, so the word
 216// after the link speed is not the transmit speed there and the pair sits one word further along.
 217// Both were checked against `cmd wifi status` at the same moment: 131/219 on the Android 16 device,
 218// 86/-1 on the Android 11 device.
 219#define FF_WIFI_ANDROID_FREQ_TX_LINK_SPEED 0x08
 220#define FF_WIFI_ANDROID_FREQ_RX_LINK_SPEED 0x04
 221#define FF_WIFI_ANDROID_SSID 0x08        // Android 12 and later
 222#define FF_WIFI_ANDROID_SSID_LEGACY 0x0c // Android 11 and older
 223#define FF_WIFI_ANDROID_SSID_MAX_LENGTH 32
 224
 225// `WifiInfo.DEFAULT_MAC_ADDRESS`: what the service writes in place of the BSSID and of the MAC
 226// address when the caller may not be told them, and also what it writes when it has neither to give.
 227// Neither reading is an address, so it is never reported as one -- the field stays empty instead.
 228#define FF_WIFI_ANDROID_DEFAULT_MAC_ADDRESS "02:00:00:00:00:00"
 229
 230// A BSSID is written as a string of `hh:hh:hh:hh:hh:hh`, so it is this many characters long -- the
 231// placeholder above included. Its length word is what stands in for the SSID octets as the check
 232// that a reply carrying no SSID is still laid out the way this file reads it.
 233#define FF_WIFI_ANDROID_MAC_STRING_LENGTH 17
 234
 235// What the two names are reported as when the service withheld them. The macOS backend spells the
 236// same situation with the same word, and it is a word rather than an empty string so that the module
 237// keeps printing the signal and the rate it did get instead of the interface state.
 238#define FF_WIFI_ANDROID_REDACTED "<redacted>"
 239
 240// `WifiInfo` writes the Wi-Fi standard and then the two maximum-supported link speeds it can reach,
 241// so the standard is the first word of a run of three. What follows the run is the passpoint id.
 242#define FF_WIFI_ANDROID_STANDARD_RUN 12
 243
 244// The longest a string16 this file is willing to step over can be. Only the passpoint id and the
 245// network key are stepped over by length, and neither is ever long; the bound is what keeps a
 246// misread length from walking the parser off the end of the reply.
 247#define FF_WIFI_ANDROID_STRING_MAX_LENGTH 64
 248
 249// The bounds the information element list is walked under. A reply carries tens of elements, each
 250// tens of bytes, so these are far above anything real and only ever catch a misread.
 251#define FF_WIFI_ANDROID_IE_MAX_COUNT 256
 252#define FF_WIFI_ANDROID_IE_MAX_LENGTH 1024
 253
 254// `WifiInfo.SecurityType`, which is what `writeToParcel` writes -- and NOT the same numbering as
 255// `WifiConfiguration.SecurityType`, which the two only share up to SAE. `Unknown` is what the field
 256// holds before the service sets it, and is reported as no security rather than as an open network.
 257typedef enum FFWifiAndroidSecurity : int32_t
 258{
 259    FF_WIFI_ANDROID_SECURITY_UNKNOWN = -1,
 260    FF_WIFI_ANDROID_SECURITY_OPEN = 0,
 261    FF_WIFI_ANDROID_SECURITY_WEP = 1,
 262    FF_WIFI_ANDROID_SECURITY_PSK = 2,
 263    FF_WIFI_ANDROID_SECURITY_EAP = 3,
 264    FF_WIFI_ANDROID_SECURITY_SAE = 4,
 265    FF_WIFI_ANDROID_SECURITY_EAP_WPA3_ENTERPRISE_192_BIT = 5,
 266    FF_WIFI_ANDROID_SECURITY_OWE = 6,
 267    FF_WIFI_ANDROID_SECURITY_WAPI_PSK = 7,
 268    FF_WIFI_ANDROID_SECURITY_WAPI_CERT = 8,
 269    FF_WIFI_ANDROID_SECURITY_EAP_WPA3_ENTERPRISE = 9,
 270    FF_WIFI_ANDROID_SECURITY_OSEN = 10,
 271    FF_WIFI_ANDROID_SECURITY_PASSPOINT_R1_R2 = 11,
 272    FF_WIFI_ANDROID_SECURITY_PASSPOINT_R3 = 12,
 273    FF_WIFI_ANDROID_SECURITY_DPP = 13,
 274    // The largest value this file knows of, which is what bounds the read below. AOSP has no such
 275    // constant; a release that adds a security type past DPP is refused rather than reported.
 276    FF_WIFI_ANDROID_SECURITY_MAX = FF_WIFI_ANDROID_SECURITY_DPP,
 277} FFWifiAndroidSecurity;
 278
 279// `WifiInfo` writes the Wi-Fi standard as one of the `ScanResult.WIFI_STANDARD_*` values. It is
 280// followed by the two maximum-supported link speeds, which is what makes it recognisable: an int
 281// naming a standard, then two plausible rates. The band is checked against it as well, so a value
 282// that only fits another band is rejected. A layout that is not recognised leaves `protocol` empty
 283// instead of reporting the wrong standard.
 284typedef enum FFWifiAndroidStandard : int32_t
 285{
 286    FF_WIFI_ANDROID_STANDARD_LEGACY = 1,
 287    FF_WIFI_ANDROID_STANDARD_11A = 2,
 288    FF_WIFI_ANDROID_STANDARD_11B = 3,
 289    FF_WIFI_ANDROID_STANDARD_11N = 4,
 290    FF_WIFI_ANDROID_STANDARD_11AC = 5,
 291    FF_WIFI_ANDROID_STANDARD_11AX = 6,
 292    FF_WIFI_ANDROID_STANDARD_11AD = 7,
 293    FF_WIFI_ANDROID_STANDARD_11BE = 8,
 294} FFWifiAndroidStandard;
 295
 296#define FF_WIFI_ANDROID_RATE_MAX 20000
 297
 298// The whole WifiInfo parcel measured 1340 bytes on Android 16, most of it the MLO and ANQP tail.
 299#define FF_WIFI_ANDROID_REPLY_SIZE 4096
 300
 301// The layout is found by shape rather than by offset, so a rejected reply leaves nothing to go on.
 302// The debug dump covers the head -- where the frequency, the address flag and the SSID sit -- and
 303// stops before the tail, which is the long MLO and ANQP list and takes no part in locating
 304// anything. Read with `fastfetch -s Wifi` under a debug build.
 305#define FF_WIFI_ANDROID_DEBUG_DUMP_SIZE 0x60
 306
 307// ---------------------------------------------------------------------------------------------
 308// Binder
 309// ---------------------------------------------------------------------------------------------
 310
 311// The caller's own package name, which `getConnectionInfo` takes as its second argument: see
 312// ffAndroidGetOwnPackage() in common/android/package.h for why it has to be that one and how it is
 313// derived. `getWallpaper` in the Wallpaper module asks for the same thing.
 314
 315static bool isMacAddress(const char* value, uint32_t length) {
 316    if (length != FF_WIFI_ANDROID_MAC_STRING_LENGTH) {
 317        return false;
 318    }
 319    for (uint32_t i = 0; i < length; ++i) {
 320        const char c = value[i];
 321        if (i % 3 == 2) {
 322            if (c != ':') {
 323                return false;
 324            }
 325        } else if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) {
 326            return false;
 327        }
 328    }
 329    return true;
 330}
 331
 332// SupplicantState, in the order AOSP declares it.
 333static const char* const FF_WIFI_ANDROID_STATES[] = {
 334    "DISCONNECTED", "INTERFACE_DISABLED", "INACTIVE", "SCANNING", "AUTHENTICATING", "ASSOCIATING",
 335    "ASSOCIATED", "FOUR_WAY_HANDSHAKE", "GROUP_HANDSHAKE", "COMPLETED", "DORMANT", "UNINITIALIZED", "INVALID",
 336};
 337
 338static bool isSupplicantState(const char* value, uint32_t length) {
 339    for (uint32_t i = 0; i < ARRAY_SIZE(FF_WIFI_ANDROID_STATES); ++i) {
 340        if (strlen(FF_WIFI_ANDROID_STATES[i]) == length && memcmp(FF_WIFI_ANDROID_STATES[i], value, length) == 0) {
 341            return true;
 342        }
 343    }
 344    return false;
 345}
 346
 347typedef struct FFWifiAndroidConnection {
 348    char interface[IF_NAMESIZE + 1];
 349    char state[24];
 350    char ssid[FF_WIFI_ANDROID_SSID_MAX_LENGTH + 1];
 351    char bssid[18];
 352    int32_t rssi;
 353    int32_t linkSpeed;
 354    int32_t txLinkSpeed;
 355    int32_t rxLinkSpeed;
 356    FFWifiAndroidStandard standard;
 357    FFWifiAndroidSecurity security;
 358    size_t stateEnd;
 359    size_t standardOffset; // where the Wi-Fi standard was found, which is the anchor for `security`
 360    uint16_t frequency;
 361    bool up;
 362    bool upKnown;       // whether the state of the interface was established at all
 363    bool connected;
 364    bool ssidAbsent;    // whether the reply carried no SSID at all, rather than one that was read
 365    bool bssidSeen;     // whether the BSSID slot was passed already, placeholder or not
 366} FFWifiAndroidConnection;
 367
 368// The Wi-Fi interface is the one the HAL names `wlan*`, and it is looked up rather than assumed so
 369// that a second one (`wlan1` on a dual-STA device) is still reported by name. The flags describe the
 370// interface, so the first entry for a name is as good as any other.
 371//
 372// What bionic's `getifaddrs` will not do is list an interface that has no address: the list is
 373// built out of the addresses, so `wlan0` leaves it as soon as nothing is associated -- measured on
 374// the Android 16 device, and the Android 11 device behaves the same. That is exactly the state its flags are wanted in, so
 375// when the name is missing the radio state stands in for them. See detectWithBinder().
 376static void detectInterface(FFWifiAndroidConnection* connection) {
 377    struct ifaddrs* addrs = nullptr;
 378    if (getifaddrs(&addrs) != 0) {
 379        FF_DEBUG("getifaddrs failed, so neither the interface name nor its state is known");
 380        return;
 381    }
 382
 383    for (const struct ifaddrs* ifa = addrs; ifa != nullptr; ifa = ifa->ifa_next) {
 384        if (ifa->ifa_name == nullptr || !ffStrStartsWith(ifa->ifa_name, "wlan")) {
 385            continue;
 386        }
 387        snprintf(connection->interface, sizeof(connection->interface), "%s", ifa->ifa_name);
 388        connection->up = (ifa->ifa_flags & IFF_UP) != 0;
 389        connection->upKnown = true;
 390        FF_DEBUG("Interface \"%s\" is %s (IFF_UP %s)", connection->interface, connection->up ? "up" : "down", connection->up ? "set" : "clear");
 391        break;
 392    }
 393    freeifaddrs(addrs);
 394    if (connection->interface[0] == '\0') {
 395        FF_DEBUG("No interface named wlan* is present");
 396    }
 397}
 398
 399static const char* wifiStandardName(FFWifiAndroidStandard standard) {
 400    switch (standard) {
 401        case FF_WIFI_ANDROID_STANDARD_LEGACY: return "802.11";
 402        case FF_WIFI_ANDROID_STANDARD_11A: return "802.11a";
 403        case FF_WIFI_ANDROID_STANDARD_11B: return "802.11b";
 404        case FF_WIFI_ANDROID_STANDARD_11N: return "802.11n (Wi-Fi 4)";
 405        case FF_WIFI_ANDROID_STANDARD_11AC: return "802.11ac (Wi-Fi 5)";
 406        case FF_WIFI_ANDROID_STANDARD_11AX: return "802.11ax (Wi-Fi 6)";
 407        case FF_WIFI_ANDROID_STANDARD_11AD: return "802.11ad (WiGig)";
 408        case FF_WIFI_ANDROID_STANDARD_11BE: return "802.11be (Wi-Fi 7)";
 409        default: return nullptr;
 410    }
 411}
 412
 413// 11ac and 11ad live in bands the connection is not in when it reports a 2.4 GHz frequency, so a
 414// candidate that disagrees with the band is a misread rather than a standard.
 415static bool isStandardPlausible(FFWifiAndroidStandard standard, uint16_t frequency) {
 416    if (frequency < 3000 && (standard == FF_WIFI_ANDROID_STANDARD_11AC || standard == FF_WIFI_ANDROID_STANDARD_11AD)) {
 417        return false;
 418    }
 419    if (frequency > 50000 && standard != FF_WIFI_ANDROID_STANDARD_11AD) {
 420        return false;
 421    }
 422    return true;
 423}
 424
 425// WifiInfo writes the BSSID, the MAC and the supplicant state as length-prefixed UTF-16 strings, and
 426// their distance from the head depends on how long the SSID is and on which release wrote the
 427// parcel. They are recognised by shape instead of by offset: a 17 character `hh:hh:hh:hh:hh:hh`, and
 428// one of the thirteen state names. The BSSID is written before the MAC, so the first match is the
 429// one wanted -- and that slot is settled by the first match even when it turns out to hold the
 430// placeholder rather than an address, since a second MAC-shaped string further down is the MAC
 431// address or a vendor's own copy of either, never the BSSID. `from` is the end of the SSID octets,
 432// or the word after the marker when the reply carried no SSID, which is where the BSSID follows.
 433static void findStrings(const uint8_t* data, size_t size, size_t from, FFWifiAndroidConnection* connection) {
 434    for (size_t offset = from; offset + 4 <= size; ++offset) {
 435        const uint32_t length = ffBinderReadU32(data, size, offset);
 436        if (length == 0 || length > 24 || offset + 4 + (size_t) length * 2 > size) {
 437            continue;
 438        }
 439
 440        const uint8_t* chars = data + offset + 4;
 441        char buffer[25];
 442        bool ascii = true;
 443        for (uint32_t i = 0; i < length; ++i) {
 444            if (chars[i * 2 + 1] != 0 || chars[i * 2] < 0x20 || chars[i * 2] >= 0x7f) {
 445                ascii = false;
 446                break;
 447            }
 448            buffer[i] = (char) chars[i * 2];
 449        }
 450        if (!ascii) {
 451            continue;
 452        }
 453        buffer[length] = '\0';
 454
 455        if (!connection->bssidSeen && isMacAddress(buffer, length)) {
 456            connection->bssidSeen = true;
 457            // The placeholder means there is no address to report: it is what the service writes
 458            // both for a caller it may not tell and for a connection that has none.
 459            if (!ffStrEquals(buffer, FF_WIFI_ANDROID_DEFAULT_MAC_ADDRESS)) {
 460                memcpy(connection->bssid, buffer, length + 1);
 461            }
 462        } else if (connection->state[0] == '\0' && isSupplicantState(buffer, length)) {
 463            memcpy(connection->state, buffer, length + 1);
 464            // The state is the last string before the tail, so where it ends is where the tail
 465            // starts. A Parcel pads a string to the next word, terminator included.
 466            connection->stateEnd = offset + 4 + ((length * 2 + 2 + 3) & ~3u);
 467        }
 468        if (connection->bssidSeen && connection->state[0] != '\0') {
 469            return;
 470        }
 471    }
 472}
 473
 474// AOSP writes the Wi-Fi standard between three nullable strings and the two maximum-supported link
 475// speeds, all of them ints. Vendors insert fields around that run -- the Android 11 device carries
 476// three words before the standard that AOSP's own Android 11 does not -- so a fixed
 477// offset would be a guess. The run is recognised by shape instead, searching forward from the end of
 478// the supplicant state: an int naming a standard, followed by two rates, in a band the standard can
 479// live in.
 480static void findWifiStandard(const uint8_t* data, size_t size, FFWifiAndroidConnection* connection) {
 481    if (connection->stateEnd == 0) {
 482        return; // without the state string there is no trustworthy place to start from
 483    }
 484
 485    for (size_t offset = connection->stateEnd; offset + 12 <= size; offset += 4) {
 486        const int32_t standard = ffBinderReadI32(data, size, offset);
 487        if (wifiStandardName(standard) == nullptr || !isStandardPlausible(standard, connection->frequency)) {
 488            continue;
 489        }
 490        const int32_t maxTx = ffBinderReadI32(data, size, offset + 4);
 491        const int32_t maxRx = ffBinderReadI32(data, size, offset + 8);
 492        if (maxTx < 0 || maxTx > FF_WIFI_ANDROID_RATE_MAX || maxRx < 0 || maxRx > FF_WIFI_ANDROID_RATE_MAX) {
 493            continue;
 494        }
 495        connection->standard = (FFWifiAndroidStandard) standard;
 496        connection->standardOffset = offset;
 497        FF_DEBUG("Wi-Fi standard %d at +0x%zx, maximum speeds %d/%d", standard, offset, maxTx, maxRx);
 498        return;
 499    }
 500    FF_DEBUG("No Wi-Fi standard matched after the supplicant state at +0x%zx", connection->stateEnd);
 501}
 502
 503// The name the module prints for a `WifiInfo.SecurityType`. The spellings follow the Windows backend
 504// where the two overlap, except that Android collapses WPA-PSK and WPA2-PSK into one value, which is
 505// reported under the WPA2 name because that is what a value of this kind almost always describes.
 506// An open network is `Insecure`, as on every other backend.
 507static const char* wifiSecurityName(FFWifiAndroidSecurity security) {
 508    switch (security) {
 509        case FF_WIFI_ANDROID_SECURITY_OPEN: return "Insecure";
 510        case FF_WIFI_ANDROID_SECURITY_WEP: return "WEP";
 511        case FF_WIFI_ANDROID_SECURITY_PSK: return "WPA2-PSK";
 512        case FF_WIFI_ANDROID_SECURITY_EAP: return "WPA2-ENT";
 513        case FF_WIFI_ANDROID_SECURITY_SAE: return "WPA3-SAE";
 514        case FF_WIFI_ANDROID_SECURITY_EAP_WPA3_ENTERPRISE_192_BIT: return "WPA3-ENT-192";
 515        case FF_WIFI_ANDROID_SECURITY_OWE: return "OWE";
 516        case FF_WIFI_ANDROID_SECURITY_WAPI_PSK: return "WAPI-PSK";
 517        case FF_WIFI_ANDROID_SECURITY_WAPI_CERT: return "WAPI-CERT";
 518        case FF_WIFI_ANDROID_SECURITY_EAP_WPA3_ENTERPRISE: return "WPA3-ENT";
 519        case FF_WIFI_ANDROID_SECURITY_OSEN: return "OSEN";
 520        case FF_WIFI_ANDROID_SECURITY_PASSPOINT_R1_R2: return "Passpoint R1/R2";
 521        case FF_WIFI_ANDROID_SECURITY_PASSPOINT_R3: return "Passpoint R3";
 522        case FF_WIFI_ANDROID_SECURITY_DPP: return "DPP";
 523        default: return nullptr;
 524    }
 525}
 526
 527// The security type is three words behind the information element list, and that list is the one
 528// part of the tail whose length is not fixed: `writeTypedList` writes a count and then a presence
 529// word in front of every element, and an element writes its id, its extension id and its body as a
 530// length-prefixed byte array padded to a word. There is no way to step over it without reading it,
 531// so the walk is exact -- every length it passes is bounds-checked -- and what it lands on has to
 532// look like the run of three that AOSP writes there: an `isPrimary` marker, the security type, and a
 533// `restricted` flag, with the network key behind them. A reply that does not is one this file cannot
 534// place the field in, and the field is then left empty rather than read from a word that only
 535// happens to fit.
 536//
 537// Android 11 is the case that matters: its parcel ends at the passpoint id, so the walk runs off the
 538// end and is refused, which is the right answer -- there is no security type in that reply to report.
 539// The walk is also the reason the field cannot be found by searching: the list is full of words that
 540// would pass for a security type on their own (the Android 16 device carries 27 elements).
 541static bool findSecurityType(const uint8_t* data, size_t size, FFWifiAndroidConnection* connection) {
 542    if (connection->standardOffset == 0) {
 543        return false; // without the standard there is no anchor to walk from
 544    }
 545
 546    // The two maximum-supported link speeds, then the passpoint id, which is null or a string16.
 547    size_t offset = connection->standardOffset + FF_WIFI_ANDROID_STANDARD_RUN;
 548    const int32_t passpoint = ffBinderReadI32(data, size, offset);
 549    if (passpoint < 0) {
 550        offset += 4; // a null string is a length of -1 and nothing behind it
 551    } else if (passpoint <= FF_WIFI_ANDROID_STRING_MAX_LENGTH) {
 552        offset += 4 + ((((size_t) passpoint * 2 + 2 + 3) & ~(size_t) 3));
 553    } else {
 554        return false;
 555    }
 556    offset += 4; // the subscription id, which takes no part in locating anything
 557
 558    const int32_t elements = ffBinderReadI32(data, size, offset);
 559    offset += 4;
 560    if (elements > 0) {
 561        if (elements > FF_WIFI_ANDROID_IE_MAX_COUNT) {
 562            return false;
 563        }
 564        for (int32_t i = 0; i < elements; ++i) {
 565            if (offset + 16 > size) {
 566                return false;
 567            }
 568            const int32_t present = ffBinderReadI32(data, size, offset);
 569            offset += 4;
 570            if (present == 0) {
 571                continue; // a null element takes its presence word and nothing else
 572            }
 573            if (present != 1) {
 574                return false;
 575            }
 576            // The element id, the extension id, then the body as a length-prefixed array.
 577            const int32_t length = ffBinderReadI32(data, size, offset + 8);
 578            offset += 12;
 579            if (length < 0 || length > FF_WIFI_ANDROID_IE_MAX_LENGTH) {
 580                return false;
 581            }
 582            offset += ((size_t) length + 3) & ~(size_t) 3;
 583        }
 584    }
 585
 586    // `isPrimary` is written from Android 12 on -- it is gated on `SdkLevel.isAtLeastS()`, which is
 587    // what FF_ANDROID_API_AT_LEAST(31) answers -- and that is also the release that introduced the
 588    // list above, so the two arrived together and neither is written before it.
 589    if (FF_ANDROID_API_AT_LEAST(31)) {
 590        if (offset + 4 > size) {
 591            return false;
 592        }
 593        const int32_t isPrimary = ffBinderReadI32(data, size, offset);
 594        if (isPrimary < -1 || isPrimary > 1) {
 595            return false;
 596        }
 597        offset += 4;
 598    }
 599    if (offset + 4 > size) {
 600        return false;
 601    }
 602    const int32_t security = ffBinderReadI32(data, size, offset);
 603    if (security < FF_WIFI_ANDROID_SECURITY_UNKNOWN || security > FF_WIFI_ANDROID_SECURITY_MAX) {
 604        return false;
 605    }
 606    // Android 13 put a `restricted` flag and the network key behind the security type, and Android
 607    // 12 ends the parcel with the security type itself. Where the two are there they are checked,
 608    // because they are the last thing that says the word above is the field it is taken for; where
 609    // the reply ends first, the check has nothing to check and the word stands on its own.
 610    if (offset + 12 <= size) {
 611        const int32_t restricted = ffBinderReadI32(data, size, offset + 4);
 612        const int32_t networkKey = ffBinderReadI32(data, size, offset + 8);
 613        if (restricted < 0 || restricted > 1) {
 614            return false;
 615        }
 616        if (networkKey < -1 || networkKey > FF_WIFI_ANDROID_STRING_MAX_LENGTH) {
 617            return false;
 618        }
 619    }
 620    if (security == FF_WIFI_ANDROID_SECURITY_UNKNOWN) {
 621        return true; // the field is there and the service has not set it: nothing to report
 622    }
 623    connection->security = (FFWifiAndroidSecurity) security;
 624    FF_DEBUG("Security type %d (%s) at +0x%zx", security, wifiSecurityName(security), offset);
 625    return true;
 626}
 627
 628#ifndef NDEBUG
 629// The reply is read by shape, so a reply that fails every shape leaves nothing behind to diagnose
 630// it with: the bytes are the only evidence. Each line carries the words next to their bytes,
 631// because what the reader is looking for -- a value that would pass for a frequency, an address
 632// flag, a string length -- is an int.
 633static void debugDumpReply(const uint8_t* data, size_t size) {
 634    const size_t limit = size < FF_WIFI_ANDROID_DEBUG_DUMP_SIZE ? size : FF_WIFI_ANDROID_DEBUG_DUMP_SIZE;
 635    for (size_t offset = 0; offset < limit; offset += 16) {
 636        char hex[16 * 3 + 1];
 637        size_t used = 0;
 638        for (size_t i = 0; i < 16 && offset + i < limit; ++i) {
 639            used += (size_t) snprintf(hex + used, sizeof(hex) - used, "%02x ", data[offset + i]);
 640        }
 641        FF_DEBUG("  +0x%02zx  %-47s | %d %d %d %d", offset, hex,
 642            ffBinderReadI32(data, size, offset), ffBinderReadI32(data, size, offset + 4),
 643            ffBinderReadI32(data, size, offset + 8), ffBinderReadI32(data, size, offset + 12));
 644    }
 645}
 646    #define FF_WIFI_ANDROID_DEBUG_DUMP(data, size) debugDumpReply(data, size)
 647#else
 648    #define FF_WIFI_ANDROID_DEBUG_DUMP(data, size) ((void) 0)
 649#endif
 650
 651static const char* parseConnectionInfo(const uint8_t* data, size_t size, FFWifiAndroidConnection* connection) {
 652    if (size < FF_WIFI_ANDROID_FREQ_SCAN_FROM + 4) {
 653        FF_DEBUG("The reply is %zu bytes, too short to hold a WifiInfo", size);
 654        return "The reply is too short to hold a WifiInfo";
 655    }
 656    const int32_t exception = ffBinderReadI32(data, size, 0);
 657    if (exception != 0) {
 658        // The two exceptions this call raises mean different things to a reader, and the code below
 659        // can not tell them apart -- it only sees that the connection could not be read. -1 is
 660        // EX_SECURITY, which is the service refusing a UID that does not hold ACCESS_WIFI_STATE, and
 661        // that is the whole module failing rather than one field going missing. The reply with the
 662        // names withheld is the *other* route: it is a successful reply, and it is handled below as
 663        // a connection that can not be named, not as an error. Saying so here is what keeps the two
 664        // from reading as one.
 665        FF_DEBUG("The Wifi service raised exception %d%s", exception, exception == -1 ? " (EX_SECURITY): this UID has no ACCESS_WIFI_STATE, so no reply carries a connection" : "");
 666        return "The Wifi service raised an exception";
 667    }
 668    if (ffBinderReadU32(data, size, 4) == 0) {
 669        // A null WifiInfo is what the service returns when it has nothing to report, which is the
 670        // normal reply while Wi-Fi is off. The interface, when it is still there, is reported as
 671        // disconnected.
 672        FF_DEBUG("The reply is %zu bytes and carries no WifiInfo", size);
 673        return nullptr;
 674    }
 675    FF_DEBUG("Reply is %zu bytes: netId %d, rssi %d, link speed %d",
 676        size,
 677        ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_NET_ID),
 678        ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_RSSI),
 679        ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_LINK_SPEED));
 680
 681    // The two orders WifiSsid has written its length in: once before the octets on Android 11 and
 682    // older, not at all on Android 12 and later, which leaves the octets one word earlier. The one
 683    // that matches this release is tried first.
 684    uint32_t ssidOffsets[2] = { FF_WIFI_ANDROID_SSID_LEGACY, FF_WIFI_ANDROID_SSID };
 685    if (FF_ANDROID_API_AT_LEAST(31)) {
 686        ssidOffsets[0] = FF_WIFI_ANDROID_SSID;
 687        ssidOffsets[1] = FF_WIFI_ANDROID_SSID_LEGACY;
 688    }
 689
 690    for (uint32_t base = FF_WIFI_ANDROID_FREQ_SCAN_FROM; base <= FF_WIFI_ANDROID_FREQ_SCAN_TO; base += 4) {
 691        const uint16_t frequency = (uint16_t) ffBinderReadU32(data, size, base);
 692        if (frequency == 0 || ffWifiFreqToChannel(frequency) == 0) {
 693            continue;
 694        }
 695        FF_DEBUG("+0x%02x holds %u, which is channel %u", base, frequency, ffWifiFreqToChannel(frequency));
 696
 697        const int32_t hasIp = ffBinderReadI32(data, size, base + FF_WIFI_ANDROID_FREQ_HAS_IP);
 698        if (hasIp != 0 && hasIp != 1) {
 699            FF_DEBUG("  rejected: +0x%02x is not an address flag but %d", base + FF_WIFI_ANDROID_FREQ_HAS_IP, hasIp);
 700            continue;
 701        }
 702        const uint32_t ipLength = ffBinderReadU32(data, size, base + FF_WIFI_ANDROID_FREQ_IP_LENGTH);
 703        if (hasIp == 1 && ipLength != 4) {
 704            FF_DEBUG("  rejected: the address at +0x%02x is %u bytes long", base + FF_WIFI_ANDROID_FREQ_IP_LENGTH, ipLength);
 705            continue;
 706        }
 707        const uint32_t ssidBase = base + (hasIp == 1 ? FF_WIFI_ANDROID_FREQ_SSID_BASE : FF_WIFI_ANDROID_FREQ_SSID_BASE_NO_IP);
 708        const int32_t ssidPresent = ffBinderReadI32(data, size, ssidBase);
 709        if (ssidPresent != 0 && ssidPresent != 1) {
 710            FF_DEBUG("  rejected: the SSID at +0x%02x is marked %d, which is neither present nor absent", ssidBase, ssidPresent);
 711            continue;
 712        }
 713
 714        // Where the BSSID follows: after the octets when there is an SSID, and directly after the
 715        // marker when there is not. A marker of 0 is not a layout to reject -- it is the service
 716        // saying it wrote no SSID, which it does both for a caller it may not name the network to and
 717        // for a connection that has no name to give. The rest of the reply is unaffected, so it is
 718        // read and reported without the two names rather than as a reply that could not be read.
 719        size_t stringsFrom = ssidBase + FF_WIFI_ANDROID_SSID_LENGTH;
 720        if (ssidPresent == 0) {
 721            // The BSSID is written directly after the marker, and it is always a 17 character
 722            // address, the placeholder included, so its length word is what keeps this branch a
 723            // shape check rather than an accept-anything: without it the marker alone would be the
 724            // whole evidence that this candidate is the head of a connection.
 725            if (ffBinderReadU32(data, size, stringsFrom) != FF_WIFI_ANDROID_MAC_STRING_LENGTH) {
 726                FF_DEBUG("  rejected: the SSID at +0x%02x is marked absent and no BSSID follows it", ssidBase);
 727                continue;
 728            }
 729            connection->ssidAbsent = true;
 730            FF_DEBUG("The service wrote no SSID at +0x%02x: this caller may not be told it, or there is none", ssidBase);
 731        } else {
 732            const uint32_t ssidLength = ffBinderReadU32(data, size, ssidBase + FF_WIFI_ANDROID_SSID_LENGTH);
 733            if (ssidLength == 0 || ssidLength > FF_WIFI_ANDROID_SSID_MAX_LENGTH) {
 734                FF_DEBUG("  rejected: the SSID length at +0x%02x is %u", ssidBase + FF_WIFI_ANDROID_SSID_LENGTH, ssidLength);
 735                continue;
 736            }
 737
 738            bool ssidFound = false;
 739            for (uint32_t i = 0; i < ARRAY_SIZE(ssidOffsets) && !ssidFound; ++i) {
 740                const size_t ssidOffset = ssidBase + ssidOffsets[i];
 741                if (ssidOffset + ssidLength > size) {
 742                    continue;
 743                }
 744
 745                // The SSID is a raw byte string, so a NUL in it means this was not the SSID after all.
 746                if (memchr(data + ssidOffset, '\0', ssidLength) != nullptr) {
 747                    FF_DEBUG("  rejected: the %u SSID bytes at +0x%zx hold a NUL", ssidLength, ssidOffset);
 748                    continue;
 749                }
 750                memcpy(connection->ssid, data + ssidOffset, ssidLength);
 751                connection->ssid[ssidLength] = '\0';
 752                stringsFrom = ssidOffset + ssidLength;
 753                ssidFound = true;
 754            }
 755            if (!ssidFound) {
 756                continue;
 757            }
 758        }
 759
 760        connection->frequency = frequency;
 761        connection->rssi = ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_RSSI);
 762        connection->linkSpeed = ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_LINK_SPEED);
 763        connection->txLinkSpeed = ffBinderReadI32(data, size, base - FF_WIFI_ANDROID_FREQ_TX_LINK_SPEED);
 764        connection->rxLinkSpeed = ffBinderReadI32(data, size, base - FF_WIFI_ANDROID_FREQ_RX_LINK_SPEED);
 765
 766        findStrings(data, size, stringsFrom, connection);
 767        FF_DEBUG("SSID \"%s\", BSSID \"%s\", supplicant state \"%s\" (the strings start at +0x%zx)",
 768            connection->ssid, connection->bssid, connection->state, stringsFrom);
 769        // The supplicant state is the authoritative signal. The network id is the fallback for a
 770        // parcel whose state string was not recognised -- but not when the service wrote no SSID,
 771        // because it withholds the network id from that caller as well and the -1 it writes there
 772        // would be read as "there is no connection". The head answers instead: a `WifiInfo` with no
 773        // connection has no frequency to write, so one that is here and passed the checks above is a
 774        // connection.
 775        if (connection->state[0] != '\0') {
 776            connection->connected = ffStrEquals(connection->state, "COMPLETED");
 777        } else {
 778            connection->connected = connection->ssidAbsent
 779                || ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_NET_ID) >= 0;
 780        }
 781        findWifiStandard(data, size, connection);
 782        if (!findSecurityType(data, size, connection)) {
 783            FF_DEBUG("No security type could be located in this reply, so the field stays empty");
 784        }
 785        FF_DEBUG("Standard %d (%s), tx link speed %d, rx link speed %d, connected %s",
 786            connection->standard, wifiStandardName(connection->standard) ?: "unknown", connection->txLinkSpeed,
 787            connection->rxLinkSpeed, connection->connected ? "yes" : "no");
 788        return nullptr;
 789    }
 790
 791    // Nothing in the head matched, but an unassociated connection has nothing in the head to match:
 792    // no frequency, no SSID and -- unlike a connection -- the two sentinels `WifiInfo` fills in when
 793    // it has nothing to describe. That reply is the service saying there is nothing to report, which
 794    // is a state and not a failure. A reply carrying a network id of its own is a connection, so a
 795    // shape that still does not match one keeps being reported as a failure.
 796    if (ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_NET_ID) == FF_WIFI_ANDROID_NET_ID_NONE &&
 797        ffBinderReadI32(data, size, FF_WIFI_ANDROID_OFF_RSSI) == FF_WIFI_ANDROID_RSSI_NONE) {
 798        FF_DEBUG("No network id and no signal in the %zu byte reply: there is no connection to report", size);
 799        return nullptr;
 800    }
 801
 802    FF_DEBUG("No frequency, no SSID and no supplicant state matched in this reply:");
 803    FF_WIFI_ANDROID_DEBUG_DUMP(data, size);
 804    return "The Wifi service returned a layout fastfetch does not understand";
 805}
 806
 807// Which of the speeds the service wrote becomes a rate. It writes a negative one when it does not
 808// know, which is what the Android 11 device does for the receive side, and a link speed of 0 when
 809// the link is down. `generic` is the fallback for a direction the service left unset: the transmit
 810// side has one -- the generic link speed is a real negotiated rate, just not direction-specific --
 811// the way the Linux backend falls back to SIOCGIWRATE when the station info carries no bitrate.
 812// There is no
 813// generic receive speed, so 0 is passed there.
 814static double wifiAndroidRate(int32_t specific, int32_t generic) {
 815    const int32_t rate = specific > 0 ? specific : generic;
 816    return rate > 0 && rate <= FF_WIFI_ANDROID_RATE_MAX ? (double) rate : -DBL_MAX;
 817}
 818
 819// The SSID and the BSSID as the module reports them: the value when the reply carried one, and
 820// `<redacted>` when the service withheld it. A withheld name is not reported as nothing, because an
 821// empty SSID is what the module prints the interface state in place of -- and the reply did carry a
 822// signal, a channel and a rate that are worth showing. This is the whole of the degradation: what a
 823// lapsed location permission costs the module is these two fields and nothing else, since the
 824// service withholds them from the copy it makes and leaves the rest of the reply intact.
 825// `withheld` is only ever true for a connection: the placeholder the service writes in place of an
 826// address is also what an empty `WifiInfo` carries, but a reply with no connection never reaches the
 827// point where the names are set.
 828static const char* wifiName(const char* value, bool withheld) {
 829    if (value[0] != '\0') {
 830        return value;
 831    }
 832    return withheld ? FF_WIFI_ANDROID_REDACTED : "";
 833}
 834
 835// Every transaction code this module sends, resolved from the device's own jar in one walk. They are
 836// asked for together because the walk is dominated by the dex's type table and both codes sit in the
 837// same class of the same entry -- see common/android/dex.h.
 838typedef struct FFWifiAndroidCodes {
 839    int32_t getConnectionInfo;
 840    int32_t getWifiEnabledState;
 841} FFWifiAndroidCodes;
 842
 843// Resolves both codes and reports the one outcome the module cannot work around: a jar that does not
 844// declare `getConnectionInfo` has no connection to report. The radio state is a refinement -- the
 845// module already carries on when the service will not answer it -- so it is allowed to come back as
 846// the sentinel, and what that costs is decided where it is used rather than here.
 847static const char* resolveCodes(FFWifiAndroidCodes* codes) {
 848    const FFDexStaticIntRequest requests[] = {
 849        { FF_WIFI_ANDROID_STUB, FF_WIFI_ANDROID_GET_CONNECTION_INFO, &codes->getConnectionInfo },
 850        { FF_WIFI_ANDROID_STUB, FF_WIFI_ANDROID_GET_WIFI_ENABLED_STATE, &codes->getWifiEnabledState },
 851    };
 852
 853    const char* error = ffDexStaticInts(FF_WIFI_ANDROID_JAR, requests, ARRAY_SIZE(requests));
 854    if (error != nullptr) {
 855        FF_DEBUG("Reading the transaction codes from \"%s\" failed: %s", FF_WIFI_ANDROID_JAR, error);
 856        return error;
 857    }
 858    if (codes->getConnectionInfo == FF_DEX_STATIC_INT_UNRESOLVED) {
 859        FF_DEBUG("The Wifi service does not declare getConnectionInfo");
 860        return "The Wifi service does not declare getConnectionInfo";
 861    }
 862    return nullptr;
 863}
 864
 865// Calls a method of the service that takes no arguments and answers with an int. `transaction` comes
 866// out of the jar, and the sentinel the reader leaves for a code it could not find is caught here: a
 867// release that does not declare the method is a message rather than a number that means something
 868// else. `transactionField` is carried only to name the method in the debug output, which a release
 869// build compiles out -- hence the attribute.
 870static const char* callIntMethod(FFBinder* binder, uint32_t handle, [[maybe_unused]] const char* transactionField, int32_t transaction, int32_t* result) {
 871    if (transaction == FF_DEX_STATIC_INT_UNRESOLVED) {
 872        FF_DEBUG("The Wifi service does not declare that method");
 873        return "The Wifi service does not declare that method";
 874    }
 875
 876    uint8_t parcelBuffer[256];
 877    FFBinderParcel parcel = ffBinderParcelCreate(parcelBuffer, sizeof(parcelBuffer));
 878    ffBinderParcelPutInterfaceToken(&parcel, FF_WIFI_ANDROID_DESCRIPTOR);
 879
 880    uint8_t replyBuffer[64];
 881    FFBinderReply reply = ffBinderReplyCreate(replyBuffer, sizeof(replyBuffer));
 882    const char* error = ffBinderTransact(binder, handle, (uint32_t) transaction, 0, &parcel, &reply);
 883    if (error != nullptr) {
 884        return error;
 885    }
 886    if (ffBinderReplyIsStatus(&reply)) {
 887        FF_DEBUG("\"%s\" resolved to transaction %d, which this build does not answer", transactionField, transaction);
 888        return "The Wifi service does not answer that request";
 889    }
 890    const int32_t exception = ffBinderReadI32(reply.data, reply.size, 0);
 891    if (exception != 0) {
 892        FF_DEBUG("\"%s\" is transaction %d and raised exception %d", transactionField, transaction, exception);
 893        return "The Wifi service raised an exception";
 894    }
 895    *result = ffBinderReadI32(reply.data, reply.size, 4);
 896    FF_DEBUG("\"%s\" is transaction %d and answered %d", transactionField, transaction, *result);
 897    return nullptr;
 898}
 899
 900// `ffAndroidGetOwnPackage` writes at most this many bytes, terminator included.
 901#define FF_WIFI_ANDROID_PACKAGE_SIZE 128
 902
 903// The parcel for `getConnectionInfo` is the interface token, the package name and the -1 that
 904// stands for a null `callingFeatureId`. A string16 costs 4 + 2 * (length + 1) bytes padded to four,
 905// and the token puts three int32 in front of the descriptor. Deriving the size from the two names
 906// rather than rounding it up is what keeps the longest package name `ffAndroidGetOwnPackage` can
 907// produce from marking the parcel truncated -- that fails the whole detection, rather than degrading
 908// to a call without a name.
 909#define FF_WIFI_ANDROID_PARCEL_SIZE \
 910    (12 + ((4 + 2 * sizeof(FF_WIFI_ANDROID_DESCRIPTOR) + 3) & ~3u) \
 911        + ((4 + 2 * FF_WIFI_ANDROID_PACKAGE_SIZE + 3) & ~3u) + 4)
 912
 913static const char* detectWithBinder(FFlist* result) {
 914    char package[FF_WIFI_ANDROID_PACKAGE_SIZE];
 915    if (!ffAndroidGetOwnPackage(package, sizeof(package))) {
 916        FF_DEBUG("Cannot determine the package name of this process");
 917        return "Cannot determine the package name of this process";
 918    }
 919
 920    // Resolved before the binder is opened, so that a jar which does not declare the method says so
 921    // rather than the module failing later with a message about the transport.
 922    FFWifiAndroidCodes codes = {};
 923    const char* error = resolveCodes(&codes);
 924    if (error != nullptr) {
 925        return error;
 926    }
 927
 928    [[gnu::cleanup(ffBinderClose)]] FFBinder binder = { .fd = -1 };
 929    error = ffBinderOpen(&binder);
 930    if (error != nullptr) {
 931        return error;
 932    }
 933
 934    // Released on every path out of this function, including the ones below that return early: the
 935    // references the lookup takes are the process's only claim on the service node.
 936    [[gnu::cleanup(ffBinderServiceHandleRelease)]] FFBinderServiceHandle service = { .binder = &binder };
 937    error = ffBinderLookupService(&binder, FF_WIFI_ANDROID_SERVICE, FF_BINDER_SM_GET_SERVICE, &service.handle);
 938    if (error != nullptr) {
 939        return error;
 940    }
 941    FF_DEBUG("The \"%s\" service is handle %u, %s is transaction %d",
 942        FF_WIFI_ANDROID_SERVICE, service.handle, FF_WIFI_ANDROID_GET_CONNECTION_INFO, codes.getConnectionInfo);
 943
 944    uint8_t parcelBuffer[FF_WIFI_ANDROID_PARCEL_SIZE];
 945    FFBinderParcel parcel = ffBinderParcelCreate(parcelBuffer, sizeof(parcelBuffer));
 946    ffBinderParcelPutInterfaceToken(&parcel, FF_WIFI_ANDROID_DESCRIPTOR);
 947    ffBinderParcelPutString16(&parcel, package);
 948    // callingFeatureId is nullable, and a null String is a length of -1 rather than an empty string.
 949    ffBinderParcelPutI32(&parcel, -1);
 950
 951    uint8_t replyBuffer[FF_WIFI_ANDROID_REPLY_SIZE];
 952    FFBinderReply reply = ffBinderReplyCreate(replyBuffer, sizeof(replyBuffer));
 953    error = ffBinderTransact(&binder, service.handle, (uint32_t) codes.getConnectionInfo, 0, &parcel, &reply);
 954    if (error != nullptr) {
 955        return error;
 956    }
 957    if (ffBinderReplyIsStatus(&reply)) {
 958        FF_DEBUG("The reply carries status %d instead of a parcel", (int) ffBinderReadI32(reply.data, reply.size, 0));
 959        return "Wifi service rejected the request";
 960    }
 961
 962    FFWifiAndroidConnection connection = {
 963        // Zero is a real security type -- an open network -- so the field is not left at what the
 964        // zero initialisation would otherwise make of it.
 965        .security = FF_WIFI_ANDROID_SECURITY_UNKNOWN,
 966    };
 967    detectInterface(&connection);
 968    error = parseConnectionInfo(reply.data, reply.size, &connection);
 969    if (error != nullptr) {
 970        return error;
 971    }
 972
 973    if (!connection.upKnown) {
 974        // `getifaddrs` leaves the interface out while it has no address, so its flags are missing in
 975        // exactly the state they are wanted in. The radio answers for it: a Wi-Fi that is off is one
 976        // whose interface is down, and a Wi-Fi that is on with nothing associated keeps an interface
 977        // that is up. WIFI_STATE_DISABLING is a radio that is still up, so only the one value means
 978        // down, and WIFI_STATE_UNKNOWN is the service declining to say.
 979        int32_t state = 0;
 980        const char* stateError = callIntMethod(&binder, service.handle, FF_WIFI_ANDROID_GET_WIFI_ENABLED_STATE, codes.getWifiEnabledState, &state);
 981        if (stateError == nullptr && state != FF_WIFI_ANDROID_WIFI_STATE_UNKNOWN) {
 982            connection.up = state != FF_WIFI_ANDROID_WIFI_STATE_DISABLED;
 983            connection.upKnown = true;
 984            FF_DEBUG("The radio is %s, which stands in for the interface flags", connection.up ? "on" : "off");
 985        } else {
 986            FF_DEBUG("The state of the radio is not known: %s",
 987                stateError != nullptr ? stateError : "the service answered WIFI_STATE_UNKNOWN");
 988        }
 989    }
 990
 991    // Nothing was found to report on at all. That is a device without a Wi-Fi interface rather than
 992    // one with a Wi-Fi that is off, which has a state to print by now.
 993    if (!connection.upKnown && !connection.connected) {
 994        FF_DEBUG("No Wi-Fi interface is present");
 995        return "No Wi-Fi interface is present";
 996    }
 997
 998    FFWifiResult* item = FF_LIST_ADD(FFWifiResult, *result);
 999    ffStrbufInit(&item->inf.description);
1000    ffStrbufInit(&item->inf.status);
1001    ffStrbufInit(&item->conn.status);
1002    ffStrbufInit(&item->conn.ssid);
1003    ffStrbufInit(&item->conn.bssid);
1004    ffStrbufInit(&item->conn.protocol);
1005    ffStrbufInit(&item->conn.security);
1006    item->conn.signalQuality = -DBL_MAX;
1007    item->conn.rxRate = -DBL_MAX;
1008    item->conn.txRate = -DBL_MAX;
1009    item->conn.channel = 0;
1010    item->conn.channelWidth = 0;
1011    item->conn.frequency = 0;
1012
1013    ffStrbufSetS(&item->inf.description, connection.interface);
1014    // The name is empty when the interface was not in the address list, and the state is empty when
1015    // nothing established it: both are left empty rather than guessed at. The module prints this
1016    // state whenever there is no SSID to print instead, which is exactly the case where neither is
1017    // in the reply.
1018    if (connection.upKnown) {
1019        ffStrbufSetStatic(&item->inf.status, connection.up ? "Up" : "Down");
1020    }
1021    ffStrbufSetStatic(&item->conn.status, connection.connected ? "connected" : "disconnected");
1022    if (!connection.connected) {
1023        FF_DEBUG("Nothing is associated: interface \"%s\" is \"%s\", the connection is \"%s\"",
1024            connection.interface, item->inf.status.chars, item->conn.status.chars);
1025        return nullptr;
1026    }
1027
1028    const char* protocol = wifiStandardName(connection.standard);
1029    if (protocol != nullptr) {
1030        ffStrbufSetStatic(&item->conn.protocol, protocol);
1031    }
1032    item->conn.signalQuality = connection.rssi >= -50 ? 100 : connection.rssi <= -100 ? 0
1033                                                                                     : (connection.rssi + 100) * 2;
1034    ffStrbufSetS(&item->conn.bssid, wifiName(connection.bssid, connection.bssidSeen));
1035    ffStrbufSetS(&item->conn.ssid, wifiName(connection.ssid, connection.ssidAbsent));
1036    const char* security = wifiSecurityName(connection.security);
1037    if (security != nullptr) {
1038        ffStrbufSetStatic(&item->conn.security, security);
1039    }
1040    item->conn.frequency = connection.frequency;
1041    item->conn.txRate = wifiAndroidRate(connection.txLinkSpeed, connection.linkSpeed);
1042    item->conn.rxRate = wifiAndroidRate(connection.rxLinkSpeed, 0);
1043    item->conn.channel = ffWifiFreqToChannel(connection.frequency);
1044    FF_DEBUG("\"%s\" %s: \"%s\", \"%s\", %s, %s, %u MHz (channel %u), signal %.0f, tx %d, rx %d",
1045        connection.interface, item->inf.status.chars, item->conn.ssid.chars, item->conn.bssid.chars,
1046        item->conn.protocol.length ? item->conn.protocol.chars : "(no standard)",
1047        item->conn.security.length ? item->conn.security.chars : "(no security)", connection.frequency,
1048        item->conn.channel, item->conn.signalQuality, connection.txLinkSpeed, connection.rxLinkSpeed);
1049    return nullptr;
1050}
1051
1052const char* ffDetectWifi(FFlist* result) {
1053    return detectWithBinder(result);
1054}