Packages

Safe, idiomatic Elixir bindings to iroh (p2p QUIC connections, dial by key)

Current section

Files

Jump to
elixiroh README.md
Raw

README.md

# elixiroh
Elixir bindings to [iroh](https://iroh.computer) — dial-by-key p2p networking
with QUIC connections, relay fallback and hole punching.
`elixiroh` wraps the [`iroh`](https://crates.io/crates/iroh) Rust crate in a
Rustler NIF. The API surface mirrors
[iroh-ffi](https://github.com/n0-computer/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`).
```elixir
def deps do
[
{:elixiroh, "~> 0.2.0"}
]
end
```
## Quick start: a supervised echo server
```elixir
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
```elixir
{: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:
```elixir
# 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:
```elixir
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_uni` only
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/1` for 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 with
`Elixiroh.Watcher.stop/1` or 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:
```elixir
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](BENCHMARKS.md).
## Contributing
Patches by mail, the sourcehut way:
```console
$ 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.