Locating Raspberry Pis in the LAN using F# scripting

There has been one big question since the beginning of Raspberry Pis: What IP address did it get?

There have been some creative solutions, like using espeak to let the Raspberry Pi speak its IP address to you on boot or mailing the IP address to you.

If you are not interested in the details, go to the bottom of the post for the complete script.

In this post we are embracing the brute-force way and go looking for it. In most cases, people use an IPv4 network with a 24-bit subnet mask, which means there are at most 254 possible IP addresses. This number is low enough to scan the entire possible IP address range and see what (if anything) is there. The F# script we are going to implement uses the following algorithm:

  • Prompt the user to enter the first three numbers of an IP address (let’s call it the NetID, for example: 192.168.0)
  • For all 254 IP addresses that start with these three numbers (192.168.0.1 to 192.168.0.254 in the example above), do:
  • Send a ping (ICMP Echo Request) to the IP address.
  • If there is no reply, stop.
  • Check if default SSH port (22) is open by trying to connect a Socket to it.
  • If the connection attempt failed, stop.
  • Try to get the hostname via DNS lookup.
  • Try to get the host’s MAC address via ARP request.
  • If the MAC address lookup was successful, try to get the registered vendor for the MAC address by using MAC Vendor’s API.
  • Print all the information about the IP address we could gather.

Now let’s get to work. We are going to use Async Workflows extensively, since most requests can be made asynchronously and we will use Async.Parallel to make as many parallel requests as possible.

Part 1: Namespaces and helper modules

The first part of the script is rather boring and out-of-context, since it opens the required namespaces and defines a couple of modules that contain helper functions for the rest of the script.

open System
open System.IO
open System.Net
open System.Net.NetworkInformation
open System.Net.Sockets
open System.Runtime.InteropServices


module Async =
    /// Catches all exceptions and returns None instead.
    let catchIgnore task =
        async.Bind (Async.Catch task, 
            function Choice1Of2 result -> Some result | _ -> None
            >> async.Return
        )

#nowarn "40"
module Lazy =
    /// Lazy fixed-point combinator.
    let fix f = let rec fix = lazy f fix in fix.Value

module Option =
    /// Returns the default value when None, calls f v when Some v.
    let private withDefaultValue d f = function Some v -> f v | _ -> d

    /// Returns empty string when None, otherwise calls f with the value.
    let toString f  = withDefaultValue String.Empty f

    /// Lifts the Option into an async computation. 
    let bindAsync f = withDefaultValue (async.Return None) f

The Async module in line 9 defines a catchIgnore helper function that will turn exceptions in tasks into the Option⟨'a⟩ type, in very much the same way that Async.Catch turns exceptions in tasks into the Choice⟨'a,'b⟩ type. As you can see, it uses Async.Catch internally and throws away the exception information, returning Some result when the task completed successfully and None when any exception was thrown.

The Lazy module in line 18 defines a fix helper function. This is a lazy version of the Fixed-point combinator. It offers another form of recursion, where, instead of marking a function with the rec keyword and calling it directly, the function itself will be passed as the first argument of the function. So by calling the first argument of the function, you are calling the function itself. Confusing at first, but this allows for some rather nifty shortcuts as you will see later. The #nowarn "40" compiler directive disables a warning about the inner fix, since it is a recursive object.

The Option module in line 22 introduces two more helper functions. The first function in the module – withDefaultValue – is a private function that returns a default value d when the third argument is None, and executes the passed function f with the value in the Option if the third argument is Some.
The first helper function – toString – uses withDefaultValue to provide a quick way to turn Options into strings. For example, Some 42 |⟩ Option.toString (sprintf "(%d)") will return the string “(42)”, whereas None |⟩ Option.toString (sprintf "(%d)") will return the empty string.
The second helper function – bindAsync – is an asynchronous version of the Option.bind function. It also uses withDefaultValue to provide a simpler way to use Options in certain computations involving Async. For example, this will turn the given stringOption into a task that appends “42” to the string after one second and returns it in a new option, or a task that returns None immediately if stringOption was also None: stringOption |⟩ Option.bindAsync (fun s -⟩ async { let! _ = Async.Sleep 1000 in return Some (s + "42")})

