elixiroh

Elixir bindings to iroh — dial-by-key p2p networking with QUIC connections, relay fallback and hole punching.

elixiroh wraps the iroh Rust crate in a Rustler NIF. The API surface mirrors iroh-ffi (the official Python, Swift, Kotlin and Node bindings): connections only — blobs and docs are not exposed until they stabilize upstream.

Installation

Requires Elixir ≥ 1.20, Erlang/OTP ≥ 27 and a Rust toolchain (Rustler compiles the NIF on first mix compile).

def deps do
[
{:elixiroh, "~> 0.2.0"}
]
end

Quick start: a supervised echo server

defmodule MyApp.Echo do
use Elixiroh.Handler
@impl Elixiroh.Handler
def handle_stream({:bi, send, recv}, _conn, state) do
for chunk <- recv do
:ok = Elixiroh.SendStream.write_all(send, chunk) |> Elixiroh.await()
end
:ok = Elixiroh.SendStream.finish(send) |> Elixiroh.await()
{:continue, state}
end
end
# In your supervision tree:
children = [
{Elixiroh.Server,
handler: MyApp.Echo,
alpns: ["my-proto/1"],
bind_opts: [preset: :minimal, relay_mode: :disabled]}
]

Quick start: a client

{:ok, endpoint} = Elixiroh.bind(preset: :minimal, relay_mode: :disabled)
{:ok, conn} = Elixiroh.connect(endpoint, server_addr, "my-proto/1")
{:ok, send, recv} = Elixiroh.Connection.open_bi(conn) |> Elixiroh.await()
:ok = Elixiroh.SendStream.write_all(send, "hello") |> Elixiroh.await()
:ok = Elixiroh.SendStream.finish(send) |> Elixiroh.await()
{:ok, "hello"} = Elixiroh.RecvStream.read_to_end(recv, 1024) |> Elixiroh.await()

Sharing the address

The quick start binds loopback-only (preset: :minimal, relay_mode: :disabled). For real deployments use the default :n0 preset (production relays plus DNS lookup by endpoint id) and share a ticket — the connection info in a copyable string:

# Server side: start the supervision tree, then use the server pid
{:ok, server} = Supervisor.start_link(children, strategy: :one_for_one)
ticket = Elixiroh.Server.address(server) |> Elixiroh.EndpointTicket.from_addr()
send_it_to_the_client(to_string(ticket))
# Client side:
{:ok, ticket} = Elixiroh.EndpointTicket.from_string(str)
{:ok, conn} = Elixiroh.connect(endpoint, ticket, "my-proto/1")

Configuration

Elixiroh.Endpoint.bind/1 starts from a preset (:n0 for the n0 production network, :minimal for offline/loopback) and overrides from there: custom relays (:relay_mode), your own DNS discovery origin (:lookup), both IP families (:bind_addr list), TLS debugging (:keylog), and a :transport keyword list mapping 1:1 onto iroh's QUIC transport config — idle timeout, keep-alive, stream limits, flow windows, MTU. Every key and default is documented in Elixiroh.Endpoint's moduledoc; Elixiroh.Server forwards all of it through :bind_opts.

The async model

Every wait-heavy function returns an op token and completes as a {:elixiroh, token, result} message in the submitting process's mailbox; Elixiroh.await/2 collects it. Nothing holds a scheduler while waiting, at any fan-out:

tokens = for addr <- peers, do: Elixiroh.Endpoint.connect(endpoint, addr, alpn)
for token <- tokens do
receive do
{:elixiroh, ^token, {:ok, conn}} -> conn
end
end

Cheap getters (alpn/1, remote_id/1, stream id/1) and queue-only operations (send_datagram/2, Connection.close/3) stay synchronous. Stream operations serialize on the stream's lock, so they are submitted ops even when cheap.

API map

Area Modules
Lifecycle Elixiroh.Endpoint (bind, connect, accept, close), Elixiroh.Connection, Elixiroh.Incoming
Streams Elixiroh.SendStream, Elixiroh.RecvStream (Enumerable/Collectable)
Identity Elixiroh.EndpointId, Elixiroh.SecretKey, Elixiroh.Signature
Addressing Elixiroh.EndpointAddr, Elixiroh.EndpointTicket
Server Elixiroh.Server, Elixiroh.Handler
Observability Elixiroh.Telemetry
Subscriptions Elixiroh.Watcher
Errors Elixiroh.Error

Semantics worth knowing

Observability

Start Elixiroh.Telemetry in your supervision tree and every iroh log line becomes both a [:elixiroh, :log] telemetry event and a standard Logger entry — attach handlers like any other BEAM library. The level comes from config :elixiroh, :log_level, falling back to the Logger level:

config :elixiroh, log_level: :debug

Stress testing

scripts/stress_connections.exs drives echo traffic at scale (--mode connections | streams | concurrent | peers | drivers); mix test --only stress runs the ExUnit scenarios. Measured results live in BENCHMARKS.md.

Contributing

Patches by mail, the sourcehut way:

$ git config sendemail.to ~ebi/elixiroh@lists.sr.ht
$ git send-email -1

License

MIT OR Apache-2.0, like iroh itself. See LICENSE-MIT and LICENSE-Apache-2.0 in the repository root.