View Source Tundra (tundra v0.1.9)
TUN device support for Elixir.
Tundra provides a simple API for creating and using TUN devices on Linux and Darwin.
As TUN device creation is a privileged operation on most systems, Tundra uses a server process to create and configure TUN devices. Once created, the device is represented within the runtime as a socket on Darwin and a NIF resource on Linux, with process-ownership semantics i.e.
- Only the owning process can read from or write to the device and receive i/o notifications from it.
- The TUN device is removed when the owning process exits.
Server Process
The server program is called tundra_svr
and is located under the priv
directory
of the Tundra application. The :tundra
application expects that this server is
already running as a privileged user when it starts.
The server listens on a Unix domain socket at /var/run/tundra.sock
and accepts
requests from the NIF to create and configure the tun device. The resulting
file descriptor is sent back to the NIF, which then creates a socket from it (on
Darwin) or wraps it in a NIF resource (on Linux).
Non-blocking I/O
Tundra only supports non-blocking I/O on TUN devices. The recv/3
and send/3
functions currently require that :nowait
is pssed as the last argument. If the
operation would block, the function will return {:select, select_info}
and
a notification of the following form will be sent to the owning process when
the device is ready:
{:"$socket", dev, :select, select_handle}
Note that, although on Linux the underlying TUN device is not technically a
socket, the same notification is used to reduce platform specific code. Only
the contents of dev
differ.
IPv6
Tundra is designed to work with IPv6 and has only been tested with IPv6.
Example
The following GenServer creates a TUN device and then relays packets by
- Reading a frame from the device
- Swapping the source and destination addresses
- Writing the modified frame back to the device
defmodule Reflector do
use GenServer
@mtu 1500
def start_link(_), do: GenServer.start_link(__MODULE__, [], name: __MODULE__)
@impl true
def init(_args) do
# Create a TUN device with the given address and options
{:ok, state} =
Tundra.create("fd11:b7b7:4360::2",
dstaddr: "fd11:b7b7:4360::1",
netmask: "ffff:ffff:ffff:ffff::",
mtu: @mtu)
{:ok, state, {:continue, :read}}
end
@impl true
def handle_continue(:read, {dev, _} = state) do
# Read a frame from the device
case Tundra.recv(dev, @mtu, :nowait) do
{:ok, data} ->
{:noreply, state, {:continue, {:reflect, data}}}
{:select, _} ->
{:noreply, state}
end
end
def handle_continue({:reflect, data}, {dev, _} = state) do
# Swap the source and destination addresses of the ipv6 frame
<<hdr::binary-size(4),
pre::binary-size(8),
src::binary-size(16),
dst::binary-size(16),
rest::binary>> = data
reflected = [hdr, pre, dst, src, rest]
# Write the frame back to the device, assume it won't block
:ok = Tundra.send(dev, reflected, :nowait)
{:noreply, state, {:continue, :read}}
end
@impl true
def handle_info({:"$socket", dev, :select, _}, {dev, _} = state) do
# Handle 'input ready' notifications
{:noreply, state, {:continue, :read}}
end
end
Summary
Types
A TUN device address. May be represented either as tuple or a string containing a dotted IP address.
A TUN device.
A TUN device creation option.
Functions
Cancel a pending operation on a TUN device.
Close a TUN device.
Transfer control of a TUN device to another process.
Create a TUN device.
Receive data from a TUN device.
Send data to a TUN device.
Types
A TUN device address. May be represented either as tuple or a string containing a dotted IP address.
@type tun_device() :: :socket.socket() | {:"$tundra", reference()}
A TUN device.
On Darwin, this is a socket. On Linux, this is a reference to a NIF resource.
@type tun_option() :: {:dstaddr, tun_address()} | {:netmask, tun_address()} | {:mtu, non_neg_integer()}
A TUN device creation option.
Functions
@spec cancel(tun_device(), :socket.select_info()) :: :ok | {:error, any()}
Cancel a pending operation on a TUN device.
@spec close(tun_device()) :: :ok | {:error, atom()}
Close a TUN device.
@spec controlling_process(tun_device(), pid()) :: :ok | {:error, any()}
Transfer control of a TUN device to another process.
Must be called by the current owner of the device.
@spec create(tun_address(), [tun_option()]) :: {:ok, {tun_device(), String.t()}} | {:error, any()}
Create a TUN device.
Creates a new TUN device with the given address and options. The options currently supported are:
:netmask
- The netmask of the device.:dstaddr
- The destination address of the device.:mtu
- The maximum transmission unit of the device.
On success returns a tuple containing a device tuple and the name of the device.
Examples
iex> Tundra.create("fd11:b7b7:4360::2",
dstaddr: "fd11:b7b7:4360::1",
netmask: "ffff:ffff:ffff:ffff::",
mtu: 16000)
{:ok, {{:"$socket", #Reference<0.2990923237.3512074243.109526>}, "utun6"}} # Darwin
{:ok, {{:"$tundra", #Reference<0.2990923237.3512074243.109526>}, "tun0"}} # Linux
@spec recv(tun_device(), non_neg_integer(), :nowait) :: {:ok, binary()} | {:select, :socket.select_info()}
Receive data from a TUN device.
The length
argument specifies the maximum number of bytes to read. The caller
is responsible for ensuring this length is at least as large at the MTU of the
device, plus the size of the TUN header (4 bytes).
The :nowait
option specifies that the operation should not block if no data is
available. If data is available, it will be returned immediately. If no data is
available, the function will return {:select, select_info}
.
@spec send(tun_device(), iodata(), :nowait) :: :ok | {:select, :socket.select_info()}
Send data to a TUN device.
The data
argument is an iodata that will be written to the device. The data should
include the 4-byte header that is expected by the TUN device.
The :nowait
option specifies that the operation should not block if the device's
output buffer is full. If the buffer is full, the function will return
{:select, select_info}
.