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
- iroh streams are lazy. The peer's
accept_bi/accept_unionly completes after the sender writes data. - Native objects are garbage collected. Endpoints, connections and
streams are released safely when their handles are collected; call
Elixiroh.Endpoint.close/1for a graceful shutdown. Pending operations complete with errors when their subject is collected or closed. - Watchers deliver messages. Subscriptions send
{:elixiroh, event, value}to the owner process and stop withElixiroh.Watcher.stop/1or when their handle is collected.
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.