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
- Elixir >= 1.17
- Rust >= 1.70 (with
cargo) - protoc (Protocol Buffers compiler — required by the Reticulum-rs gRPC layer)
Installing protoc
# macOS
brew install protobuf
# Ubuntu/Debian
apt install protobuf-compiler
# Arch
pacman -S protobufSetup
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 compileQuick 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}")
endSubscriptions 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
endThen 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:
- A single Tokio runtime handles all async Rust operations
- Rust objects (Transport, Destinations, Links) are held as opaque ResourceArc references
- Events flow from Rust to Elixir via OwnedEnv::send() — no polling
- Crypto operations run on BEAM dirty CPU schedulers; network I/O on dirty I/O schedulers
-
All Mutex locks handle poisoning gracefully (return
{:error, :lock_error}, never panic)
Testing
mix test # Run all tests (including E2E)
mix test --exclude e2e # Skip end-to-end testsThe E2E tests use TCP loopback to verify two-node announce discovery and link data exchange.
License
MIT