使用ioctl扫描wifi信号获取信号属性的实例(二)
使用工具软件扫描 wifi 信号是一件很平常的事情,在知晓 wifi 密码的前提下,通常我们会尽可能地连接信号质量比较好的 wifi 信号,但是如何通过编程来扫描 wifi 信号并获得这些信号的属性(比如信号强度等),却鲜有文章提及,本文在前面博文的基础上通过实例向读者介绍如何通过编程扫描 wifi 信号,并获得信号的一系列的属性,本文给出了完整的源代码,本文程序在 ubuntu 20.04 下编译测试完成,gcc 版本号 9.4.0;阅读本文并不需要对 IEEE802.11 协议有所了解,但本文实例中大量涉及链表和指针,所以本文可能不适合初学者阅读。
1 前言
- 在『网络编程专栏』专栏里写过一篇wifi信号扫描的文章《使用ioctl扫描wifi信号获取信号属性的一个范例(一)》,与该文相比本文所附带的实例将可以获取更多的 wifi 属性;
- 在阅读本文前,请阅读《使用ioctl扫描wifi信号获取信号属性的一个范例(一)》,并请理解范例中的程序,该文中所涉及的概念以及数据结构,本文将不再做介绍;
- 在《使用ioctl扫描wifi信号获取信号属性的一个范例(一)》中,我们使用
ioctl()
启动了 wifi 信号的扫描,并获取了 wifi 信号的 SSID、MAC地址、工作频率和工作信道,但有一些重要的信号属性并没有获得,比如:信号强度、信号质量、信号噪音以及加密方式等,本文将讨论如何获取这些属性; - 本文提供的实例的基本框架与文章《使用ioctl扫描wifi信号获取信号属性的一个范例(一)》中的基本一致;
- 本文所提供的实例中并不需要第三方库的支持,所以不需要安装任何其它支持软件和库。
2 遍历网络设备列表
- 在对无线网卡操作之前,首先要找到无线网卡的设备名,在文章《使用ioctl扫描wifi信号获取信号属性的一个范例(一)》中,使用
getifaddrs()
找到所有的网络接口,然后再用ioctl()
的 SIOCGIWNAME 命令从中找到无线网卡; - 其实我们可以从
/proc/net/dev
中找到所有的网络接口,而不必使用getifaddrs()
; - 就本文而言,需要知道的就是无线网卡的设备名,我们也可以从文件
/proc/net/wireless
文件中直接获得,这种方法更加简单一些,先来看一下这个文件中有什么内容:$ cat /proc/net/wireless Inter-| sta-| Quality | Discarded packets | Missed | WE face | tus | link level noise | nwid crypt frag retry misc | beacon | 22 wlp1s0: 0000 70. -256. -256 0 0 0 0 1 0 $
- 可以看到,这个文件的前两行是标题,从第三行起开始是无线网卡的信息,其中接口名称后面紧跟着 ": ";
这台电脑上只有一个无线网卡,其接口名称为:wlp1s0,下面程序片段从
/proc/net/wireless
中提取出无线接口的名称:FILE *fh; char buff[1024]; fh = fopen("/proc/net/wireless", "r"); if (fh != NULL) { // Skip 2 lines of header fgets(buff, sizeof(buff), fh); fgets(buff, sizeof(buff), fh); // Read each device line while (fgets(buff, sizeof(buff), fh)) { char name[IFNAMSIZ + 1]; // Skip empty lines. if ((buff[0] == '\0') || (buff[1] == '\0')) continue; // Extract interface name char *p = buff; // Skip leading spaces while (isspace(*p)) p++; char *end; end = strstr(buf, ": "); // Not found if (end == NULL) continue; // Copy memcpy(name, p, (end - p)); name[end - p] = '\0'; printf("The wireless interface name is %s\n", name); } fclose(fh); } else { printf("Can't open file /proc/net/wireless\n"); }
3 信号质量、信号强度、信号噪音的获取
通过阅读文章《使用ioctl扫描wifi信号获取信号属性的一个范例(一)》,应该可以了解如何使用
ioctl()
启动 wifi 信号的扫描并获得扫描结果,在此简单回顾一下:struct iwreq
定义,其中struct iwreq_data
见下面定义;struct iwreq { union { char ifrn_name[IFNAMSIZ]; /* if name, e.g. "eth0" */ } ifr_ifrn; /* Data part (defined just above) */ union iwreq_data u; };
ioctl()
的调用方式:int ioctl(int socket, unsigned long request, struct iwreq *wrq)
- 启动 wifi 扫描时,
request=SIOCSIWSCAN
,wrq->ifr_name
设置为无线接口名称,wrq->u.data.pointer=NULL
,wrq->u.data.flags=0
,wrq->u.data.length=0
,然后调用ioctl()
; - 获取扫描结果时,
request=SIOCGIWSCAN
,wrq->u.data.pointer=接收数据缓冲区指针
,wrq->u.data.length=接收缓冲区的长度
,wrq->u.data.flags=0
,然后调用ioctl()
,调用时,wrq->ifr_name
也是要设置为无线接口名称的,只是因为在启动扫描时已经设置过,所以这里通常不需要再设置; - 如果返回的扫描结果数据比较大,设置的接收缓冲区不够用,
ioctl()
将返回 -1,errno 为 E2BIG,此时应该重新为缓冲区分配内存并再次调用ioctl()
获取扫描结果; - 如果在使用
ioctl()
获取扫描结果时,扫描还没有完成,ioctl()
将返回 -1,errno 为 EAGAIN,此时应该等待一会再次调用ioctl()
获取扫描结果; - 正常获取扫描结果时,
wreq->u.data.falgs
将被设为 1(调用时为 0),wreq->u.data.length
中为返回数据的实际长度,返回的数据被存放在wreq->u.data.pointer
指向的数据缓冲区中;
返回的扫描结果是一个数据流(stream),其中包含着许多的事件(event),每个 event 包含着一个属性,返回的扫描结果数据符合
struct iw_event
,每个 event 数据也符合struct iw_event
,这个结构定义在wireless.h
中:union iwreq_data { /* Config - generic */ char name[IFNAMSIZ]; /* Name : used to verify the presence of wireless extensions. * Name of the protocol/provider... */ struct iw_point essid; /* Extended network name */ struct iw_param nwid; /* network id (or domain - the cell) */ struct iw_freq freq; /* frequency or channel : * 0-1000 = channel * > 1000 = frequency in Hz */ struct iw_param sens; /* signal level threshold */ struct iw_param bitrate; /* default bit rate */ struct iw_param txpower; /* default transmit power */ struct iw_param rts; /* RTS threshold threshold */ struct iw_param frag; /* Fragmentation threshold */ __u32 mode; /* Operation mode */ struct iw_param retry; /* Retry limits & lifetime */ struct iw_point encoding; /* Encoding stuff : tokens */ struct iw_param power; /* PM duration/timeout */ struct iw_quality qual; /* Quality part of statistics */ struct sockaddr ap_addr; /* Access point address */ struct sockaddr addr; /* Destination address (hw/mac) */ struct iw_param param; /* Other small parameters */ struct iw_point data; /* Other large parameters */ }; struct iw_event { __u16 len; /* Real length of this stuff */ __u16 cmd; /* Wireless IOCTL */ union iwreq_data u; /* IOCTL fixed payload */ }
struct iw_event
的前两个字段,len 表明了这个 event 的数据长度,cmd 表明了这个 event 的类别,不同的 event,字段 u 中对应的数据结构也不相同;- 在文章《使用ioctl扫描wifi信号获取信号属性的一个范例(一)》中,我们只解析了三个 cmd,SIOCGIWAP(MAC地址)、SIOCGIWESSID(SSID) 和 SIOCGIWFREQ(Frequence 和 Channel):
- 当 cmd 为 SIOCGIWAP 时,字段 u 对应的数据结构为
struct sockaddr
,MAC 地址存放在u.addr.sa_data
的前 6 个字节中; - 当 cmd 为 SIOCGIWESSID 时,字段 u 对应的数据结构为
struct iw_essid
,这个是自己定义的,在上面 union 中并没有列出,这个定义可以使问题更加简单一些; - 当 cmd 为 SIOCGIWFREQ 时,字段 u 对应的数据结构为
struct iw_freq freq
,当计算出的频率大于 1000 时,则结果为 wifi 信号的工作频率,否则为该信号的工作信道;
- 当 cmd 为 SIOCGIWAP 时,字段 u 对应的数据结构为
当 cmd 为 IWEVQUAL,获得的信息为统计数据的信号质量部分(Quality part of statistics),这部分数据中包括信号质量、信号强度、信号噪音等信息;
- 此时字段 u 对应的数据结构为
struct iw_quality
,在wireless.h
中定义,如下:struct iw_quality { __u8 qual; /* link quality */ __u8 level; /* signal level (dBm) */ __u8 noise; /* noise level (dBm) */ __u8 updated; /* Flags to know if updated */ };
qual 字段为信号连接质量,先看一下使用无线工具
sudo iwlist [wifname] scan
扫描信号看到的信号连接质量是什么样子的,[wifname] 是无线网络接口的名称,在我的电脑上是wlp1s0
,不同电脑可能会不一样;图中红线所示部分就是信号连接质量,其表达方式为
47/70
,这是什么含义呢?- WE(Wireless Extension) 假设信号范围为
-110dBm ~~ -40dBm
,信号质量的值为信号强度 +110 得出的,这样信号质量值的范围为0~~70
,47/70
的 47 表示信号连接质量值为 47,70 标志信号质量值最大为 70; - level 字段为信号的强度,其单位为 dBm(分贝毫瓦),通常用于表示无线电信号的功率,如上所述,正常情况下,
level + 110 = qual
- noise 字段为信号背景噪音的强度,这个字段在我的电脑上并不支持,如何判断是否支持,请看下面对 updated 字段的介绍;
updated 字段是一个位掩码(bit mask),在
wireless.h
中定义了该字段每个 bit 表达的含义,如下:/* Statistics flags (bitmask in updated) */ #define IW_QUAL_QUAL_UPDATED 0x01 /* Value was updated since last read */ #define IW_QUAL_LEVEL_UPDATED 0x02 #define IW_QUAL_NOISE_UPDATED 0x04 #define IW_QUAL_ALL_UPDATED 0x07 #define IW_QUAL_DBM 0x08 /* Level + Noise are dBm */ #define IW_QUAL_QUAL_INVALID 0x10 /* Driver doesn't provide value */ #define IW_QUAL_LEVEL_INVALID 0x20 #define IW_QUAL_NOISE_INVALID 0x40 #define IW_QUAL_RCPI 0x80 /* Level + Noise are 802.11k RCPI */ #define IW_QUAL_ALL_INVALID 0x70
- 如果网卡驱动程序不支持 quality、level 或者 noise,则 IW_QUAL_QUAL_INVALID、IW_QUAL_LEVEL_INVALID 或者 IW_QUAL_NOISE_INVALID 对应的 bit 就会被置 1;
- 如果自上次读取 quality、level 或者 noise 后,数据已经被网卡驱动程序再次更新,则 IW_QUAL_QUAL_UPDATED、IW_QUAL_LEVEL_UPDATED 或者 IW_QUAL_NOISE_UPDATED 对应的 bit 会被置 1;
- 此时字段 u 对应的数据结构为
4 无线信号的工作方式
- wireless.h 中定义了 8 中无线信号的工作方式:
/* Modes of operation */ #define IW_MODE_AUTO 0 /* Let the driver decides */ #define IW_MODE_ADHOC 1 /* Single cell network */ #define IW_MODE_INFRA 2 /* Multi cell network, roaming, ... */ #define IW_MODE_MASTER 3 /* Synchronisation master or Access Point */ #define IW_MODE_REPEAT 4 /* Wireless Repeater (forwarder) */ #define IW_MODE_SECOND 5 /* Secondary master/repeater (backup) */ #define IW_MODE_MONITOR 6 /* Passive monitor (listen only) */ #define IW_MODE_MESH 7 /* Mesh (IEEE 802.11s) network */
- 我们扫描到的信号,大多数应该是 Master;
- 当使用
ioctl()
扫描无线信号时,返回的struct iw_event
中当 cmd 字段为 SIOCGIWMODE,该事件为工作方式; - 当 cmd 为 SIOCGIWMODE时,
struct iw_event
中的u.mode
为该无线信号的工作方式;
5 无线信号支持的传输速率
- 当使用
ioctl()
扫描无线信号时,返回的struct iw_event
中当 cmd 字段为 SIOCGIWRATE 时,该事件中的数据为信号支持的传输速率; - 一个信号支持的传输速率通常有很多种,所以这个数据通常也是有很多组的,下面是一组实际的数据(16进制数):
0000: 28 00 21 8B 00 00 00 00 80 8D 5B 00 00 00 00 00 0010: 00 1B B7 00 00 00 00 00 00 36 6E 01 00 00 00 00 0020: 00 6C DC 02 00 00 00 00
- 按照
struct iw_event
的定义,前两个字节是这个 event 的长度,为 0x0028,也就是 40 个字节,后面两个字节 0x8B21 是 cmd 字段,0x8b21 也就是 SIOCGIWRATE(见wireless.h
中的定义),所以这个 event 的数据是传输速率; - 当收到的是传输速率时,
struct iw_event
中的u.bitrate
为对应的传输率的数据结构(见第 3 节关于union iwreq_data
的介绍),u.bitrate
是一个struct iw_param
,其定义如下(见 wireless.h):struct iw_param { __s32 value; /* The value of the parameter itself */ __u8 fixed; /* Hardware should not use auto select */ __u8 disabled; /* Disable the feature */ __u16 flags; /* Various specifc flags (if any) */ };
- 根据其定义,其中的
u.bitrate.value
字段即为传输速率; - 如上数据,一个 wifi 信号通常都是支持多种传输速率的,这时可以将数据部分定义成一个
struct iw_param
的结构数组,并通过 event 的长度和struct iw_param
的长度计算得出这个 event 中有多少组传输速率的数据,如下:...... struct iw_event *evp = data; int rate_count = (evp.len - IW_EV_LCP_LEN) / sizeof(struct iw_param); struct iw_param *rates = &evp->u.bitrate; int i = 0; for (i = 0; i < rate_count; ++i) { ...... printf("Bit rate: %d Mb/s\n", rates[i].value / 1000000); }
- 其中 IW_EV_LCP_LEN 为
struct iw_event
中结构头(len 和 cmd 字段)长度(包含为对齐而填充的空字符),请见文章《使用ioctl扫描wifi信号获取信号属性的一个范例(一)》中的相关解释; - 当一个 WiFi 信号支持的传输速率比较多时,可能会收到两个 SIOCGIWRATE 事件(也许会有多个,但我没有遇到过),每个事件中的速率是不一样的,所以都需要处理,不能忽略任何一个事件;
6 beacon 相关信息
- 当使用
ioctl()
扫描无线信号时,返回的struct iw_event
中当 cmd 字段为 IWEVCUSTOM 时,该事件在wireless.h
中定义为 "Driver specific ascii string",意为:驱动程序特定的 ASCII 字符串; - AP 要周期性地在 wifi 上广播 beacon 帧,用于在网络上宣告一个 wifi 信号的存在,之所以可以扫描到 wifi 信号就是因为收到了 beacon 帧;
- beacon 帧并不是本文要讨论的问题,本文不会展开讨论;
- 回到 WiFi 信号的扫描主题上,
wireless.h
中并没有定义一个事件可以收到有关 beacon 帧的信息,但是我们在事件 IWEVCUSTOM 中看到了 beacon 信息; - IWEVCUSTOM 事件中的这个字符串的结构与 essid 是一样的,所以可以用相同的方法提取,可以参考文章《使用ioctl扫描wifi信号获取信号属性的一个范例(一)》;
有意思的是起初并不知道这个字符串中会有什么内容,但把收到的内容显示出来后,发现是类似下面的内容:
图中红线所示就是收到的字符串,原来这个字符串中藏着与 beacon 帧有关的信息,Last beacon 是最后收到的 beacon 帧时间,TSF 是 Timing Synchronization Function 的缩写,是 beacon 帧中的一个字段,用于 AP 和 Station 之间同步时间;
- 这个事件与 essid 还是有所不同,一个 WiFi 信号只有一个 essid,所以 SIOCGIWESSID 事件只会收到一次,但是 IWEVCUSTOM 有可能收到多次,而每次收到的字符串是不相同的,所以接收 IWEVCUSTOM 事件比接收 essid 还是要麻烦一些,在本文实例源程序中这部分有详细的中文注释;
7 Information Elements
- 当使用
ioctl()
扫描无线信号时,返回的struct iw_event
中当 cmd 字段为 IWEVGENIE 时,该事件在wireless.h
中定义为 "Generic IE (WPA, RSN, WMM, ..)",意为通用 IE(Information Elements); - IE 可以提供非常多的信息,本文中仅就 SSID 和速率信息进行示范性的解析;
- 首先,这个事件在大多数情况下都是提供多个 IE,所以事件通常比较长,而且长度并不固定;
我们先来看一个实际收到的 IE 数据
数据与普通事件一样,符合
struct iw_event
结构,前两个字节是事件数据的长度,0x00D2(十进制 210)字节,第 3、4 字节为事件类型,0x8C05(IWEVGENIE) 表示这个事件中是 IE 数据;- 前四个字节组成了
struct iw_event
的头信息,紧跟着的 4 个字节为按 64 位对齐填充的字符,第 9、10 字节为实际 IE 数据所占的长度 ie_length,第 11-16 字节是对齐填充字节; - 所以从第 17 个字节(第 2 行)开始才是真正的 IE 数据,正是因为这个原因,通常 ie_length(第 9、10 字节) 比事件的总长度(第 1、2 字节)小 16(0x10),在这组实际数据中,事件长度为 0x00D2(十进制 210),而 IE 数据的长度为 0x00C2(十进制 194),要注意的是,事件长度是包含
struct iw_event
头信息的 8 个字节,而 IE 数据长度是不包括头信息的长度(仅为 IE 数据长度); - IEEE 标准
802.11-2007
文档中定义了 IE 的结构,该文档的下载地址如下: 该标准的
7.3.2 Information elements
定义了 IE 的结构:每个 IE 符合
type-length-value
格式,即:第 1 个字节表示 IE 类型 Element ID,第 2 个字节表示数据的长度 Length,后面若干字节为实际数据 Information,数据长度为 Length;struct ieee80211_ie { unsigned char eid; unsigned char len; unsigned char data[0]; };
- 当收到一个 IE 数据的事件时,可以考虑如下定义:
struct iw_event_ie { unsigned short len; unsigned short cmd; #ifdef __x86_64__ // 64位系统按8字节对齐 unsigned short __attribute((aligned(8)))ie_len; struct ieee80211_ie __attribute((aligned(8)))ie[0]; #else // 32位系统按4字节对齐 unsigned short __attribute((aligned(4)))ie_len; struct ieee80211_ie __attribute((aligned(4)))ie[0]; #endif };
- 字段 len 和 cmd 与
struct iw_event
是一致的,ie_len 字段将得到 ie 这个字段所对应的数据的总长度,struct ieee80211_ie
则定义了每个 IE 的结构,具体有多少个 IE 则需要在遍历 IE 时根据 ie_len 字段的值做出判断; struct ieee80211_ie
中的 eid(Element ID) 字段定义了这个 IE 的类型,这些类型在802.11-2007
的文档中第 100 页有定义,这里摘录其中的一部分:从上面定义可以看到,当 Element ID 为 0 时,其信息内容为 SSID,以上面的实际数据为例,数据的第二行,也就是第 1 个 IE 的数据为:
00 07 31 35 2D 31 31 30 31
- 按照 IE 的格式定义,Element ID 为 0,表示其信息为 SSID,数据长度 Length 为 7,所以后面的 7 个字节
31 35 2D 31 31 30 31
为实际数据; - 查 ASCII 表,这个数据其实就是 "15-1101",这就是这个信号的 SSID
- 按照 IE 的格式定义,Element ID 为 0,表示其信息为 SSID,数据长度 Length 为 7,所以后面的 7 个字节
- IE 提供了一种非常灵活的传递信息的方法,内容非常丰富,在本文所载实例中仅就其中的几个进行了解析;
- 还要简单介绍一下所支持传输速率的 IE,因为在实例中解析了这个 IE;
根据 Element ID 的定义,当 Element ID 为 1 时,IE 的数据为该信号所支持的传输速率,其结构在
802.11-2007
文档的第 102 页有介绍:根据文档,一个 IE 中最多描述 8 个传输速率,单位为 500 kb/s,每个速率占用 1 个字节,其最高位(bit 7)有其它意义,(bit 0 - 6) 表示速率值,所以在计算时要将 bit 7 过滤掉;
- IE 信息会有空信息,也就是长度字段为 0,这种 IE 通常只有 2 个字节,没有意义,在实际解析中要过滤掉,比如:
00 00
8 实例
- 完整的源代码,文件名:wifi-new-scanner.c(点击文件名下载源程序),请务必使用 UTF-8 字符集,否则源程序中的中文注释为乱码;
- 阅读这个源码最好先阅读文章《使用ioctl扫描wifi信号获取信号属性的一个范例(一)》,并搞懂其中的源代码;
- 简述一下程序的基本流程:
- 通过读取文件
/proc/net/wireless
获取无线网络接口的列表(在文章《使用ioctl扫描wifi信号获取信号属性的一个范例(一)》中使用的是另一种方法); - 使用
ioctl()
向无线网络接口发出 wifi 信号扫描指令; - 使用
ioctl()
获取扫描结果,根据返回的结果生成事件链表; - 从事件链表中分析每一个事件,从中解析出每个信号的属性,生成 ap 链表;
- 如果有 IE 事件,还要在 AP 链表中生成 IE 链表;
- 从 AP 链表中解析出信息并显示出来;
- 通过读取文件
- 比较《使用ioctl扫描wifi信号获取信号属性的一个范例(一)》中的实例,本文除了解析出 MAC 地址、ESSID、工作频率、工作信道外,还可以解析出信号质量、工作模式、信号支持的传输速率、驱动程序字符串以及 Information Elements;
- 源程序中有比较详细的注释请自行参考;
- 其中比较复杂的是 IE 的解析,IE 的内容极其丰富,作为示范,本例仅解析了 SSID 和速率,需要更多信息的读者可以查阅 802.11-2007 文档;
- 编译:
gcc -Wall wifi-new-scanner.c -o wifi-new-scanner -lm
- 运行:
sudo ./wifi-new-scanner
运行截图:
欢迎订阅 『网络编程专栏』
欢迎访问我的博客:https://whowin.cn
email: hengch@163.com
Subscribe to my newsletter
Read articles from whowin directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
whowin
whowin
一枚有30多年经验的退休程序员,主要从事嵌入式软件开发