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:
Open a datagram socket
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.
Call an ioctl on the socket with the request set to SIOCSIWSCAN (0x8B18).
Allocate another iwreq structure containing a pointer to a user allocated buffer with enough space to hold the response.
Call a second ioctl on the socket with the request set to SIOCGIWSCAN (0x8B19)
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
The BSSID is a struct sockaddr:
<<1,0>>
? No idea as yet :)
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).
Amazing article! I was losing momentum on my Erlang g projects, this has inspired me. :-)
ReplyDeleteThanks Zach! And good luck with the Erlang projects, looking forward to seeing them!
ReplyDelete