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.1.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")
```

## 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.