Packet Sniffer in Elixir
Tags: elixir, erlang, socket, network
Published: 2022-10-02 21:45:23

There is no time to explain, let's create a simple sniffer on Elixir!

The source code lives [here](https://github.com/mike-kasianov/sniffer_example).

OTP 22.0 release added a new  socket module and now Erlang developers have the same socket power as C-developers (+/- 10 volts). Let's try it out.

Disclaimer: Erlang's socket is a wrapper around OS's socket. Thus I give links for implementation of POSIX functions in the Linux's source code to provide proofs and show how the code looks like. But in another OS the code may look different and placed somewhere else. But the general concept should be the same.

So, let's say we're going to listen for raw packets at the device driver (OSI Layer 2). To achieve this there are some steps to be done:

* [Create a socket](#create-a-socket)
* [Read data from the socket](#read-data-from-the-socket)
* [Bind the socket to the interface (optional)](#bind-the-socket-to-an-interface)
* [Set promiscuous mode (optional)](#set-promiscuous-mode)

## Create a socket

The way to do this is to  call [:socket.open/3](https://www.erlang.org/doc/man/socket.html#open-3). There are few variants of this function but we will use one with "open(Domain, Type, Protocol)" arguments. This function is a binding for a standard POSIX's C function named [socket](https://man7.org/linux/man-pages/man2/socket.2.html).

Socket creation works this way:

```elixir
:socket.open(domain, socket_type, protocol)
```

The `domain` variable must be equal to `17`.
This is a communication domain constant that specifies what kind of data we expect to receive from the OS. In the current situation "17" stands for raw low-level packets. [Read more](https://man7.org/linux/man-pages/man7/packet.7.html). The constant is defined [here](https://github.com/torvalds/linux/blob/v5.18/include/linux/socket.h#L195).

The `socket_type` must have `:raw` value. It means we are interested in raw packets.

The `protocol` specifies what kind of packets we're interested in. Since we're interested in all types we use [ETH_P_ALL](https://github.com/torvalds/linux/blob/v5.18/include/uapi/linux/if_ether.h#L131) constant. There is one more thing to mention about the value. The value must be passed into the :socket.open/3 in network byte order ([big-endian](https://en.wikipedia.org/wiki/Endianness)). My processor has little-endian encodings so if I don't convert this value I would get an error.

So, let's see how the socket creation looks like:

```elixir
domain = 17
protocol = 0x0003

# Convert the protocol to a network byte order
<<protocol_host::big-unsigned-integer-size(16)>> = <<protocol::native-unsigned-integer-size(16)>>

{:ok, socket} = :socket.open(domain, :raw, protocol_host)
```

## Read data from the socket
OK, the socket is created and now it's time to read some data. There are a few ways to read but let's focus on [:socket.recvfrom/3](https://www.erlang.org/doc/man/socket.html#recvfrom-3). The documentation describes this function pretty well. In short it sounds like "Hello, OS. Please give me data for this socket. If there is no data let me know when it comes". When the data is available it returns it. Otherwise it sends a message to a socket owner process  when data is available.

```elixir
defmodule Sniffer do
  use GenServer
  …

  defp socket_recieve(socket) do
    case :socket.recvfrom(socket, 1500, :nowait) do
      {:ok, {source, data}} ->
        GenServer.cast(self(), {:socket_data, source, data})

      {:select, _select_info} ->
        :ok

      {:error, reason} ->
        GenServer.cast(self(), {:socket_error, reason})
    end
  end
end
```

The `socket` is our socket we’ve created earlier. The `1500` is a buffer size for new data. And the `:nowait` atom says that we don’t want to wait for data and want this function to return something immediately.

Let’s review the possible result this function produces.
The first clause matches the successful result, i.e. the data is ready, The `source` variable is a map that describes where the `data` came from.

The second clause basically means “I don’t have any data right now, but I'll let you know when it comes.”

The last clause is an error handler.

Now let’s see the callbacks implementation:

```elixir
defmodule Sniffer do
  ...

  def handle_cast({:socket_data, source, data}, socket) do
    IO.puts(“Hurray, we have a new packet”)
    socket_recieve(socket)

    {:noreply, socket}
  end

  def handle_cast({:socket_error, reason}, socket) do
    IO.inspect(reason, label: "Something goes wrong :-(")

    {:noreply, socket}
  end

  def handle_info({:"$socket", socket, :select, _select_handle}, socket) do
    IO.puts(“Knock knock new data is available to pick up”)
    socket_recieve(socket)

    {:noreply, socket}
  end
end

```

## Bind the socket to an interface

If you start reading data from a socket without any limits you find that data comes from different sources like loopback device, different network devices, from docker etc. To limit this behavior we can bind the socket to a special device like ethernet or wifi card. To achieve this just use [:socket.bind/2](https://www.erlang.org/doc/man/socket.html#bind-2) function.

The second argument of the `bind` function is a socket address. For a raw packets it seems the `:socket.bind/2` does not accept the `sockaddr_ll` type as address so let’s implement it ourselves. The C definition of the `sockaddr_ll` type you can find [here](https://man7.org/linux/man-pages/man7/packet.7.html). It’s just a C struct. A C struct looks like a data type but basically it’s a blob of binary data. Since it’s just a binary we can build it ourselves. By the way we don’t need to fill all the values to create a binding for a socket. Only `sll_protocol` and `sll_ifindex` fields are required, the rest fields should are empty.
The `sll_protocol` is the same as we know from the [Create a socket](#create-a-socket) section. But `sll_ifindex` is special for every machine.

`ifindex` stands for Interface Index. You can see the list of all available network interfaces on your machine using command line `ip addr`. There are interface names like `eth0`, `wlp1s0`, etc and their indexes. Erlang has a similar function to retrieve such a list [:net.if_names()](https://www.erlang.org/doc/man/net.html#if_names-0). Notice that indexes in OS and Erlang may be different.

The `if_index` definition:
```elixir
if_index = 1
```
or
```elixir
{:ok, if_index} = :binary.bin_to_list(“eth0”) |> :net.if_name2index()
```

Now we have values to build sockaddr_ll struct:

```elixir
sll_protocol = 0x0003
sll_ifindex = if_index
sll_hatype = 0
sll_pkttype = 0
sll_halen = 0
sll_addr = <<0::native-unsigned-size(8)-unit(8)>>
```

The C sockaddr_ll struct representation on Erlang:

```elixir
addr = <<
      sll_protocol::big-unsigned-size(16),
      sll_ifindex::native-unsigned-size(32),
      sll_hatype::native-unsigned-size(16),
      sll_pkttype::native-unsigned-size(8),
      sll_halen::native-unsigned-size(8),
      sll_addr::binary
    >>
```

And finally the second argument for “:socket.bind/2” is:

```elixi

sockaddr = %{
      family: @af_packet,
      addr: addr
    }

:ok = :socket.bind(socket, sockaddr)
```

That’s it. Now the socket is bound to a specific network interface.

## Set promiscuous mode

What is promiscuous mode? In this mode a net interface controller does not filter any traffic and allows you to do this. For example you have a laptop and a smartphone connected to Wif-Fi. When promiscuous mode is off the laptop sees only traffic aimed at it. But when it is on it’s possible to monitor packets sent to smartphone also.

**NOTE**: The promiscuous mode consumes CPU so turn it off when you’re done. On Ubuntu you can check if any network adapter is in promiscuous mode using the command `ip addr | grep PROMISC`. And turn it off `ip link set eth0 promisc off`.

To turn promiscuous mode on just use [:socket.ioctl/4](https://www.erlang.org/doc/man/socket.html#ioctl-4) function:
```elixir
if_name = :binary.bin_to_list(“eth0”)
:ok = :socket.ioctl(socket, :sifflags, if_name, %{promisc: true})
```

### Permissions
The usage of a raw packets scanning and promiscuous mode requires privileged access. It could be sudo or Linux capabilities.
To set up proper Linux capabilities you need to know where the beam.smp file is located. It should be somewhere on Erlang directory like erlang/24.3.3/erts-12.3.1/bin/beam.smp.
To set up capabilities use `setcup` command:

```bash
sudo setcap cap_net_raw,cap_net_admin=ep ERLANG_PATH/erts-VERSION/bin/beam.smp
```

## That’s it

I described some pitfalls people may face trying to use raw socket on Erlang. The code is placed on [GitHub](https://github.com/mike-kasianov/sniffer_example).

Thanks for reading and have fun!