ohmu
·
2026-10-15
1#include "wifi.h"
2#include "common/endian.h"
3#include "common/io.h"
4#include "common/debug.h"
5#include "common/percent.h"
6#include "common/strutil.h"
7
8#include <sys/time.h>
9#include <sys/socket.h>
10#include <sys/ioctl.h>
11#include <sys/types.h>
12#include <net/if.h>
13#include <linux/wireless.h>
14#include <unistd.h>
15
16#include <linux/genetlink.h>
17#include <linux/nl80211.h>
18
19// Silence warning of `NLA_HDRLEN` and `NLA_ALIGN`
20#pragma GCC diagnostic ignored "-Wsign-conversion"
21
22typedef struct FFWifiNlContext {
23 int sockFd;
24 uint16_t nl80211FamilyId;
25 uint32_t portId;
26 uint32_t seq;
27} FFWifiNlContext;
28
29typedef struct FFWifiSecurityFlags {
30 bool privacy : 1;
31 bool wep : 1;
32 bool wpa : 1;
33 bool wpa2 : 1;
34 bool wpa3 : 1;
35 bool owe : 1;
36 bool eap : 1;
37} FFWifiSecurityFlags;
38
39static inline uint32_t ffWifiGetNetlinkPortId(int sockFd) {
40 struct sockaddr_nl addr = {};
41 socklen_t addrLen = sizeof(addr);
42 if (getsockname(sockFd, (struct sockaddr*) &addr, &addrLen) < 0) {
43 FF_DEBUG("Failed to query netlink socket address (use PID instead): %s", strerror(errno));
44 return instance.state.platform.pid;
45 }
46
47 return addr.nl_pid;
48}
49
50static inline bool ffWifiNlAttrOk(const struct nlattr* attr, size_t remaining) {
51 return remaining >= sizeof(*attr) &&
52 attr->nla_len >= sizeof(*attr) &&
53 attr->nla_len <= remaining;
54}
55
56static const struct nlattr* ffWifiNlAttrNext(const struct nlattr* attr, size_t* remaining) {
57 size_t alignedLen = NLA_ALIGN(attr->nla_len);
58 if (alignedLen > *remaining) {
59 *remaining = 0;
60 return nullptr;
61 }
62
63 *remaining -= alignedLen;
64 return (const struct nlattr*) ((const char*) attr + alignedLen);
65}
66
67static inline size_t ffWifiNlAttrPayload(const struct nlattr* attr) {
68 return attr->nla_len > NLA_HDRLEN ? attr->nla_len - NLA_HDRLEN : 0;
69}
70
71static inline const void* ffWifiNlAttrData(const struct nlattr* attr) {
72 // Big endian?
73 return (const uint8_t*) attr + NLA_HDRLEN;
74}
75
76static bool ffWifiNlAppendAttr(struct nlmsghdr* nlh, size_t maxLen, uint16_t type, const void* data, uint16_t dataLen) {
77 size_t offset = NLMSG_ALIGN(nlh->nlmsg_len);
78 size_t attrLen = NLA_HDRLEN + dataLen;
79 size_t alignedLen = NLA_ALIGN(attrLen);
80 size_t newLen = offset + alignedLen;
81 if (newLen > maxLen || attrLen > UINT16_MAX || newLen > UINT32_MAX) {
82 return false;
83 }
84
85 struct nlattr* attr = (struct nlattr*) ((char*) nlh + offset);
86 attr->nla_type = type;
87 attr->nla_len = (uint16_t) attrLen;
88 memcpy((char*) attr + NLA_HDRLEN, data, dataLen);
89 memset((char*) attr + attrLen, 0, alignedLen - attrLen);
90 nlh->nlmsg_len = (uint32_t) newLen;
91 return true;
92}
93
94static bool ffWifiNlGetFamilyId(FFWifiNlContext* ctx) {
95 struct {
96 struct nlmsghdr nlh;
97 struct genlmsghdr genl;
98 char attrs[64];
99 } req = {
100 .nlh = {
101 .nlmsg_len = NLMSG_LENGTH(sizeof(struct genlmsghdr)),
102 .nlmsg_type = GENL_ID_CTRL,
103 .nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK,
104 .nlmsg_seq = ++ctx->seq,
105 .nlmsg_pid = ctx->portId,
106 },
107 .genl = {
108 .cmd = CTRL_CMD_GETFAMILY,
109 .version = 1, // generic netlink control protocol version
110 },
111 };
112
113 if (!ffWifiNlAppendAttr(&req.nlh, sizeof(req), CTRL_ATTR_FAMILY_NAME, "nl80211", sizeof("nl80211"))) {
114 FF_DEBUG("Failed to append CTRL_ATTR_FAMILY_NAME attribute");
115 return false;
116 }
117
118 struct sockaddr_nl addr = {
119 .nl_family = AF_NETLINK,
120 };
121
122 ssize_t sent = sendto(ctx->sockFd, &req, req.nlh.nlmsg_len, 0, (struct sockaddr*) &addr, sizeof(addr));
123 if (sent != (ssize_t) req.nlh.nlmsg_len) {
124 FF_DEBUG("Failed to send nl80211 family request: sent=%zd expected=%u", sent, req.nlh.nlmsg_len);
125 return false;
126 }
127
128 uint8_t buffer[8192];
129 while (true) {
130 ssize_t received = recvfrom(ctx->sockFd, buffer, sizeof(buffer), 0, nullptr, nullptr);
131 if (received < 0) {
132 FF_DEBUG("Failed to receive nl80211 family reply: %s", strerror(errno));
133 return false;
134 }
135
136 for (const struct nlmsghdr* nlh = (const struct nlmsghdr*) buffer;
137 NLMSG_OK(nlh, received);
138 nlh = NLMSG_NEXT(nlh, received)) {
139 if (nlh->nlmsg_seq != req.nlh.nlmsg_seq) {
140 continue;
141 }
142
143 if (nlh->nlmsg_type == NLMSG_ERROR) {
144 const struct nlmsgerr* err = (const struct nlmsgerr*) NLMSG_DATA(nlh);
145 if (err->error != 0) {
146 FF_DEBUG("nl80211 family query failed: %s", strerror(-err->error));
147 return false;
148 }
149 continue;
150 }
151
152 if (nlh->nlmsg_type != GENL_ID_CTRL) {
153 continue;
154 }
155
156 const struct genlmsghdr* genl = (const struct genlmsghdr*) NLMSG_DATA(nlh);
157 if (genl->cmd != CTRL_CMD_NEWFAMILY) {
158 continue;
159 }
160
161 size_t attrRemaining = nlh->nlmsg_len - NLMSG_HDRLEN - GENL_HDRLEN;
162 for (const struct nlattr* attr = (const struct nlattr*) ((const char*) genl + GENL_HDRLEN);
163 ffWifiNlAttrOk(attr, attrRemaining);
164 attr = ffWifiNlAttrNext(attr, &attrRemaining)) {
165 if ((attr->nla_type & NLA_TYPE_MASK) != CTRL_ATTR_FAMILY_ID || ffWifiNlAttrPayload(attr) < sizeof(uint16_t)) {
166 continue;
167 }
168
169 ctx->nl80211FamilyId = *(const uint16_t*) ffWifiNlAttrData(attr);
170 return true;
171 }
172 }
173 }
174}
175
176static bool ffWifiNlInit(FFWifiNlContext* ctx) {
177 FF_AUTO_CLOSE_FD int _ = ctx->sockFd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_GENERIC);
178 if (ctx->sockFd < 0) {
179 FF_DEBUG("Failed to create generic netlink socket: %s", strerror(errno));
180 return false;
181 }
182
183 struct sockaddr_nl addr = {
184 .nl_family = AF_NETLINK,
185 };
186
187 if (bind(ctx->sockFd, (struct sockaddr*) &addr, sizeof(addr)) < 0) {
188 FF_DEBUG("Failed to bind generic netlink socket: %s", strerror(errno));
189 return false;
190 }
191
192 if (setsockopt(
193 ctx->sockFd,
194 SOL_SOCKET,
195 SO_RCVTIMEO,
196 &(struct timeval){ .tv_sec = 0, .tv_usec = 250000 }, // 250 ms recv timeout
197 sizeof(struct timeval)) < 0) {
198 FF_DEBUG("Failed to set netlink receive timeout: %s", strerror(errno));
199 return false;
200 }
201
202 ctx->portId = ffWifiGetNetlinkPortId(ctx->sockFd);
203 if (!ffWifiNlGetFamilyId(ctx)) {
204 return false;
205 }
206
207 _ = -1; // We are ok now
208 return true;
209}
210
211static double ffWifiParseBitrateFromRateInfo(const struct nlattr* rateAttr, FFstrbuf* protocol, uint16_t* channelWidth) {
212 double rate = -DBL_MAX;
213 uint16_t width = 0;
214 size_t remaining = ffWifiNlAttrPayload(rateAttr);
215
216 for (const struct nlattr* info = (const struct nlattr*) ffWifiNlAttrData(rateAttr);
217 ffWifiNlAttrOk(info, remaining);
218 info = ffWifiNlAttrNext(info, &remaining)) {
219 uint16_t type = (uint16_t) (info->nla_type & NLA_TYPE_MASK);
220 size_t payload = ffWifiNlAttrPayload(info);
221
222 switch (type) {
223 case NL80211_RATE_INFO_40_MHZ_WIDTH:
224 width = 40;
225 break;
226 case NL80211_RATE_INFO_80_MHZ_WIDTH:
227 case NL80211_RATE_INFO_80P80_MHZ_WIDTH:
228 width = 80;
229 break;
230 case NL80211_RATE_INFO_160_MHZ_WIDTH:
231 width = 160;
232 break;
233 case 18 /* NL80211_RATE_INFO_320_MHZ_WIDTH */:
234 width = 320;
235 break;
236 case NL80211_RATE_INFO_10_MHZ_WIDTH:
237 width = 10;
238 break;
239 case NL80211_RATE_INFO_5_MHZ_WIDTH:
240 width = 5;
241 break;
242 case 25 /* NL80211_RATE_INFO_1_MHZ_WIDTH */:
243 width = 1;
244 break;
245 case 26 /* NL80211_RATE_INFO_2_MHZ_WIDTH */:
246 width = 2;
247 break;
248 case 27 /* NL80211_RATE_INFO_4_MHZ_WIDTH */:
249 width = 4;
250 break;
251 case 28 /* NL80211_RATE_INFO_8_MHZ_WIDTH */:
252 width = 8;
253 break;
254 case 29 /* NL80211_RATE_INFO_16_MHZ_WIDTH */:
255 width = 16;
256 break;
257 case 30 /* NL80211_RATE_INFO_UHR_MCS */:
258 ffStrbufSetStatic(protocol, "802.11bn (Wi-Fi 8)");
259 break;
260 case 23 /* NL80211_RATE_INFO_S1G_MCS */:
261 ffStrbufSetStatic(protocol, "802.11ah (Wi-Fi HaLow)");
262 break;
263 case 19 /* NL80211_RATE_INFO_EHT_MCS */:
264 ffStrbufSetStatic(protocol, "802.11be (Wi-Fi 7)");
265 break;
266 case 13 /* NL80211_RATE_INFO_HE_MCS */:
267 ffStrbufSetStatic(protocol, "802.11ax (Wi-Fi 6)");
268 break;
269 case NL80211_RATE_INFO_VHT_MCS:
270 ffStrbufSetStatic(protocol, "802.11ac (Wi-Fi 5)");
271 break;
272 case NL80211_RATE_INFO_MCS:
273 ffStrbufSetStatic(protocol, "802.11n (Wi-Fi 4)");
274 break;
275 case NL80211_RATE_INFO_BITRATE32:
276 if (payload >= sizeof(uint32_t)) {
277 rate = *(uint32_t*) ffWifiNlAttrData(info) / 10.0; // nl80211 bitrate unit: 100 kbps => Mbps
278 }
279 break;
280 case NL80211_RATE_INFO_BITRATE:
281 if (payload >= sizeof(uint16_t) && rate == -DBL_MAX) {
282 rate = *(uint16_t*) ffWifiNlAttrData(info) / 10.0; // nl80211 bitrate unit: 100 kbps => Mbps
283 }
284 break;
285 }
286 }
287
288 if (rate != -DBL_MAX && *channelWidth == 0) {
289 *channelWidth = width;
290 }
291
292 return rate;
293}
294
295static uint16_t ffWifiChannelWidthToMhz(uint32_t width) {
296 switch (width) {
297 case NL80211_CHAN_WIDTH_20_NOHT:
298 case NL80211_CHAN_WIDTH_20:
299 return 20;
300 case NL80211_CHAN_WIDTH_40:
301 return 40;
302 case NL80211_CHAN_WIDTH_80:
303 case NL80211_CHAN_WIDTH_80P80:
304 return 80;
305 case NL80211_CHAN_WIDTH_160:
306 return 160;
307 case 6 /* NL80211_CHAN_WIDTH_5 */:
308 return 5;
309 case 7 /* NL80211_CHAN_WIDTH_10 */:
310 return 10;
311 case 8 /* NL80211_CHAN_WIDTH_1 */:
312 return 1;
313 case 9 /* NL80211_CHAN_WIDTH_2 */:
314 return 2;
315 case 10 /* NL80211_CHAN_WIDTH_4 */:
316 return 4;
317 case 11 /* NL80211_CHAN_WIDTH_8 */:
318 return 8;
319 case 12 /* NL80211_CHAN_WIDTH_16 */:
320 return 16;
321 case 13 /* NL80211_CHAN_WIDTH_320 */:
322 return 320;
323 default:
324 return 0;
325 }
326}
327
328static bool ffWifiFetchInterfaceInfo(FFWifiNlContext* ctx, FFWifiResult* item, uint32_t ifIndex) {
329 struct {
330 struct nlmsghdr nlh;
331 struct genlmsghdr genl;
332 char attrs[32];
333 } req = {
334 .nlh = {
335 .nlmsg_len = NLMSG_LENGTH(sizeof(struct genlmsghdr)),
336 .nlmsg_type = ctx->nl80211FamilyId,
337 .nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK,
338 .nlmsg_seq = ++ctx->seq,
339 .nlmsg_pid = ctx->portId,
340 },
341 .genl = {
342 .cmd = NL80211_CMD_GET_INTERFACE,
343 .version = 0,
344 },
345 };
346
347 if (!ffWifiNlAppendAttr(&req.nlh, sizeof(req), NL80211_ATTR_IFINDEX, &ifIndex, sizeof(ifIndex))) {
348 FF_DEBUG("Failed to build nl80211 interface request");
349 return false;
350 }
351
352 struct sockaddr_nl addr = {
353 .nl_family = AF_NETLINK,
354 };
355
356 ssize_t sent = sendto(ctx->sockFd, &req, req.nlh.nlmsg_len, 0, (struct sockaddr*) &addr, sizeof(addr));
357 if (sent != (ssize_t) req.nlh.nlmsg_len) {
358 FF_DEBUG("Failed to send nl80211 interface request");
359 return false;
360 }
361
362 uint8_t buffer[8192];
363 while (true) {
364 ssize_t received = recvfrom(ctx->sockFd, buffer, sizeof(buffer), 0, nullptr, nullptr);
365 if (received < 0) {
366 FF_DEBUG("Failed to receive nl80211 interface reply: %s", strerror(errno));
367 return false;
368 }
369
370 for (const struct nlmsghdr* nlh = (const struct nlmsghdr*) buffer;
371 NLMSG_OK(nlh, received);
372 nlh = NLMSG_NEXT(nlh, received)) {
373 if (nlh->nlmsg_seq != req.nlh.nlmsg_seq) {
374 continue;
375 }
376
377 if (nlh->nlmsg_type == NLMSG_ERROR) {
378 const struct nlmsgerr* err = (const struct nlmsgerr*) NLMSG_DATA(nlh);
379 if (err->error != 0) {
380 FF_DEBUG("nl80211 interface request failed: %s", strerror(-err->error));
381 }
382 return false;
383 }
384
385 if (nlh->nlmsg_type != ctx->nl80211FamilyId) {
386 continue;
387 }
388
389 const struct genlmsghdr* genl = (const struct genlmsghdr*) NLMSG_DATA(nlh);
390 size_t attrRemaining = nlh->nlmsg_len - NLMSG_HDRLEN - GENL_HDRLEN;
391 for (const struct nlattr* attr = (const struct nlattr*) ((const char*) genl + GENL_HDRLEN);
392 ffWifiNlAttrOk(attr, attrRemaining);
393 attr = ffWifiNlAttrNext(attr, &attrRemaining)) {
394 if ((attr->nla_type & NLA_TYPE_MASK) != NL80211_ATTR_CHANNEL_WIDTH ||
395 ffWifiNlAttrPayload(attr) < sizeof(uint32_t)) {
396 continue;
397 }
398
399 item->conn.channelWidth = ffWifiChannelWidthToMhz(*(const uint32_t*) ffWifiNlAttrData(attr));
400 return item->conn.channelWidth != 0;
401 }
402
403 return false;
404 }
405 }
406}
407
408static void ffWifiApplySecurityFlags(FFWifiResult* item, const FFWifiSecurityFlags* sec) {
409 ffStrbufClear(&item->conn.security);
410
411 if (sec->wep) {
412 ffStrbufAppendS(&item->conn.security, "WEP/");
413 }
414 if (sec->wpa) {
415 ffStrbufAppendS(&item->conn.security, "WPA/");
416 }
417 if (sec->wpa2) {
418 ffStrbufAppendS(&item->conn.security, "WPA2/");
419 }
420 if (sec->wpa3) {
421 ffStrbufAppendS(&item->conn.security, "WPA3/");
422 }
423 if (sec->owe) {
424 ffStrbufAppendS(&item->conn.security, "OWE/");
425 }
426 if (sec->eap) {
427 ffStrbufAppendS(&item->conn.security, "802.1X/");
428 }
429
430 if (!item->conn.security.length) {
431 if (sec->privacy) {
432 ffStrbufSetStatic(&item->conn.security, "WEP");
433 } else {
434 ffStrbufSetStatic(&item->conn.security, "Insecure");
435 }
436 } else {
437 ffStrbufTrimRight(&item->conn.security, '/');
438 }
439}
440
441static void ffWifiParseRsnIe(const uint8_t* ie, size_t len, FFWifiSecurityFlags* sec) {
442 if (len < 8) { // version(2) + group cipher suite(4) + pairwise count(2)
443 return;
444 }
445
446 sec->wpa2 = true;
447 size_t pos = 0;
448
449 pos += 2; // RSN version field length
450 if (pos + 4 > len) { // group cipher suite selector length
451 return;
452 }
453 pos += 4; // skip group cipher suite selector
454
455 if (pos + 2 > len) { // pairwise cipher suite count field length
456 return;
457 }
458 uint16_t pairwiseCount = FF_READ_LE(*(const uint16_t*) (ie + pos));
459 pos += 2; // skip pairwise cipher suite count field
460
461 size_t pairwiseLen = (size_t) pairwiseCount * 4; // each suite selector is 4 bytes
462 if (pos + pairwiseLen > len) {
463 return;
464 }
465 pos += pairwiseLen;
466
467 if (pos + 2 > len) { // AKM suite count field length
468 return;
469 }
470 uint16_t akmCount = FF_READ_LE(*(const uint16_t*) (ie + pos));
471 pos += 2; // skip AKM suite count field
472
473 for (uint16_t i = 0; i < akmCount && pos + 4 <= len; ++i, pos += 4) { // each AKM suite selector is 4 bytes
474 const uint8_t* akm = ie + pos;
475 if (akm[0] != 0x00 || akm[1] != 0x0f || akm[2] != 0xac) { // RSN OUI 00:0f:ac
476 continue;
477 }
478
479 switch (akm[3]) {
480 case 1: // 802.1X
481 case 5: // FT/802.1X
482 case 11: // 802.1X-SHA256
483 case 12: // FT/802.1X-SHA384 (suite selector value)
484 sec->eap = true;
485 break;
486 case 8: // SAE (WPA3-Personal)
487 sec->wpa3 = true;
488 break;
489 case 18: // OWE
490 sec->owe = true;
491 break;
492 default:
493 break;
494 }
495 }
496
497 if (sec->owe) {
498 sec->wpa2 = false;
499 }
500}
501
502static void ffWifiParseWpaVendorIe(const uint8_t* ie, size_t len, FFWifiSecurityFlags* sec) {
503 if (len < 8) { // OUI+type(4) + version(2) + multicast cipher suite(4) starts here
504 return;
505 }
506
507 if (!(ie[0] == 0x00 && ie[1] == 0x50 && ie[2] == 0xf2 && ie[3] == 0x01)) { // Microsoft WPA OUI/type
508 return;
509 }
510
511 sec->wpa = true;
512
513 size_t pos = 4; // WPA vendor OUI/type selector length
514 if (pos + 2 > len) { // WPA version field length
515 return;
516 }
517 pos += 2; // skip WPA version
518
519 if (pos + 4 > len) { // multicast cipher suite selector length
520 return;
521 }
522 pos += 4; // skip multicast cipher suite selector
523
524 if (pos + 2 > len) { // unicast cipher suite count field length
525 return;
526 }
527 uint16_t pairwiseCount = FF_READ_LE(*(const uint16_t*) (ie + pos));
528 pos += 2 + (size_t) pairwiseCount * 4; // count field(2) + N unicast suite selectors(4 each)
529
530 if (pos + 2 > len) { // AKM suite count field length
531 return;
532 }
533 uint16_t akmCount = FF_READ_LE(*(const uint16_t*) (ie + pos));
534 pos += 2; // skip AKM suite count field
535
536 for (uint16_t i = 0; i < akmCount && pos + 4 <= len; ++i, pos += 4) { // each AKM suite selector is 4 bytes
537 const uint8_t* akm = ie + pos;
538 if (!(akm[0] == 0x00 && akm[1] == 0x50 && akm[2] == 0xf2)) { // WPA vendor OUI 00:50:f2
539 continue;
540 }
541 if (akm[3] == 1) { // WPA Enterprise (802.1X)
542 sec->eap = true;
543 }
544 }
545}
546
547static void ffWifiParseInformationElements(const uint8_t* ies, size_t length, FFWifiResult* item, FFWifiSecurityFlags* sec) {
548 size_t pos = 0;
549 while (pos + 2 <= length) {
550 uint8_t id = ies[pos];
551 uint8_t len = ies[pos + 1];
552 pos += 2;
553 if (pos + len > length) {
554 break;
555 }
556
557 const uint8_t* ie = ies + pos;
558 if (id == 0) { // SSID element ID
559 ffStrbufSetNS(&item->conn.ssid, len, (const char*) ie);
560 } else if (id == 48) { // RSN element ID
561 ffWifiParseRsnIe(ie, len, sec);
562 } else if (id == 221) { // vendor-specific element ID (WPA IE lives here)
563 ffWifiParseWpaVendorIe(ie, len, sec);
564 }
565
566 pos += len;
567 }
568}
569
570static bool ffWifiIsBssAssociated(const struct nlattr* bssAttr) {
571 size_t remaining = ffWifiNlAttrPayload(bssAttr);
572 for (const struct nlattr* attr = (const struct nlattr*) ffWifiNlAttrData(bssAttr);
573 ffWifiNlAttrOk(attr, remaining);
574 attr = ffWifiNlAttrNext(attr, &remaining)) {
575 uint16_t type = (uint16_t) (attr->nla_type & NLA_TYPE_MASK);
576 size_t payload = ffWifiNlAttrPayload(attr);
577
578 if (type == NL80211_BSS_STATUS && payload >= sizeof(uint32_t)) {
579 return *(uint32_t*) ffWifiNlAttrData(attr) == NL80211_BSS_STATUS_ASSOCIATED;
580 }
581 }
582
583 return false;
584}
585
586static void ffWifiParseBssAttr(const struct nlattr* bssAttr, FFWifiResult* item) {
587 FFWifiSecurityFlags sec = {};
588 size_t remaining = ffWifiNlAttrPayload(bssAttr);
589
590 for (const struct nlattr* attr = (const struct nlattr*) ffWifiNlAttrData(bssAttr);
591 ffWifiNlAttrOk(attr, remaining);
592 attr = ffWifiNlAttrNext(attr, &remaining)) {
593 uint16_t type = (uint16_t) (attr->nla_type & NLA_TYPE_MASK);
594 size_t payload = ffWifiNlAttrPayload(attr);
595
596 if (type == NL80211_BSS_BSSID && payload >= 6) {
597 const uint8_t* mac = (const uint8_t*) ffWifiNlAttrData(attr);
598 ffStrbufSetF(&item->conn.bssid, "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
599 } else if (type == NL80211_BSS_FREQUENCY && payload >= sizeof(uint32_t)) {
600 item->conn.frequency = (uint16_t) *(uint32_t*) ffWifiNlAttrData(attr);
601 item->conn.channel = ffWifiFreqToChannel(item->conn.frequency);
602 } else if (type == NL80211_BSS_SIGNAL_MBM && payload >= sizeof(int32_t)) {
603 int rssi = *(int32_t*) ffWifiNlAttrData(attr) / 100; // mBm (100 * dBm) => dBm
604 item->conn.signalQuality = ffRssiToSignalQuality(rssi);
605 } else if (type == NL80211_BSS_CAPABILITY && payload >= sizeof(uint16_t)) {
606 uint16_t capability = *(uint16_t*) ffWifiNlAttrData(attr);
607 sec.privacy = (capability & (1u << 4u)) != 0; // IEEE 802.11 capability bit 4: privacy
608 } else if (type == NL80211_BSS_INFORMATION_ELEMENTS || type == NL80211_BSS_BEACON_IES) {
609 ffWifiParseInformationElements((const uint8_t*) ffWifiNlAttrData(attr), payload, item, &sec);
610 }
611 }
612
613 ffWifiApplySecurityFlags(item, &sec);
614 return;
615}
616
617static bool ffWifiFetchScanInfo(FFWifiNlContext* ctx, FFWifiResult* item, uint32_t ifIndex) {
618 struct {
619 struct nlmsghdr nlh;
620 struct genlmsghdr genl;
621 char attrs[32];
622 } req = {
623 .nlh = {
624 .nlmsg_len = NLMSG_LENGTH(sizeof(struct genlmsghdr)),
625 .nlmsg_type = ctx->nl80211FamilyId,
626 .nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP | NLM_F_ACK,
627 .nlmsg_seq = ++ctx->seq,
628 .nlmsg_pid = ctx->portId,
629 },
630 .genl = {
631 .cmd = NL80211_CMD_GET_SCAN,
632 .version = 0, // nl80211 command version
633 },
634 };
635
636 if (!ffWifiNlAppendAttr(&req.nlh, sizeof(req), NL80211_ATTR_IFINDEX, &ifIndex, sizeof(ifIndex))) {
637 FF_DEBUG("Failed to build nl80211 scan request");
638 return false;
639 }
640
641 struct sockaddr_nl addr = {
642 .nl_family = AF_NETLINK,
643 };
644
645 ssize_t sent = sendto(ctx->sockFd, &req, req.nlh.nlmsg_len, 0, (struct sockaddr*) &addr, sizeof(addr));
646 if (sent != (ssize_t) req.nlh.nlmsg_len) {
647 FF_DEBUG("Failed to send nl80211 scan request");
648 return false;
649 }
650
651 uint8_t buffer[1024 * 16];
652 while (true) {
653 ssize_t received = recvfrom(ctx->sockFd, buffer, sizeof(buffer), 0, nullptr, nullptr);
654 if (received < 0) {
655 FF_DEBUG("Failed to receive nl80211 scan reply: %s", strerror(errno));
656 return false;
657 }
658
659 for (const struct nlmsghdr* nlh = (const struct nlmsghdr*) buffer;
660 NLMSG_OK(nlh, received);
661 nlh = NLMSG_NEXT(nlh, received)) {
662 if (nlh->nlmsg_seq != req.nlh.nlmsg_seq) {
663 continue;
664 }
665
666 if (nlh->nlmsg_type == NLMSG_DONE) {
667 return false;
668 }
669
670 if (nlh->nlmsg_type == NLMSG_ERROR) {
671 const struct nlmsgerr* err = (const struct nlmsgerr*) NLMSG_DATA(nlh);
672 if (err->error == 0) {
673 continue;
674 }
675 FF_DEBUG("nl80211 scan request failed: %s", strerror(-err->error));
676 return false;
677 }
678
679 if (nlh->nlmsg_type != ctx->nl80211FamilyId) {
680 continue;
681 }
682
683 const struct genlmsghdr* genl = (const struct genlmsghdr*) NLMSG_DATA(nlh);
684 size_t attrRemaining = nlh->nlmsg_len - NLMSG_HDRLEN - GENL_HDRLEN;
685 for (const struct nlattr* attr = (const struct nlattr*) ((const char*) genl + GENL_HDRLEN);
686 ffWifiNlAttrOk(attr, attrRemaining);
687 attr = ffWifiNlAttrNext(attr, &attrRemaining)) {
688 if ((attr->nla_type & NLA_TYPE_MASK) != NL80211_ATTR_BSS) {
689 continue;
690 }
691
692 if (!ffWifiIsBssAssociated(attr)) {
693 continue;
694 }
695
696 ffWifiParseBssAttr(attr, item);
697 ffStrbufSetStatic(&item->conn.status, "connected");
698 return true;
699 }
700 }
701 }
702
703 return false;
704}
705
706static void ffWifiParseStationInfo(const struct nlattr* staInfoAttr, FFWifiResult* item) {
707 size_t remaining = ffWifiNlAttrPayload(staInfoAttr);
708 for (const struct nlattr* attr = (const struct nlattr*) ffWifiNlAttrData(staInfoAttr);
709 ffWifiNlAttrOk(attr, remaining);
710 attr = ffWifiNlAttrNext(attr, &remaining)) {
711 uint16_t type = (uint16_t) (attr->nla_type & NLA_TYPE_MASK);
712 size_t payload = ffWifiNlAttrPayload(attr);
713
714 if (type == NL80211_STA_INFO_SIGNAL && payload >= sizeof(uint8_t) && item->conn.signalQuality == -DBL_MAX) {
715 int rssi = (int8_t) *(const uint8_t*) ffWifiNlAttrData(attr);
716 item->conn.signalQuality = ffRssiToSignalQuality(rssi);
717 } else if (type == NL80211_STA_INFO_TX_BITRATE && (item->conn.txRate == -DBL_MAX || item->conn.channelWidth == 0)) {
718 double tx = ffWifiParseBitrateFromRateInfo(attr, &item->conn.protocol, &item->conn.channelWidth);
719 if (tx != -DBL_MAX) {
720 item->conn.txRate = tx;
721 }
722 } else if (type == NL80211_STA_INFO_RX_BITRATE && (item->conn.rxRate == -DBL_MAX || item->conn.channelWidth == 0)) {
723 double rx = ffWifiParseBitrateFromRateInfo(attr, &item->conn.protocol, &item->conn.channelWidth);
724 if (rx != -DBL_MAX) {
725 item->conn.rxRate = rx;
726 }
727 }
728 }
729}
730
731static bool ffWifiFetchStationInfo(FFWifiNlContext* ctx, FFWifiResult* item, uint32_t ifIndex) {
732 struct {
733 struct nlmsghdr nlh;
734 struct genlmsghdr genl;
735 char attrs[32];
736 } req = {
737 .nlh = {
738 .nlmsg_len = NLMSG_LENGTH(sizeof(struct genlmsghdr)),
739 .nlmsg_type = ctx->nl80211FamilyId,
740 .nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP | NLM_F_ACK,
741 .nlmsg_seq = ++ctx->seq,
742 .nlmsg_pid = ctx->portId,
743 },
744 .genl = {
745 .cmd = NL80211_CMD_GET_STATION,
746 .version = 0, // nl80211 command version
747 },
748 };
749
750 if (!ffWifiNlAppendAttr(&req.nlh, sizeof(req), NL80211_ATTR_IFINDEX, &ifIndex, sizeof(ifIndex))) {
751 FF_DEBUG("Failed to build nl80211 station request");
752 return false;
753 }
754
755 struct sockaddr_nl addr = {
756 .nl_family = AF_NETLINK,
757 };
758
759 ssize_t sent = sendto(ctx->sockFd, &req, req.nlh.nlmsg_len, 0, (struct sockaddr*) &addr, sizeof(addr));
760 if (sent != (ssize_t) req.nlh.nlmsg_len) {
761 FF_DEBUG("Failed to send nl80211 station request");
762 return false;
763 }
764
765 uint8_t buffer[8192];
766 bool gotStation = false;
767 while (true) {
768 ssize_t received = recvfrom(ctx->sockFd, buffer, sizeof(buffer), 0, nullptr, nullptr);
769 if (received < 0) {
770 FF_DEBUG("Failed to receive nl80211 station reply: %s", strerror(errno));
771 return gotStation;
772 }
773
774 for (const struct nlmsghdr* nlh = (const struct nlmsghdr*) buffer;
775 NLMSG_OK(nlh, received);
776 nlh = NLMSG_NEXT(nlh, received)) {
777 if (nlh->nlmsg_seq != req.nlh.nlmsg_seq) {
778 continue;
779 }
780
781 if (nlh->nlmsg_type == NLMSG_DONE) {
782 return gotStation;
783 }
784
785 if (nlh->nlmsg_type == NLMSG_ERROR) {
786 const struct nlmsgerr* err = (const struct nlmsgerr*) NLMSG_DATA(nlh);
787 if (err->error != 0) {
788 FF_DEBUG("nl80211 station request failed: %s", strerror(-err->error));
789 }
790 return gotStation;
791 }
792
793 if (nlh->nlmsg_type != ctx->nl80211FamilyId) {
794 continue;
795 }
796
797 const struct genlmsghdr* genl = (const struct genlmsghdr*) NLMSG_DATA(nlh);
798 size_t attrRemaining = nlh->nlmsg_len - NLMSG_HDRLEN - GENL_HDRLEN;
799 for (const struct nlattr* attr = (const struct nlattr*) ((const char*) genl + GENL_HDRLEN);
800 ffWifiNlAttrOk(attr, attrRemaining);
801 attr = ffWifiNlAttrNext(attr, &attrRemaining)) {
802 if ((attr->nla_type & NLA_TYPE_MASK) != NL80211_ATTR_STA_INFO) {
803 continue;
804 }
805
806 ffWifiParseStationInfo(attr, item);
807 gotStation = true;
808 }
809 }
810 }
811}
812
813static const char* detectWithNetlink(FFWifiNlContext* ctx, FFWifiResult* item, uint32_t ifIndex) {
814 if (ctx->sockFd < 0) {
815 if (ctx->sockFd == -1) {
816 if (!ffWifiNlInit(ctx)) {
817 FF_DEBUG("Failed to initialize netlink context, skipping");
818 ctx->sockFd = -2; // sentinel: permanent netlink failure, don't retry
819 return "Netlink initialization failed";
820 }
821 } else {
822 FF_DEBUG("Netlink socket is not available, skipping");
823 return "Netlink socket unavailable";
824 }
825 }
826
827 FF_DEBUG("Starting netlink wifi detection for interface %s", item->inf.description.chars);
828 if (ffWifiFetchScanInfo(ctx, item, ifIndex)) {
829 FF_DEBUG("found associated BSS: %s", item->conn.ssid.chars);
830 ffStrbufSetStatic(&item->conn.status, "connected");
831 ffWifiFetchInterfaceInfo(ctx, item, ifIndex);
832 ffWifiFetchStationInfo(ctx, item, ifIndex);
833 if (!item->conn.protocol.length && item->conn.txRate != -DBL_MAX) {
834 FF_DEBUG("nl80211 station info did not include MCS family fields");
835 }
836 } else {
837 FF_DEBUG("No associated BSS found");
838 ffStrbufSetStatic(&item->conn.status, "disconnected");
839 }
840
841 FF_DEBUG("Netlink wifi detection completed");
842 return nullptr;
843}
844typedef struct FFWifiIcContext {
845 int sockFd;
846} FFWifiIcContext;
847
848static const char* detectWithIoctl(FFWifiIcContext* ctx, FFWifiResult* item, char ifName[static IFNAMSIZ]) {
849 int sock = -1;
850 if (ctx->sockFd < 0) {
851 if (ctx->sockFd == -1) {
852 sock = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0);
853 if (sock < 0) {
854 FF_DEBUG("Failed to initialize ioctl context, skipping: %s", strerror(errno));
855 ctx->sockFd = -2; // sentinel: permanent ioctl failure, don't retry
856 return "socket() failed";
857 }
858 ctx->sockFd = sock;
859 } else {
860 FF_DEBUG("Ioctl socket is not available, skipping");
861 return "ioctl socket unavailable";
862 }
863 } else {
864 sock = ctx->sockFd;
865 }
866
867 FF_DEBUG("Starting ioctl wifi detection for interface %s", ifName);
868 struct iwreq iwr = {};
869 strcpy(iwr.ifr_name, ifName);
870
871 if (!item->conn.ssid.length) {
872 FF_DEBUG("Getting SSID via ioctl");
873 ffStrbufEnsureFree(&item->conn.ssid, IW_ESSID_MAX_SIZE);
874 iwr.u.essid.pointer = (caddr_t) item->conn.ssid.chars;
875 iwr.u.essid.length = IW_ESSID_MAX_SIZE + 1;
876 iwr.u.essid.flags = 0;
877 if (ioctl(sock, SIOCGIWESSID, &iwr) >= 0) {
878 ffStrbufSetStatic(&item->conn.status, "connected");
879 ffStrbufRecalculateLength(&item->conn.ssid);
880 FF_DEBUG("SSID: %s", item->conn.ssid.chars);
881 } else {
882 FF_DEBUG("Failed to get SSID via ioctl: %s", strerror(errno));
883 }
884 }
885
886 if (!item->conn.protocol.length) {
887 FF_DEBUG("Getting protocol name via ioctl");
888 if (ioctl(sock, SIOCGIWNAME, &iwr) >= 0) {
889 char* token = iwr.u.name;
890 if (ffStrStartsWithIgnCase(iwr.u.name, "IEEE ")) {
891 token += strlen("IEEE ");
892 }
893 if (ffStrStartsWith(token, "802.11")) {
894 token += strlen("802.11");
895 if (*token) {
896 if (*token == ' ') {
897 token++;
898 }
899 for (char* c = token; *c; ++c) {
900 if (*c >= 'A' && *c <= 'Z') {
901 *c += 'a' - 'A';
902 }
903 }
904 if (ffStrEquals(token, "n")) {
905 ffStrbufSetStatic(&item->conn.protocol, "802.11n (Wi-Fi 4)");
906 } else if (ffStrEquals(token, "ac")) {
907 ffStrbufSetStatic(&item->conn.protocol, "802.11ac (Wi-Fi 5)");
908 } else if (ffStrEquals(token, "ax")) {
909 ffStrbufSetStatic(&item->conn.protocol, "802.11ax (Wi-Fi 6)");
910 } else if (ffStrEquals(token, "be")) {
911 ffStrbufSetStatic(&item->conn.protocol, "802.11be (Wi-Fi 7)");
912 } else if (ffStrEquals(token, "bn")) {
913 ffStrbufSetStatic(&item->conn.protocol, "802.11bn (Wi-Fi 8)");
914 } else {
915 ffStrbufSetStatic(&item->conn.protocol, "802.11");
916 ffStrbufAppendS(&item->conn.protocol, token);
917 }
918 }
919 }
920 FF_DEBUG("Protocol: %s", item->conn.protocol.length ? item->conn.protocol.chars : "(unknown)");
921 } else {
922 FF_DEBUG("Failed to get protocol name via ioctl: %s", strerror(errno));
923 }
924 }
925
926 if (!item->conn.bssid.length) {
927 FF_DEBUG("Getting BSSID via ioctl");
928 if (ioctl(sock, SIOCGIWAP, &iwr) >= 0) {
929 for (int i = 0; i < 6; ++i) {
930 ffStrbufAppendF(&item->conn.bssid, "%.2X:", (uint8_t) iwr.u.ap_addr.sa_data[i]);
931 }
932 ffStrbufTrimRight(&item->conn.bssid, ':');
933 FF_DEBUG("BSSID: %s", item->conn.bssid.chars);
934 } else {
935 FF_DEBUG("Failed to get BSSID via ioctl: %s", strerror(errno));
936 }
937 }
938
939 if (item->conn.txRate == -DBL_MAX) {
940 FF_DEBUG("Getting bitrate via ioctl");
941 if (ioctl(sock, SIOCGIWRATE, &iwr) >= 0) {
942 if (iwr.u.bitrate.value > 0) {
943 item->conn.txRate = iwr.u.bitrate.value / 1000000.; // bps => Mbps
944 FF_DEBUG("TX bitrate: %.2f Mbps", item->conn.txRate);
945 } else {
946 FF_DEBUG("Bitrate value is zero or negative, ignoring");
947 }
948 } else {
949 FF_DEBUG("Failed to get bitrate via ioctl: %s", strerror(errno));
950 }
951 }
952
953 if (item->conn.frequency == 0 && item->conn.channel == 0) {
954 FF_DEBUG("Getting frequency via ioctl");
955 if (ioctl(sock, SIOCGIWFREQ, &iwr) >= 0) {
956 if (iwr.u.freq.e == 0 && iwr.u.freq.m <= 1000) { // kernel may return direct channel number
957 item->conn.channel = (uint16_t) iwr.u.freq.m;
958 FF_DEBUG("Direct channel value: %u", item->conn.channel);
959 } else {
960 // convert it to MHz
961 while (iwr.u.freq.e < 6) { // normalize exponent to 10^6 (MHz)
962 iwr.u.freq.m /= 10;
963 iwr.u.freq.e++;
964 }
965 while (iwr.u.freq.e > 6) { // normalize exponent to 10^6 (MHz)
966 iwr.u.freq.m *= 10;
967 iwr.u.freq.e--;
968 }
969 item->conn.frequency = (uint16_t) iwr.u.freq.m;
970 item->conn.channel = ffWifiFreqToChannel(item->conn.frequency);
971 FF_DEBUG("Frequency: %u MHz, Channel: %u", item->conn.frequency, item->conn.channel);
972 }
973 } else {
974 FF_DEBUG("Failed to get frequency via ioctl: %s", strerror(errno));
975 }
976 }
977
978 if (item->conn.signalQuality == -DBL_MAX) {
979 FF_DEBUG("Getting signal stats via ioctl");
980 struct iw_statistics stats;
981 iwr.u.data.pointer = &stats;
982 iwr.u.data.length = sizeof(stats);
983 iwr.u.data.flags = 0;
984
985 if (ioctl(sock, SIOCGIWSTATS, &iwr) >= 0) {
986 int8_t level = (int8_t) stats.qual.level;
987 item->conn.signalQuality = level >= -50 ? 100 : level <= -100 ? 0
988 : (level + 100) * 2;
989 FF_DEBUG("Signal level: %d dBm, quality: %.0f%%", level, item->conn.signalQuality);
990 } else {
991 FF_DEBUG("Failed to get signal stats via ioctl: %s", strerror(errno));
992 }
993 }
994
995 if (!item->conn.security.length) {
996 FF_DEBUG("Getting security info via ioctl");
997 struct iw_encode_ext iwe;
998 iwr.u.data.pointer = &iwe;
999 iwr.u.data.length = sizeof(iwe);
1000 iwr.u.data.flags = 0;
1001 if (ioctl(sock, SIOCGIWENCODEEXT, &iwr) >= 0) {
1002 switch (iwe.alg) {
1003 case IW_ENCODE_ALG_WEP:
1004 ffStrbufAppendS(&item->conn.security, "WEP");
1005 FF_DEBUG("Security: WEP");
1006 break;
1007 case IW_ENCODE_ALG_TKIP:
1008 ffStrbufAppendS(&item->conn.security, "TKIP");
1009 FF_DEBUG("Security: TKIP");
1010 break;
1011 case IW_ENCODE_ALG_CCMP:
1012 ffStrbufAppendS(&item->conn.security, "CCMP");
1013 FF_DEBUG("Security: CCMP");
1014 break;
1015 case IW_ENCODE_ALG_PMK:
1016 ffStrbufAppendS(&item->conn.security, "PMK");
1017 FF_DEBUG("Security: PMK");
1018 break;
1019 case IW_ENCODE_ALG_AES_CMAC:
1020 ffStrbufAppendS(&item->conn.security, "CMAC");
1021 FF_DEBUG("Security: CMAC");
1022 break;
1023 default:
1024 ffStrbufAppendF(&item->conn.security, "Unknown (%d)", (int) iwe.alg);
1025 FF_DEBUG("Security: Unknown (%d)", (int) iwe.alg);
1026 break;
1027 }
1028 } else {
1029 FF_DEBUG("Failed to get security info via ioctl: %s", strerror(errno));
1030 }
1031 }
1032
1033 FF_DEBUG("Ioctl wifi detection completed");
1034 return nullptr;
1035}
1036
1037const char* ffDetectWifi(FFlist* result) {
1038 FF_DEBUG("Starting wifi detection");
1039
1040 struct if_nameindex* infs = if_nameindex();
1041 if (!infs) {
1042 FF_DEBUG("if_nameindex failed: %s", strerror(errno));
1043 return "if_nameindex() failed";
1044 }
1045
1046 FFWifiNlContext nl = { .sockFd = -1 };
1047 FFWifiIcContext ic = { .sockFd = -1 };
1048
1049 FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
1050
1051 for (struct if_nameindex* i = infs; !(i->if_index == 0 && i->if_name == nullptr); ++i) {
1052 FF_DEBUG("Checking interface: %s (index: %u)", i->if_name, i->if_index);
1053 ffStrbufSetF(&buffer, "/sys/class/net/%s/phy80211/", i->if_name);
1054 if (!ffPathExists(buffer.chars, FF_PATHTYPE_DIRECTORY)) {
1055 FF_DEBUG("Not a wifi interface (no phy80211 directory)");
1056 continue;
1057 }
1058
1059 FFWifiResult* item = FF_LIST_ADD(FFWifiResult, *result);
1060 ffStrbufInitS(&item->inf.description, i->if_name);
1061 ffStrbufInit(&item->inf.status);
1062 ffStrbufInit(&item->conn.status);
1063 ffStrbufInit(&item->conn.ssid);
1064 ffStrbufInit(&item->conn.bssid);
1065 ffStrbufInit(&item->conn.protocol);
1066 ffStrbufInit(&item->conn.security);
1067 item->conn.signalQuality = -DBL_MAX;
1068 item->conn.rxRate = -DBL_MAX;
1069 item->conn.txRate = -DBL_MAX;
1070 item->conn.channel = 0;
1071 item->conn.channelWidth = 0;
1072 item->conn.frequency = 0;
1073
1074 char operstate;
1075 ffStrbufSetF(&buffer, "/sys/class/net/%s/operstate", i->if_name);
1076 if (!ffReadFileData(buffer.chars, 1, &operstate)) {
1077 ffStrbufSetStatic(&item->inf.status, "unknown");
1078 ffStrbufSetStatic(&item->conn.status, "disconnected");
1079 continue;
1080 }
1081
1082 if (operstate == 'u') {
1083 ffStrbufSetStatic(&item->inf.status, "up");
1084
1085 detectWithNetlink(&nl, item, i->if_index);
1086 detectWithIoctl(&ic, item, i->if_name);
1087 } else {
1088 ffStrbufSetStatic(&item->conn.status, "disconnected");
1089
1090 ffStrbufSetF(&buffer, "/sys/class/net/%s/flags", i->if_name);
1091 char flags[16];
1092 ssize_t len = ffReadFileData(buffer.chars, sizeof(flags) - 1, flags);
1093 if (len <= 0) {
1094 ffStrbufSetStatic(&item->inf.status, "unknown");
1095 continue;
1096 }
1097 flags[len] = '\0';
1098
1099 unsigned flagsVal = (unsigned) strtoul(flags, nullptr, 16); // parse /sys flags as hexadecimal
1100 if (flagsVal & IFF_UP) {
1101 ffStrbufSetStatic(&item->inf.status, "up");
1102 } else {
1103 ffStrbufSetStatic(&item->inf.status, "down");
1104 }
1105 }
1106 }
1107
1108 if_freenameindex(infs);
1109 if (nl.sockFd >= 0) {
1110 close(nl.sockFd);
1111 }
1112 if (ic.sockFd >= 0) {
1113 close(ic.sockFd);
1114 }
1115
1116 FF_DEBUG("Wifi detection completed, found %u wifi interfaces", result->length);
1117 return nullptr;
1118}