Question

Using Getting essid via ioctl in ruby as a template I wanted to get the BSSID rather than the ESSID. However, not being a C developer, there are a few things that I don't understand.

What I have so far which does not work :( ...

NOTE I'm a bit confused because part of me thinks, according to some comments in wireless.h, that the BSSID can only be set via ioctl. However, the ioctl to get exists. That along with my almost complete lack of understanding of the more intermediate C type isms (structs, unions, and stuff ;) ), I simply don't know.

def _get_bssid(interface)
    # Copied from wireless.h
    # supposing a 16 byte address and 32 byte buffer but I'm totally 
    # guessing here.
    iwreq = [interface, '' * 48,0].pack('a*pI') 
    sock = Socket.new(Socket::AF_INET, Socket::SOCK_DGRAM, 0)

    # from wireless.h
    # SIOCGIWAP 0x8B15      /* get access point MAC addresses */
    sock.ioctl('0x8B15', iwreq) # always get an error: Can't convert string to Integer

    puts iwreq.inspect
end

So, in the meantime, I'm using a wpa_cli method for grabbing the BSSID but I'd prefer to use IOCTL:

def _wpa_status(interface)
    wpa_data = nil

    unless interface.nil?
        # need to write a method to get the src_sock_path 
        # programmatically. Fortunately, for me
        # this is going to be the correct sock path 99% of the time.
        # Ideas to get programmatically would be:
        # parse wpa_supplicant.conf
        # check process table | grep wpa_suppl | parse arguments
        src_sock_path  = '/var/run/wpa_supplicant/' + interface
    else
        return nil
    end

    client_sock_path = '/var/run/hwinfo_wpa'

    # open Domain socket
    socket = Socket.new(Socket::AF_UNIX, Socket::SOCK_DGRAM, 0)

    begin
        # bind client domain socket
        socket.bind(Socket.pack_sockaddr_un(client_sock_path))

        # connect to server with our client socket
        socket.connect(Socket.pack_sockaddr_un(src_sock_path))

        # send STATUS command
        socket.send('STATUS', 0)

        # receive 1024 bytes (totally arbitrary value)
        # split lines by \n
        # store in variable wpa_data.
        wpa_data = socket.recv(1024)
    rescue => e
        $stderr.puts 'WARN: unable to gather wpa data: ' + e.inspect
    end
    # close or next time we attempt to read it will fail.
    socket.close

    begin
        # remove the domain socket file for the client
        File.unlink(client_sock_path)
    rescue => e
        $stderr.puts 'WARN: ' + e.inspect
    end

    unless wpa_data.nil?
        @wifis = Hash[wpa_data.split(/\n/).map\
                 {|line|
                    # first, split into pairs delimited by '='
                    key,value = line.split('=')

                    # if key is camel-humped then put space in front
                    # of capped letter
                    if key =~ /[a-z][A-Z]/
                        key.gsub!(/([a-z])([A-Z])/,'\\1_\\2')
                    end

                    # if key is "id" then rename it.
                    key.eql?('id') && key = 'wpa_id'

                    # fix key so that it can be used as a table name
                    # by replacing spaces with underscores
                    key.gsub!(' ','_')

                    # lower case it.
                    key.downcase!

                    [key,value]
                 }]
    end
end

EDIT: So far nobody has been able to answer this question. I think I'm liking the wpa method better anyway because I'm getting more data from it. That said, one call-out I'd like to make is if anyone uses the wpa code, be aware that it will require escalated privileges to read the wlan socket.

EDIT^2 (full code snippet): Thanks to @dasup I've been able to re-factor my class to correctly pull the bssid and essids using system ioctls. (YMMV given the implementation, age, and any other possible destabilization thing to your Linux distribution - the following code snippet works with the 3.2 and 3.7 kernels though.)

require 'socket'

class Wpa
    attr_accessor :essid, :bssid, :if

    def initialize(interface)
        @if = interface

        puts 'essid: ' + _get_essid.inspect
        puts 'bssid: ' + _get_bssid.inspect
    end

    def _get_essid
        # Copied from wireless.h
        iwreq = [@if, " " * 32, 32, 0 ].pack('a16pII')

        sock = Socket.new(Socket::AF_INET, Socket::SOCK_DGRAM, 0)
        sock.ioctl(0x8B1B, iwreq)

        @essid = iwreq.unpack('@16p').pop.strip
    end 

    def _get_bssid
        # Copied from wireless.h
        # supposing a 16 byte address and 32 byte buffer but I'm totally 
        # guessing here.
        iwreq = [@if, "\0" * 32].pack('a16a32') 
        sock = Socket.new(Socket::AF_INET, Socket::SOCK_DGRAM, 0)

        # from wireless.h
        # SIOCGIWAP 0x8B15      /* get access point MAC addresses */
        sock.ioctl(0x8B15, iwreq) # always get an error: Can't convert string to Integer

        @bssid = iwreq.unpack('@18H2H2H2H2H2H2').join(':')
    end
end

h = Wpa.new('wlan0')
Was it helpful?

Solution

I'm not very much familiar with Ruby, but I spotted two mistakes:

  • The hex number for SIOCGIWAP should be given without quotes/ticks.
  • The initialization of the data buffer ends up with some trailing bytes after the interface name (debugged using gdb). The initialization given below works.

Be aware that your code will break if any of the data structures or constants change (IFNAMSIZ, sa_family, struct sockaddr etc.) However, I don't think that such changes are likely anytime soon.

require 'socket'

def _get_bssid(interface)
    # Copied from wireless.h
    # supposing a 16 byte address and 32 byte buffer but I'm totally 
    # guessing here.
    iwreq = [interface, "\0" * 32].pack('a16a32') 
    sock = Socket.new(Socket::AF_INET, Socket::SOCK_DGRAM, 0)

    # from wireless.h
    # SIOCGIWAP 0x8B15      /* get access point MAC addresses */
    sock.ioctl(0x8B15, iwreq) # always get an error: Can't convert string to Integer

    puts iwreq.inspect
end

You'll get back an array/buffer with:

  • The interface name you sent, padded with 0x00 bytes to a total length of 16 bytes.
  • Followed by a struct sockaddr, i.e. a two-byte identifier 0x01 0x00 (coming from ARPHRD_ETHER?) followed by the BSSID padded with 0x00 bytes to a total length of 14 bytes.

Good luck!

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top