Pages

Monday, March 7, 2011

Wireless Scanning with Erlang

Scanning for Wireless Access Points with Erlang

The Linux wireless LAN network interface (802.11) uses a socket/ioctl interface to communicate with the kernel. I am going to go over building an Erlang interface for initiating scanning and retrieving the scan results.

All of this code was run on Ubuntu 10.04 using constants defined in wireless.22.h of the wireless tools for Linux.

The Scanning Process

The scan process works as follows:

  1. Open a datagram socket

  2. Allocate a iwreq structure. The structure contains the device name used for scanning and a pointer to an optional user allocated buffer containing an ESSID. According to the man page, specifying an ESSID with some drivers allows hidden networks to be found.

  3. Call an ioctl on the socket with the request set to SIOCSIWSCAN (0x8B18).

  4. Allocate another iwreq structure containing a pointer to a user allocated buffer with enough space to hold the response.

  5. Call a second ioctl on the socket with the request set to SIOCGIWSCAN (0x8B19)

  6. When the ioctl returns successfully, both the iwreq structure and the user allocated buffer are updated. The iwreq structure contains the actual size of the data held in the buffer and the buffer holds the set of events.

  • length:2 bytes
  • command:2 bytes
  • data:(length-4)

Examples of data types are the ESSID, BSSID, channel, etc.

The iwreq Structure

The iwreq structure is composed of 2 unions:

#define IFNAMSIZ 16

struct  iwreq
{
    union
    {
        char    ifrn_name[IFNAMSIZ];    /* if name, e.g. "eth0" */
    } ifr_ifrn;

    /* Data part (defined just above) */
    union   iwreq_data  u;
};

The iwreq_data union is constrained to 32 bytes and composed of the following:

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 */
};

Some of the data may be larger than can be held in the union. The iw_point structure, for example, contains a pointer to user allocated memory that can be used to hold either arguments to be read by the kernel or data returned by the kernel.

The iw_point structure looks like:

struct  iw_point
{
    void __user   *pointer; /* Pointer to the data  (in user space) */
    __u16     length;       /* number of fields or size in bytes */
    __u16     flags;        /* Optional params */
};

The SIOCSIWSCAN ioctl

To initiate the scan, we call an ioctl on a socket file descriptor. Any socket type can be used. The structure we pass to ioctl contains the interface name. For example, to scan using the default ESSID:

struct iwreq iwr = {0};

(void)memcpy(iwr.ifr_ifrn.ifrn_name, "wlan0", IFNAMSIZ);
iwr.u.data.pointer = NULL;
iwr.u.data.length = 0;
iwr.u.data.flags = 0;

To scan a specific ESSID without disassociating from the current AP:

#define IW_SCAN_THIS_ESSID 16#0002

struct iwreq iwr = {0};
char *essid = NULL;

(void)memcpy(iwr.ifr_ifrn.ifrn_name, "wlan0", IFNAMSIZ);
essid = strdup("linksys");
if (essid == NULL)
    err(EXIT_FAILURE, "strdup");

iwr.u.data.pointer = essid;
iwr.u.data.length = strlen(essid)+1;
iwr.u.data.flags |= IW_SCAN_THIS_ESSID;

The ioctl is called using the iwreq structure and indicates an error by a non-zero return value, with the reason held in errno.

ioctl(socket, SIOCSIWSCAN, &iwr);

The iwreq structure is not modified upon return (or at least, we do not care if has changed).

Common errors are:

  • EAGAIN: the scan has not completed. We will need to wait and poll the socket after a period of time has passed.

  • EBUSY: another scan is currently in progress. Again, we will need to sleep and poll the socket later.

  • ENOTSUP: the requested device is not a wireless device

The SIOCGIWSCAN ioctl

To retrieve the results of the last scan, we issue another ioctl with the request set to SIOCGIWSCAN. The iwreq structure must point to a buffer large enough to hold the response (the list of APs).

#define BUFSZ 4096

struct iwreq iwr = {0};
char *p = NULL;

(void)memcpy(iwr.ifr_ifrn.ifrn_name, "wlan0", IFNAMSIZ);
p = calloc(BUFSZ,1);
if (p == NULL)
    err(EXIT_FAILURE, "calloc");

iwr.u.data.pointer = p;
iwr.u.data.length = BUFSZ;
iwr.u.data.flags = 0;

After the ioctl returns (it may fail for the same reasons as above):

  • the iwreq structure will be updated to hold the actual size of the data in our buffer
  • our buffer will hold the scan list

An Erlang Approach to Pointers and ioctl()'s: Resources

