Current section
Files
Jump to
Current section
Files
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.3.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)
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
```elixir
{: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:
```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.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:
```elixir
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
- **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`); `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.