Taken out-of-context, these helper functions might seem questionable at first, but we’ll see how they are used later.

Part 2: Prompting the user for the NetID

This task of the script asks the user to enter the NetID (e.g. 192.168.0). It will retry until the user enters a valid NetID.

/// Prompt the user to enter a valid NetID.
let readNetIDTask = async {
    do printfn "Enter the first three numbers of the IP address range to scan (e.g. 192.168.0)."
    return! Lazy.fix (fun askAgain -> async {
        printf "IP address range: "
        let netID = Console.ReadLine ()
        match netID |> sprintf "%s.1" |> IPAddress.TryParse with
        | true, _ -> return netID
        | _ -> return! askAgain.Value
    })
}

Technically, this does not need to be an asynchronous task. But since everything else is, I thought I might as well keep it consistent. After the user is informed what input is expected, the actual value is read inside a subcomputation of the fixed-point combinator.

After the user enters the netID, in line 7 it is checked if by appending “.1” to it we end up with a valid IP address. If this is the case, the netID is returned. If not, we ask the user again. But not in the usual way.

As described above, the fixed-point combinator is just another form of recursion: The lambda function inside the Lazy.fix call in line 4 has one argument – askAgain – and returns a new async computation.
Here comes the important part: askAgain is the same async computation wrapped in the Lazy⟨'a⟩ type. This means askAgain.Value is the same async block. By return!ing askAgain.Value, we are in effect calling the async block again until the NetID entered by the user is valid. (Or the user terminates the script in frustration.)

Part 3: Checking if a host exists

This task of the script returns true when the provided IP address exists in the network, and false if it doesn’t.

let [<Literal>] MaxPingRetries = 2 // retries
let [<Literal>] PingTimeout = 200 // ms

/// Sends a ping to check if a host with the given IP address exists.
let doesHostExistTask (address : IPAddress) = MaxPingRetries |> Lazy.fix (fun retryPing retriesLeft -> async {
    use ping = new Ping ()
    let! pingReply = ping.SendPingAsync (address, PingTimeout) |> Async.AwaitTask
    if pingReply.Status = IPStatus.Success then return true
    elif retriesLeft = 0 then return false
    else return! retryPing.Value (retriesLeft - 1)
})

The fixed-point combinator is used again – in line 5, but this time to try a maximum number of times until we are confident that the given IP address is not used in the network. We could just send the ping once and return false immediately, but the problem is ICMP messages can get lost, and hosts that are connected to a network via WiFi regularly lose packets. This is why we have to send a ping more than once if it failed. By default the constant MaxPingRetries in line 1 is set to two, which means a single address is pinged at most three times. With the PingTimeout constant in line 2 set to 200ms, this means it takes just over half a second to determine if an address is not available, which is fast enough.

The pinging itself is rather straightforwared: Beginning in line 6, the task creates a Ping object, send a request asynchronously and checks the result. If a reply was received, true is returned. If no more retries are available, false is returned. Otherwise, the task is called again with the retry counter decremented by one.

Now you might wonder how the retriesLeft argument fits in, and why MaxPingRetries is piped into the fixed-point combinator. Here’s the secret: Only the first argument of the function passed into the combinator will be used for recursion, all other arguments are “preserved”. So if you pass a function with two arguments into the fixed-point combinator, the Value property of the first argument will be a function too, with one argument of its own. To make it a little bit more obvious, consider this:

// NOT part of the script!
1 // <= the first value of counter
|> Lazy.fix (fun recurse -> 
    ((* recurse.Value will be this function => *) fun counter -> 
        printfn "Counting to 5: %d" counter
        if counter < 5 then recurse.Value (counter + 1)
    )
)

Once counter reaches the value 5, the recursion stops and we have reached a fixed-point (that’s where the name comes from).

Part 4: Checking if a port is open

This task of the script establishes a socket connection to the given IP address and port to see if the port is open.

