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

Sharing the address

The quick start runs fully offline — the :minimal preset has no relays and no lookup service. 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.addr(server) |> Elixiroh.EndpointTicket.from_addr()
# deliver `ticket` to the client out of band
send_it_to_the_client(to_string(ticket))
# Client side:
{:ok, ticket} = Elixiroh.EndpointTicket.from_string(str)
{:ok, conn} = Elixiroh.Endpoint.connect(endpoint, ticket, "my-proto/1")

Configuration

Elixiroh.Endpoint.bind/2 starts from a preset (:n0 for the n0 production network, :minimal for offline use) 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 blocking model

Every wait-heavy function blocks the calling process and returns the completed result, the way :gen_tcp does. Blocking is a selective receive, so it holds no scheduler and costs only a suspended process — concurrency is just process count:

conns =
peers
|> Task.async_stream(&Elixiroh.Endpoint.connect(endpoint, &1, alpn))
|> Enum.map(fn {:ok, result} -> result end)

Waits default to :infinity (accepts, reads, writes — they complete on their own once the peer acts or the connection goes away); bind/2 and connect/3 default to 5 000 ms to fail fast on unresponsive peers. Every blocking function takes a trailing timeout argument for a per-call bound. Cheap getters (alpn/1, remote_id/1, stream id/1) and queue-only operations (send_datagram/2, Connection.close/1) return immediately without blocking.

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); 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.