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}