/// Checks if the given remote endpoint is available.
let isPortOpenTask (address : IPAddress) port = async {
    use socket = new Socket (address.AddressFamily, SocketType.Stream, ProtocolType.Tcp)
    try do! Async.FromBeginEnd ((fun (c, s) -> socket.BeginConnect (address, port, c, s)), socket.EndConnect)
        do! Async.FromBeginEnd ((fun (c, s) -> socket.BeginDisconnect (false, c, s)), socket.EndDisconnect)
        return true
    with _ ->
        return false
}

The only complicated section in this part of the script is the fact that Socket does not offer any ConnectAsync and DisconnectAsync methods. So we use Async.FromBeginEnd to construct an awaitable asynchronous computation from the traditional CLI Begin/End operations – which are available.

In line 4 a connection attempt is made. If it succeeds, we immediately close the connection and return true. If it fails, we return false.

Part 5: Resolving the IP address to a hostname

This task of the script checks if a hostname is available for the given IP address.

/// Tries to resolve the given IP address into a corresponding hostname.
let tryGetHostNameTask (address : IPAddress) = Async.catchIgnore (async {
    let! hostEntry = Dns.GetHostEntryAsync address |> Async.AwaitTask
    return hostEntry.HostName
})

This is the first time we catch a glimpse of the Async.catchIgnore helper function, which will wrap the hostname in Some if the lookup succeeds, or return None if an exception was thrown somewhere along the way.

The lookup itself is done at line 3. Dns.GetHostEntryAsync will throw an exception if the host entry could not be found.

Part 6: Getting the MAC address

This task of the script gets the MAC address corresponding to the given IP address.

/// The SendARP function sends an ARP request to obtain the MAC address of the specified IP address.
[<DllImport "iphlpapi.dll">]
extern Int32 SendARP (Int32 destIP, Int32 srcIP, Byte[] macAddr, UInt32& phyAddrLen)

/// Tries to get the MAC address corresponding to the given IP address.
let tryGetMacAddressTask (address : IPAddress) = async {
    let macAddress = Array.zeroCreate 6
    let mutable macAddressLength = uint32 macAddress.Length
    match SendARP (BitConverter.ToInt32 (address.GetAddressBytes (), 0), 0, macAddress, &macAddressLength) with
    | 0 -> return BitConverter.ToString macAddress |> Some
    | _ -> return None
}

Since .NET does not expose ARP requests itself, we need to use P/Invoke to call the external function SendARP defined in the iphlpapi.dll library. The definition of the external function can be seen in line 3, which tells the compiler the function is implemented in native code.

You might notice the phyAddrLen parameter in line 3, which has a type followed by &, which means this parameter is passed by reference. To do this, we first have to create a mutable variable in line 8, which will be passed to the SendARP function with the address-of operator (&) in line 9.

The SendARP function returns 0 on success, in which case we convert the macAddress byte array back into a string in line 10.

Part 7: Getting the MAC vendor

The first three numbers of any MAC address specify the device vendor. Macvendor.com offers an API to get the names of the vendors of MAC addresses. This is useful since the Raspberry Pi foundation has its own MAC addresses.

/// Tries to get the vendor of the specified MAC address.
let tryGetMacVendorTask =
    sprintf "http://api.macvendors.com/%s"
    >> HttpWebRequest.CreateHttp
    >> fun request -> Async.catchIgnore (async {
        let! response = request.GetResponseAsync () |> Async.AwaitTask
        use reader = new StreamReader (response.GetResponseStream ())
        let! vendor = reader.ReadToEndAsync () |> Async.AwaitTask
        return vendor
    })

The task is prepared in line 3 by creating the request URL containing the MAC address for the API. Afterwards a web request is created which will communicate with the service for us. Then we use the Async.catchIgnore helper again to wrap the result value or return None if the request fails. After waiting for the response the vendor name needs to be extracted from the response stream using a StreamReader.

Part 8: Handling a single IP address

