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
})