To make system calls that aren't supported by the Erlang VM, we will need to integrate a small amount of C code using Erlang's NIF interface. The procket library on GitHub was made to perform some low level operations on sockets. I've added some additional functions to deal with versions of ioctl() using input/output fields containing pointers to memory:

procket:alloc/1
procket:buf/1

It's easier to explain by giving an example:

Len = 16,
{ok, Struct, [Resource1, Resource2]} = procket:alloc([
        <<Len:2/native-unsigned-integer-unit:8>>,
        {ptr, Len},
        <<Flags:2/native-unsigned-integer-unit:8>>,
        {ptr, <<"some data">>}
        ]).

Where:

  • Struct: a binary that can be passed to procket:ioctl/3

This binary contains the actual pointer and so should be considered to be read-only. Modifying the binary could result in the VM crashing.

  • Resource1, Resource2: NIF resources

The first resource points to a zero'ed 16 byte buffer.

The second resource points to a 9 byte buffer initialized with the string "some data".

procket:buf/1 is used to retrieve the contents of the buffer. Here is a complete example: retrieving the list of network interfaces (usually you would just use inet:getifaddrs/0).

-module(ifconf).
-export([dev/0]).

%% Get the list of network interfaces similar to
%% inet:getifaddrs/0

-define(SIOCGIFCONF, 16#00008912).


% struct ifconf
% {   
%     int ifc_len;            /* size of buffer   */
%     union
%     {   
%         char *ifcu_buf;
%         struct ifreq *ifcu_req;
%     } ifc_ifcu;
% };

dev() ->
    Len = 8192,
    {ok, Ifconf, [Res]} = procket:alloc([
        <<Len:4/native-integer-unit:8>>,
        {ptr, Len}
        ]),

    {ok, Socket} = procket:socket(inet, dgram, 0),
    {ok, Ifconf1} = procket:ioctl(Socket, ?SIOCGIFCONF, Ifconf),
    {ok, Buf} = procket:buf(Res),

    <<N:4/native-integer-unit:8, _/binary>> = Ifconf1,
    Ifs = ifreq(Buf, N),

    procket:close(Socket),
    {ok, Ifconf, Ifconf1, Buf, Ifs}.

% struct ifreq
% {
%     #define IFHWADDRLEN 6
%     union
%     {  
%         char    ifrn_name[IFNAMSIZ];        /* if name, e.g. "en0" */
%     } ifr_ifrn;
% 
%     union {
%         struct  sockaddr ifru_addr;
%         struct  sockaddr ifru_dstaddr;
%         struct  sockaddr ifru_broadaddr;
%         struct  sockaddr ifru_netmask;
%         struct  sockaddr ifru_hwaddr;
%         short   ifru_flags;
%         int ifru_ivalue;
%         int ifru_mtu;
%         struct  ifmap ifru_map;
%         char    ifru_slave[IFNAMSIZ];   /* Just fits the size */
%         char    ifru_newname[IFNAMSIZ];
%         void *  ifru_data;
%         struct  if_settings ifru_settings;
%     } ifr_ifru;
% };

% struct sockaddr_in
% {
%     __SOCKADDR_COMMON (sin_);
%     in_port_t sin_port;         /* Port number.  */
%     struct in_addr sin_addr;        /* Internet address.  */
% 
%     /* Pad to size of `struct sockaddr'.  */
%     unsigned char sin_zero[sizeof (struct sockaddr) -
%         __SOCKADDR_COMMON_SIZE -
%         sizeof (in_port_t) - sizeof (struct in_addr)];
% };

ifreq(Buf, N) ->
    <<Buf1:N/bytes, _/binary>> = Buf,
    ifreq1(Buf1, []).

ifreq1(<<>>, Ifs) ->
    Ifs;
ifreq1(<<
    Name:16/bytes,
    _Family:16,
    _Port:16,
    IP1:8, IP2:8, IP3:8, IP4:8,
    _:64,
    Rest/binary
    >>, Ifs) ->
    [Dev, _] = binary:split(Name, <<0>>),
    ifreq1(Rest, [{Dev, {IP1,IP2,IP3,IP4}}|Ifs]).

Running a Scan Using Erlang

So finally combining all of the above, we can begin putting the code together to do an AP scan from Erlang. For brevity, I've removed some of the error handling, etc, the code is available on GitHub.

Privileges

To use this code, beam will either have to be running as root or (preferably) have the CAP_NET_ADMIN privilege. To set it:

setcap cap_net_admin=ep /path/to/beam

To check the privs have been set:

getcap /path/to/beam

To remove the privs after you're done playing:

setcap -r /path/to/beam

The code

Running it

erl -pa /path/to/procket/ebin

1> wierl:start(<<"wlan0">>).
bssid:<<1,0,0,30,74,31,75,76,0,0,0,0,0,0,0,0>>
freq:<<1,0,0,0,0,0,0,0>>
freq:<<108,9,0,0,6,0,0,0>>
qual:<<40,186,0,75>>
encode:<<0,0,0,128>>
essid:<<14,0,1,0,116,104,101,101,115,115,105,100,105,115,97,108, 105,101>>
rate:<<64,66,15,0,0,0,0,0,128,132,30,0,0,0,0,0,96,236,83,0,0,0,0,0,128,141,91,
       0,0,0,0,0,64,84,137,0,0,0,0,0,192,216,167,0,0,0,0,0,0,27,183,0,0,0,0,0,
       128,168,18,1,0,0,0,0>>
rate:<<0,54,110,1,0,0,0,0,0,81,37,2,0,0,0,0,0,108,220,2,0,0,0,0,128,249,55,3,0,
       0,0,0>>
mode:<<3,0,0,0>>
custom:<<20,0,0,0,116,115,102,61,48,48,48,48,48,48,50,57,50,48,99,98,101,49,
         102,56>>
custom:<<23,0,0,0,32,76,97,115,116,32,98,101,97,99,111,110,58,32,53,54,52,109,
         115,32,97,103,111>>
genie:<<88,0,0,0,0,19,87,105,114,101,108,101,115,115,77,105,115,115,105,115,
        115,97,117,103,97,1,8,130,132,139,12,18,150,24,36,3,1,1,7,6,67,65,32,1,
        11,30,42,1,2,50,4,48,72,96,108,150,6,0,64,150,0,20,0,221,6,0,64,150,1,
        1,4,221,5,0,64,150,3,5,221,5,0,64,150,11,9,221,5,0,64,150,20,0>>

Decoding the Binaries

Decoding the scan results is simple. For example, for those of you stalking me, from the example above:

  • the bssid looks like: <<1,0, Bytes:6/bytes, 0,0,0,0,0,0,0,0>>

Each of the 6 bytes can be printed as hex. In the example:

1> <<1,0, BSSID:6/bytes, 0,0,0,0,0,0,0,0>> = <<1,0,0,30,74,31,75,76,0,0,0,0,0,0,0,0>>.
<<1,0,0,30,74,31,75,76,0,0,0,0,0,0,0,0>>
2> lists:flatten(string:join([ io_lib:format("~.16b", [N]) || <<N:8>> <= BSSID ], ":")).
"0:1e:4a:1f:4b:4c"

What's the preceding <<1,0>>? No idea as yet :) The BSSID is a struct sockaddr:


struct sockaddr {
    sa_family_t family; /* uint16_t */
    char sa_data[14];
}
The family is set to ARPHRD_ETHER or 1 in native endian format (little in the example above). The BSSID, like a MAC address, is 6 bytes. The remaining 8 bytes are zeroes.

  • the frequency is either a native, unsigned 64-bit integer holding either the channel (usually a number from 1-11) or the frequency

In the example above, a channel is indicated by a number less than 1000:

1> <<Channel:8/native-unsigned-integer-unit:8>> = <<1,0,0,0,0,0,0,0>>.
1.

And the frequency, if it's available, can be calculated like this:

1> <<Mantissa:4/native-signed-integer-unit:8, Exponent:2/native-signed-integer-unit:8, _I:8, _Flags:8>> = <<108,9,0,0,6,0,0,0>>.
<<108,9,0,0,6,0,0,0>>
2> Mantissa*math:pow(10, Exponent).
2.412e9
  • the ESSID is prefaced by the length, 2 bytes set to 1 and the ESSID:

From the example:

1> <<Len:2/native-unsigned-integer-unit:8, 1:2/native-unsigned-integer-unit:8, ESSID/binary>> = <<14,0,1,0,116,104,101,101,115,115,105,100,105,115,97,108, 105,101>>.
<<14,0,1,0,116,104,101,101,115,115,105,100,105,115,97,108, 105,101>>
2> Len.
14
16> ESSID.
<<"theessidisalie">>
  • the quality: each byte represents a statistic

From the example:

17> <<Qual:8, Level:8/signed, Noise:8, Updated:8>> = <<40,186,0,75>>. 
<<40,186,0,75>>
18> {Qual, Level, Noise, Updated}.
{40,-70,0,75}

So the signal quality of this AP is ok (40/70).

2 comments:

  1. Amazing article! I was losing momentum on my Erlang g projects, this has inspired me. :-)

    ReplyDelete
  2. Thanks Zach! And good luck with the Erlang projects, looking forward to seeing them!

    ReplyDelete