This task is the main work horse of the script: It checks if a host with the given IP address exists and the SSH port is open. Then it gathers information about the host: The hostname, the MAC address and the MAC vendor. Everything that could be gathered is displayed on the console eventually.

let [<Literal>] SSHPort = 22
let printfLockObj = Object ()
 
/// Checks if the host with the given IP address exists,
/// then collects and prints host name, MAC address and
/// MAC vendor information.
let identifyHostTask (address : IPAddress) = async {
    let! doesHostExist = doesHostExistTask address
    if not doesHostExist then do () else
 
    let! isSSHEnabled = isPortOpenTask address SSHPort
    if not isSSHEnabled then do () else
 
    let! hostName = tryGetHostNameTask address
    let! macAddress = tryGetMacAddressTask address
    let! macVendor = macAddress |> Option.bindAsync tryGetMacVendorTask

    do lock printfLockObj (fun () -> 
        printfn "%O%s: %s%s"
            address
            (hostName   |> Option.toString (sprintf ", %s"))
            (macAddress |> Option.toString  id             )
            (macVendor  |> Option.toString (sprintf ", %s"))
    )
}

In line 8, it is checked if the host can be pinged. If not, the task stops. In line 10 it is checked if the SSH port, which is defined as a constant SSHPort in line 1 is accessible. If not, the task stops. You might wonder why lines 9 and 11 read if not X then do () else instead of just if X then, and I would tell you that this is a legitimate question. The problem with if X then is that since there might be an else part further down, F# requires you to indent the lines belonging to the if statement to avoid ambiguity. But in the first case the if statement is already complete and we are not required to indent the subsequent lines any further.

After line 11 we know that the host exists and the SSH port is open. This means the host is potentially a Raspberry Pi and we should gather information about it. In line 13 you will notice the only use of the Option.bindAsync helper function: Since macAddress is of type Option⟨'a⟩, we pass it to Option.bindAsync to lift it into the asynchronous computation.

Lines 21 to 23 use the Option.toString helper function to convert the hostname, MAC address and MAC vendor values to strings.

printfLockObj is used to ensure no two printfns happen at the same time, which results in unreadable text on the console.

Part 9: Handling all IP addresses – In parallel

This is the final task of the script and runs synchronously on the main thread. It iterates over all the possible IP addresses and passes them to the identifyHostTask.

// Main task.
Async.RunSynchronously (async {
    let! addressPart = readNetIDTask

    do! { 1 .. 254 }
        |> Seq.map (sprintf "%s.%d" addressPart >> IPAddress.Parse)
        |> Seq.map identifyHostTask
        |> Async.Parallel
        |> Async.Ignore

    do printfn "Done. Press any key to exit..."
    do Console.ReadKey true |> ignore
})

At first we prompt the user to enter the NetID in line 3. Then in line 5 we create a sequence containing the number 1 to 254, which will be converted to IP addresses in the next line.

In line 7 every IP address is passed to the identifyHostTask. Now we have a sequence of 254 async computations ready to be executed.

In line 8 we start the tasks in the sequence in parallel, which will result in a single async computation containing an array of results. Since identifyHostTask returns nothing, this will be an array of Units, which we throw away in the next line.

When all the IP addresses have been processed, lines 11 and 12 inform the user that the script has finished.

The complete script

Put the following code into a file called PiDetector.fsx. After installing the F# tools, you should be able to right-click the file and choose “Execute in F# Interactive”.

open System
open System.IO
open System.Net
open System.Net.NetworkInformation
open System.Net.Sockets
open System.Runtime.InteropServices


module Async =
    /// Catches all exceptions and returns None instead.
    let catchIgnore task =
        async.Bind (Async.Catch task, 
            function Choice1Of2 result -> Some result | _ -> None
            >> async.Return
        )

#nowarn "40"
module Lazy =
    /// Lazy fixed-point combinator.
    let fix f = let rec fix = lazy f fix in fix.Value

