使用ioctl扫描wifi信号获取信号属性的实例(二)

whowinwhowin
8 min read

使用工具软件扫描 wifi 信号是一件很平常的事情,在知晓 wifi 密码的前提下,通常我们会尽可能地连接信号质量比较好的 wifi 信号,但是如何通过编程来扫描 wifi 信号并获得这些信号的属性(比如信号强度等),却鲜有文章提及,本文在前面博文的基础上通过实例向读者介绍如何通过编程扫描 wifi 信号,并获得信号的一系列的属性,本文给出了完整的源代码,本文程序在 ubuntu 20.04 下编译测试完成,gcc 版本号 9.4.0;阅读本文并不需要对 IEEE802.11 协议有所了解,但本文实例中大量涉及链表和指针,所以本文可能不适合初学者阅读。

1 前言

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=SIOCSIWSCANwrq->ifr_name 设置为无线接口名称,wrq->u.data.pointer=NULLwrq->u.data.flags=0wrq->u.data.length=0,然后调用 ioctl();
    • 获取扫描结果时,request=SIOCGIWSCANwrq->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 为 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,不同电脑可能会不一样;

      Screenshot of wifi scanning

    • 图中红线所示部分就是信号连接质量,其表达方式为 47/70,这是什么含义呢?

    • WE(Wireless Extension) 假设信号范围为 -110dBm ~~ -40dBm,信号质量的值为信号强度 +110 得出的,这样信号质量值的范围为 0~~7047/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;

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信号获取信号属性的一个范例(一)》
  • 有意思的是起初并不知道这个字符串中会有什么内容,但把收到的内容显示出来后,发现是类似下面的内容:

    Screenshot of driver specific ascii string

  • 图中红线所示就是收到的字符串,原来这个字符串中藏着与 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 数据

    Real IE data

  • 数据与普通事件一样,符合 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 Structure

  • 每个 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 页有定义,这里摘录其中的一部分:

    Parts of IE type definition

  • 从上面定义可以看到,当 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 提供了一种非常灵活的传递信息的方法,内容非常丰富,在本文所载实例中仅就其中的几个进行了解析;
  • 还要简单介绍一下所支持传输速率的 IE,因为在实例中解析了这个 IE;
  • 根据 Element ID 的定义,当 Element ID 为 1 时,IE 的数据为该信号所支持的传输速率,其结构在 802.11-2007 文档的第 102 页有介绍:

    Supported Rates

  • 根据文档,一个 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
  • 运行截图:

    GIF of running wifi-new0scanner

欢迎订阅 『网络编程专栏』


欢迎访问我的博客:https://whowin.cn

email: hengch@163.com

donation

0
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多年经验的退休程序员,主要从事嵌入式软件开发