ExReticulum

Elixir NIF wrapper for Reticulum-rs, the Rust implementation of the Reticulum cryptographic mesh networking stack.

ExReticulum lets Elixir applications participate as full nodes in Reticulum networks — creating identities, announcing destinations, establishing encrypted links, and routing packets over any physical layer.

Prerequisites

Installing protoc

# macOS
brew install protobuf

# Ubuntu/Debian
apt install protobuf-compiler

# Arch
pacman -S protobuf

Setup

ExReticulum's NIF crate pulls the reticulum Rust crate directly from the Reticulum-rs GitHub repo. No need to clone it separately.

git clone https://github.com/jtippett/ex_reticulum
cd ex_reticulum

# Fetch dependencies and compile (first build compiles the Rust NIF — ~60-90s)
mix deps.get
mix compile

Quick Start

# Generate a cryptographic identity
{:ok, identity} = ExReticulum.Identity.generate()

# Start a transport node
{:ok, transport} = ExReticulum.Transport.start_link(
  identity: identity,
  name: "my_node"
)

# Connect to the network
{:ok, :added} = ExReticulum.Transport.add_interface(
  transport, :tcp_client, address: "127.0.0.1:4242"
)

# Create and announce a destination
{:ok, dest} = ExReticulum.Transport.add_destination(
  transport, identity, "myapp", "service"
)
{:ok, :announced} = ExReticulum.Transport.announce(transport, dest)

Core Concepts

Identity

Ed25519/X25519 key pairs used for signing, verification, and key exchange.

# Generate a random identity
{:ok, id} = ExReticulum.Identity.generate()

# Deterministic identity from a name (same name = same keys)
{:ok, id} = ExReticulum.Identity.from_name("my_node")

# Serialize/deserialize for persistence
{:ok, hex} = ExReticulum.Identity.to_hex(id)
{:ok, restored} = ExReticulum.Identity.from_hex(hex)

# Sign and verify data
{:ok, signature} = ExReticulum.Identity.sign(id, "hello")
{:ok, :verified} = ExReticulum.Identity.verify(id, "hello", signature)

All functions return {:ok, result} or {:error, reason}. Bang variants (generate!, sign!, etc.) are available and raise ExReticulum.Error on failure.

Transport

The central GenServer managing network participation. Wraps the Rust Transport via NIF.

{:ok, transport} = ExReticulum.Transport.start_link(
  identity: identity,
  name: "my_node",
  broadcast: true,       # forward packets (default: true)
  retransmit: true,      # retransmit announces (default: true)
  reroute_eager: false,  # eager path rerouting (default: false)
  restart_outlinks: false, # restart failed outbound links (default: false)
  announce_forever: false  # keep retransmitting announces (default: false)
)

Interfaces

Connect to the network over TCP or UDP. Note that add_interface/3 returns {:ok, :added} once the interface worker is spawned — the actual bind/connect happens asynchronously and retries every 5 seconds on failure.

# TCP server (listen for incoming connections)
{:ok, :added} = ExReticulum.Transport.add_interface(
  transport, :tcp_server, address: "0.0.0.0:4242"
)

# TCP client (connect to a peer)
{:ok, :added} = ExReticulum.Transport.add_interface(
  transport, :tcp_client, address: "192.168.1.10:4242"
)

# UDP (with optional forwarding address)
{:ok, :added} = ExReticulum.Transport.add_interface(
  transport, :udp, bind_address: "0.0.0.0:5555", forward_address: "192.168.1.255:5555"
)

Destinations and Announces

Destinations are addressable endpoints. Announce them so other nodes can discover and reach you.

# Create a destination
{:ok, dest} = ExReticulum.Transport.add_destination(
  transport, identity, "myapp", "chat"
)

# Announce with optional app data
{:ok, :announced} = ExReticulum.Transport.announce(transport, dest, "status: online")

# Check if a destination exists locally
{:ok, true} = ExReticulum.Transport.has_destination(transport, dest.address_hash)

Subscribing to Events

Subscribe any process to receive network events as messages.

# Subscribe to announces, link events, or data
{:ok, :subscribed} = ExReticulum.Transport.subscribe(transport, :announces)
{:ok, :subscribed} = ExReticulum.Transport.subscribe(transport, :link_events)
{:ok, :subscribed} = ExReticulum.Transport.subscribe(transport, :data)

# Events arrive as {:ex_reticulum, struct} messages
receive do
  {:ex_reticulum, %ExReticulum.Announce{} = announce} ->
    IO.puts("Discovered: #{Base.encode16(announce.address_hash)}")
    IO.puts("App data: #{announce.app_data}")

  {:ex_reticulum, %ExReticulum.LinkEvent{event: :activated, link_id: id}} ->
    IO.puts("Link activated: #{Base.encode16(id)}")

  {:ex_reticulum, %ExReticulum.LinkEvent{event: {:data, payload}}} ->
    IO.puts("Received: #{payload}")

  {:ex_reticulum, %ExReticulum.ReceivedData{data: data}} ->
    IO.puts("Data: #{data}")
end

Subscriptions are automatically cleaned up when the subscribing process exits.

Links

Bidirectional encrypted channels for sustained communication between two nodes.

# After receiving an announce, create a link to that destination
{:ok, :subscribed} = ExReticulum.Transport.subscribe(transport, :announces)
{:ok, :subscribed} = ExReticulum.Transport.subscribe(transport, :link_events)

receive do
  {:ex_reticulum, %ExReticulum.Announce{} = announce} ->
    # Create a link using the announce data
    {:ok, link} = ExReticulum.Transport.link(transport, %{
      address_hash: announce.address_hash,
      identity: announce.identity,
      name_hash: announce.name_hash
    })

    # Wait for the link to activate (handshake completes)
    receive do
      {:ex_reticulum, %ExReticulum.LinkEvent{event: :activated}} ->
        # Send data over the encrypted link
        {:ok, :sent} = ExReticulum.Transport.link_send(transport, link, "hello!")
    end
end

# Query link state
{:ok, :active} = ExReticulum.Link.status(link)
{:ok, rtt_ms} = ExReticulum.Link.rtt(link)

# Close when done
{:ok, :closed} = ExReticulum.Transport.link_close(transport, link)

Supervision

Add transports to your application's supervision tree:

defmodule MyApp.Application do
  use Application

  def start(_type, _args) do
    {:ok, identity} = ExReticulum.Identity.from_name("my_app")

    children = [
      {ExReticulum.Transport,
       identity: identity,
       name: "my_app",
       process_name: MyApp.Transport}
    ]

    Supervisor.start_link(children, strategy: :one_for_one)
  end
end

Then reference it by registered name:

ExReticulum.Transport.add_interface(MyApp.Transport, :tcp_client, address: "peer:4242")

Architecture

ExReticulum uses Rustler to bridge Elixir and Rust:

Testing

mix test              # Run all tests (including E2E)
mix test --exclude e2e  # Skip end-to-end tests

The E2E tests use TCP loopback to verify two-node announce discovery and link data exchange.

License

MIT