module Option =
    /// Returns the default value when None, calls f v when Some v.
    let private withDefaultValue d f = function Some v -> f v | _ -> d

    /// Returns empty string when None, otherwise calls f with the value.
    let toString f  = withDefaultValue String.Empty f

    /// Lifts the Option into an async computation. 
    let bindAsync f = withDefaultValue (async.Return None) f


/// Prompt the user to enter a valid NetID.
let readNetIDTask = async {
    do printfn "Enter the first three numbers of the IP address range to scan (e.g. 192.168.0)."
    return! Lazy.fix (fun askAgain -> async {
        printf "IP address range: "
        let netID = Console.ReadLine ()
        match netID |> sprintf "%s.1" |> IPAddress.TryParse with
        | true, _ -> return netID
        | _ -> return! askAgain.Value
    })
}

let [<Literal>] MaxPingRetries = 2 // retries
let [<Literal>] PingTimeout = 200 // ms

/// Sends a ping to check if a host with the given IP address exists.
let doesHostExistTask (address : IPAddress) = MaxPingRetries |> Lazy.fix (fun retryPing retriesLeft -> async {
    use ping = new Ping ()
    let! pingReply = ping.SendPingAsync (address, PingTimeout) |> Async.AwaitTask
    if pingReply.Status = IPStatus.Success then return true
    elif retriesLeft = 0 then return false
    else return! retryPing.Value (retriesLeft - 1)
})

/// Checks if the given remote endpoint is available.
let isPortOpenTask (address : IPAddress) port = async {
    use socket = new Socket (address.AddressFamily, SocketType.Stream, ProtocolType.Tcp)
    try do! Async.FromBeginEnd ((fun (c, s) -> socket.BeginConnect (address, port, c, s)), socket.EndConnect)
        do! Async.FromBeginEnd ((fun (c, s) -> socket.BeginDisconnect (false, c, s)), socket.EndDisconnect)
        return true
    with _ ->
        return false
}

/// Tries to resolve the given IP address into a corresponding hostname.
let tryGetHostNameTask (address : IPAddress) = Async.catchIgnore (async {
    let! hostEntry = Dns.GetHostEntryAsync address |> Async.AwaitTask
    return hostEntry.HostName
})

/// The SendARP function sends an ARP request to obtain the MAC address of the specified IP address.
[<DllImport "iphlpapi.dll">]
extern Int32 SendARP (Int32 destIP, Int32 srcIP, Byte[] macAddr, UInt32& phyAddrLen)

/// Tries to get the MAC address corresponding to the given IP address.
let tryGetMacAddressTask (address : IPAddress) = async {
    let macAddress = Array.zeroCreate 6
    let mutable macAddressLength = uint32 macAddress.Length
    match SendARP (BitConverter.ToInt32 (address.GetAddressBytes (), 0), 0, macAddress, &macAddressLength) with
    | 0 -> return BitConverter.ToString macAddress |> Some
    | _ -> return None
}

/// Tries to get the vendor of the specified MAC address.
let tryGetMacVendorTask =
    sprintf "http://api.macvendors.com/%s"
    >> HttpWebRequest.CreateHttp
    >> fun request -> Async.catchIgnore (async {
        let! response = request.GetResponseAsync () |> Async.AwaitTask
        use reader = new StreamReader (response.GetResponseStream ())
        let! vendor = reader.ReadToEndAsync () |> Async.AwaitTask
        return vendor
    })

let [<Literal>] SSHPort = 22
let printfLockObj = Object ()
 
/// Checks if the host with the given IP address exists,
/// then collects and prints host name, MAC address and
/// MAC vendor information.
let identifyHostTask (address : IPAddress) = async {
    let! doesHostExist = doesHostExistTask address
    if not doesHostExist then do () else
 
    let! isSSHEnabled = isPortOpenTask address SSHPort
    if not isSSHEnabled then do () else
 
    let! hostName = tryGetHostNameTask address
    let! macAddress = tryGetMacAddressTask address
    let! macVendor = macAddress |> Option.bindAsync tryGetMacVendorTask

    do lock printfLockObj (fun () -> 
        printfn "%O%s: %s%s"
            address
            (hostName   |> Option.toString (sprintf ", %s"))
            (macAddress |> Option.toString  id             )
            (macVendor  |> Option.toString (sprintf ", %s"))
    )
}

