Distributed Erlang over iroh P2P + TLS, with a bot powers SDK
0

Configure Feed

Select the types of activity you want to include in your feed.

2 1 0

Clone this repository

https://git.vm.fail/nandi.uk/erl-iroh https://git.vm.fail/did:plc:sky5lt3vipqcc6uxl2orvw6u
ssh://git@knot1.tangled.sh:2222/nandi.uk/erl-iroh ssh://git@knot1.tangled.sh:2222/did:plc:sky5lt3vipqcc6uxl2orvw6u

For self-hosted knots, clone URLs may differ based on your setup.


README.md

erl_iroh#

Distributed Erlang over iroh P2P, with optional classic TCP + TLS, and a small bot powers SDK.

Bots import the crate, get an iroh endpoint (public-key dialing, holepunch, QUIC), speak the real Erlang distribution handshake (erl_dist), and unlock capability-gated operations: RPC, send, introspect, cluster, eval, trace.

  Bot SDK  (powers: rpc | send | introspect | cluster | listen | eval | trace)
  Session  ── erl_dist handshake + message channel + net ticks + erpc
  Transport
    ├─ iroh   public-key P2P (QUIC, TLS 1.3 built-in, relays + holepunch)
    ├─ tcp    classic BEAM / EPMD
    └─ tls    TCP + rustls (mTLS welcome, security pedants)

Why#

  • NAT-friendly mesh of BEAM-adjacent bots without babysitting IPs — dial EndpointIds.
  • Wire-compatible distribution so a bot can also talk to real OTP nodes (TCP/TLS + cookie).
  • Powers keep each bot least-privileged: a scout does not need TRACE just to RPC.

Quick start#

Nix (flake app)#

# in-process two-bot iroh handshake demo
nix run git+https://tangled.org/nandi.uk/erl-iroh
# or from a checkout:
nix run .#demo

# long-running listener / dialer
nix run .#bot  -- --name alpha
nix run .#peer -- --name beta --peer <endpoint-id>

Cargo#

# two bots in one process (handshake over local iroh)
cargo run --bin erl-iroh-demo
# same as: cargo run --example two_bots

# or two terminals
cargo run --bin erl-iroh-bot  -- --name alpha
cargo run --bin erl-iroh-peer -- --name beta --peer <endpoint-id-from-alpha>

As a library#

use erl_iroh::bot::Bot;
use erl_iroh::powers::Powers;
use erl_iroh::term;
use erl_iroh::TlsConfig;

#[tokio::main]
async fn main() -> erl_iroh::Result<()> {
    let bot = Bot::builder("scout")
        .cookie("super-secret-cookie")
        .powers(Powers::worker()) // rpc|send|introspect|cluster
        .tls(TlsConfig::client_webpki()?) // optional classic path
        .build()
        .await?;

    bot.listen_iroh().await?;
    println!("dial me: {}", bot.endpoint_id());

    // P2P
    // bot.connect_iroh("…endpoint id…").await?;

    // Real BEAM via EPMD + TCP
    // bot.connect_tcp("app@localhost", None).await?;

    // Real BEAM via TLS
    // bot.connect_tls("app@host", "host", 4370, "host", None).await?;

    // Powers
    // let procs = bot.processes("app@localhost").await?;
    // bot.send("app@localhost", "logger", term::atom("hi").into()).await?;
    // let v = bot.eval("app@localhost", "1 + 1").await?;

    Ok(())
}

Powers#

Power Gates
RPC rpc, ping
SEND send to registered names
INTROSPECT processes, applications, ets_tables, …
CLUSTER connect_iroh, connect_tcp, connect_tls
LISTEN listen_iroh, accept_tls
EVAL eval (scan/parse/eval over RPC)
TRACE reserved for dbg/recon-style tracing

Presets: Powers::all(), Powers::worker(), Powers::observer().

Security model#

  1. iroh path — every connection is QUIC with TLS 1.3; peers authenticate by ed25519 EndpointId. The Erlang cookie still gates the dist handshake.
  2. TLS path — rustls (ring) over TCP; optional mutual TLS via TlsConfig::server(..., Some(client_ca)) and TlsConfig::client_with_ca(...).
  3. TCP path — plain sockets for local/dev; cookie is the only auth. Prefer TLS or iroh off-localhost.
  4. Powers — application-level capability checks before sensitive ops.

Dev-only: TlsConfig::client_insecure() skips cert verification (self-signed labs).

Protocol notes#

  • ALPN: erl-iroh/1
  • Handshake: OTP 23+ (HANDSHAKE_23, protocol v6), including NAME_ME dynamic names
  • RPC: SPAWN_REQUESTerpc:execute_call/4 (same approach as erl_rpc)
  • Ticks every 15s to keep links alive

Real OTP nodes do not speak iroh natively. To bridge:

  • run a bot with LISTEN + connect_tcp/connect_tls as a gateway, or
  • put a TLS terminator / TCP proxy in front of erl distribution ports.

Layout#

src/
  lib.rs          crate root, ALPN
  bot.rs          Bot + BotBuilder SDK
  powers.rs       capability bitflags
  session.rs      handshake, channel, ticks, RPC
  stream.rs       clonable futures-io duplex (iroh/tls/tcp → erl_dist)
  transport/
    iroh.rs       Endpoint bind / dial / accept
    tcp.rs        TCP + EPMD PORT_PLEASE2
    tls.rs        rustls client/server
  term.rs         ETF helpers
  error.rs
examples/
  bot.rs          listen forever
  peer.rs         dial + send
  two_bots.rs     in-process mesh smoke test

License#

MIT OR Apache-2.0