SmolNet

SmolNet is an OTP-friendly, userspace TCP/IP stack for Elixir. It embeds smoltcp behind a deliberately small Rustler NIF and exposes TCP and UDP through familiar :gen_tcp, :gen_udp, and low-level socket-style APIs.

Each SmolNet stack is an independent network namespace. It exchanges complete IPv4 and IPv6 packets with a link process supplied by the application, rather than opening host operating-system sockets itself.

Why a userspace network stack?

BEAM applications normally use the host kernel's network stack, which is the right choice for ordinary clients and servers. An application-owned stack is useful when packets need to travel over something other than a host network interface, or when a program needs isolated addresses, routes, sockets, and failure domains. Examples include encrypted tunnels, network simulations, packet-level tests, and application-defined transports.

gVisor's Netstack is a larger example of the same architectural idea: its Go userspace network stack lets a sandbox handle network protocols without giving the application direct access to the host network stack. SmolNet applies that idea at a smaller scale for Elixir. It combines smoltcp's protocol implementation with OTP supervision, bounded native work, and standard Erlang socket interfaces.

SmolNet is transport-neutral. It does not provide an Ethernet device, TUN/TAP setup, or a VPN protocol. Your link process decides where emitted raw IP packets go and feeds packets from that transport back into the stack.

Installation

Add smolnet to your dependencies:

def deps do
[
{:smolnet, "~> 0.2"}
]
end

Hex packages include precompiled NIFs for glibc-based GNU/Linux on x86-64 and AArch64, and Apple Silicon macOS 14 or newer. Other targets can build from a source checkout with Rust 1.91 or newer.

Starting a stack

Configure the addresses owned by the stack and identify the process that will carry its outbound packets:

local_address = {0xFD00, 0, 0, 0, 0, 0, 0, 1}
{:ok, stack} =
SmolNet.start_stack(
egress: {self(), :my_link},
mtu: 1280,
addresses: [{local_address, 64}]
)

The link process receives bounded, ordered batches of complete raw IP packets:

receive do
{:smol_stack, :my_link, :egress, packets} ->
send_over_transport(packets)
end

Packets arriving from that transport go back into the stack one at a time:

:ok = SmolNet.ingress(stack, complete_ip_packet)

This boundary carries IP packets, not Ethernet frames. The link owns transport backpressure. When it terminates, the stack follows its configured :link_down policy. SmolNet.stop_stack/1 stops the complete temporary supervision bundle.

For a self-contained stack with no external transport, use SmolNet.Loopback. It feeds every emitted packet back into the same stack:

{:ok, _link, stack} =
SmolNet.Loopback.start_link(
addresses: [{{127, 0, 0, 1}, 8}, {{0, 0, 0, 0, 0, 0, 0, 1}, 128}]
)

Run the complete loopback TCP example from a source checkout with:

mix run examples/loopback.exs

Choosing an API

SmolNet provides three interfaces over the same stack and socket machinery:

Interface Use it when Guide
:gen_tcp and :inet Existing code expects OTP TCP sockets, active mode, or packet framing Using :gen_tcp
:gen_udp and :inet Existing code expects OTP UDP sockets and active or passive delivery Using :gen_udp
SmolNet You want explicit endpoint maps, direct timeouts, or nonblocking readiness Using the low-level socket API

Both address families are supported. The OTP adapters use SmolNet.Inet.Tcp and SmolNet.Inet.Udp for IPv4, and SmolNet.Inet6.Tcp and SmolNet.Inet6.Udp for IPv6. The low-level API selects :inet or :inet6 when a socket is opened.

Design and limits

Each stack has one Elixir owner process and one private native stack. The owner serializes access to the native resource; temporary supervised adapter processes provide :gen_tcp and :gen_udp semantics. A failed stack or adapter cannot corrupt another stack bundle.

Native calls never wait for network traffic. Work is bounded by a time budget and deterministic limits on copied bytes, emitted packets, readiness events, and maintenance. Blocking socket operations wait and retry in the calling Elixir process. SmolNet.stack_info/1 exposes lifecycle, socket, queue, and work-budget telemetry.

The API is intentionally smaller than the host socket API. TCP supports raw, line, and 1/2/4-byte length-prefixed packet modes. UDP preserves datagram boundaries. Ancillary data, multicast, broadcast, OS file descriptors, IPv4-mapped IPv6 addresses, and fragmented IPv4 ingress are not supported.

Troubleshooting

License

SmolNet is released under the MIT License.