// Main task.
Async.RunSynchronously (async {
    let! addressPart = readNetIDTask

    do! { 1 .. 254 }
        |> Seq.map (sprintf "%s.%d" addressPart >> IPAddress.Parse)
        |> Seq.map identifyHostTask
        |> Async.Parallel
        |> Async.Ignore

    do printfn "Done. Press any key to exit..."
    do Console.ReadKey true |> ignore
})

Compiling Mono & F# on the Raspberry Pi

This guide assumes you already have Raspbian installed on your Raspberry Pi, that the system is fairly up-to-date and Mono is not already installed.

If you already have Mono installed, do not follow this guide! We are going to compile Mono from source and install it into the /usr prefix, which will severely disrupt your system if another version of Mono was already installed before. If you want to have two different versions of Mono installed at the same time, you might want to read up on Parallel Mono Environments.

Enabling extra Swap-Space

Compiling F# takes a lot of RAM and Swap-Space. More than the default configuration offers. Plug a flash drive with at least 1GiB into the Raspberry Pi. Ensure that the drive already has one partition on it that we can use as a swap partition and you don’t have anything you want to keep on it.

Next, we have to figure out what path the flash drive has been given. Usually it’s /dev/sda for the device and /dev/sda1 for the first partition. To make sure, we can run the following command that will print the vendor and model name of the drive with the specified path.

udevadm info --name=/dev/sda1 --query=property | grep --color=never --perl-regexp "ID_(VENDOR|MODEL)="

Once you have ensured that /dev/sda1 is the right partition (or you have figured out the correct path, in which case you need to adjust the commands accordingly), we can enable the extra Swap-Space. This will only last until the next reboot.

sudo mkswap /dev/sda1
sudo swapon /dev/sda1

Compiling Mono

Fetch the current Mono source code. It will be placed inside a build directory in the current user’s home directory.

git clone https://github.com/mono/mono ~/build/mono
cd ~/build/mono

Before running autogen.sh to generate the Makefile, we have to install a couple of dependencies: autoconf, automake and libtool.

sudo apt-get install auto{conf,make} libtool

Next, we run autogen.sh and set the installation prefix to /usr, which indicates the place where the binaries will be installed to. National Language Support is disabled with the --enable-nls=no argument, since we do not need it and it would add another dependency (gettext).

./autogen.sh --prefix=/usr --enable-nls=no

Since parts of Mono are written in C#, we need a C# compiler to compile Mono, which we currently do not have. To bootstrap the compilation, we can do as the Mono Readme says and fetch the current monolite distribution, which enables us to compile C# code.

make get-monolite-latest

Now everything is ready to be built and installed, which takes quite some time.

make EXTERNAL_MCS=${PWD}/mcs/class/lib/monolite/gmcs.exe
sudo make install

Compiling F#  

Fetch the current F# source code. Like Mono, it will be placed inside the build directory in the current user’s home directory.

git clone https://github.com/fsharp/fsharp ~/build/fsharp
cd ~/build/fsharp

In addition to the Mono dependencies, F# adds one more – pkg-config – which has to be installed before running autogen.sh.

sudo apt-get install pkg-config

Then, as before, we run autogen.sh and set the installation prefix also to /usr, so that the Mono and F# binaries end up in the same place.

./autogen.sh --prefix=/usr

To compile and install F#, run the following two commands. Make sure you have enough Swap-Space as described above.

make
sudo make install

Testing the Installation

To test the new F# installation, the following commands will create an executable F# script inside the current user’s home directory and call it.

cat << EOF > ~/helloworld.fsx
#light (*
    exec fsharpi --exec \$0 \$*
*)
printfn "Hello, World!"
EOF
chmod +x ~/helloworld.fsx
~/helloworld.fsx

Starting up fsharpi can take nearly a minute, so be patient.
And have fun with F# on the Raspberry Pi.