Distributed Erlang over iroh P2P + TLS, with a bot powers SDK
4.9 kB
147 lines
1# erl_iroh
2
3**Distributed Erlang over [iroh](https://iroh.computer) P2P**, with optional classic **TCP + TLS**, and a small **bot powers SDK**.
4
5Bots 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.
6
7```text
8 Bot SDK (powers: rpc | send | introspect | cluster | listen | eval | trace)
9 │
10 Session ── erl_dist handshake + message channel + net ticks + erpc
11 │
12 Transport
13 ├─ iroh public-key P2P (QUIC, TLS 1.3 built-in, relays + holepunch)
14 ├─ tcp classic BEAM / EPMD
15 └─ tls TCP + rustls (mTLS welcome, security pedants)
16```
17
18## Why
19
20- **NAT-friendly mesh of BEAM-adjacent bots** without babysitting IPs — dial `EndpointId`s.
21- **Wire-compatible distribution** so a bot can also talk to real OTP nodes (TCP/TLS + cookie).
22- **Powers** keep each bot least-privileged: a scout does not need `TRACE` just to `RPC`.
23
24## Quick start
25
26### Nix (flake app)
27
28```bash
29# in-process two-bot iroh handshake demo
30nix run git+https://tangled.org/nandi.uk/erl-iroh
31# or from a checkout:
32nix run .#demo
33
34# long-running listener / dialer
35nix run .#bot -- --name alpha
36nix run .#peer -- --name beta --peer <endpoint-id>
37```
38
39### Cargo
40
41```bash
42# two bots in one process (handshake over local iroh)
43cargo run --bin erl-iroh-demo
44# same as: cargo run --example two_bots
45
46# or two terminals
47cargo run --bin erl-iroh-bot -- --name alpha
48cargo run --bin erl-iroh-peer -- --name beta --peer <endpoint-id-from-alpha>
49```
50
51### As a library
52
53```rust
54use erl_iroh::bot::Bot;
55use erl_iroh::powers::Powers;
56use erl_iroh::term;
57use erl_iroh::TlsConfig;
58
59#[tokio::main]
60async fn main() -> erl_iroh::Result<()> {
61 let bot = Bot::builder("scout")
62 .cookie("super-secret-cookie")
63 .powers(Powers::worker()) // rpc|send|introspect|cluster
64 .tls(TlsConfig::client_webpki()?) // optional classic path
65 .build()
66 .await?;
67
68 bot.listen_iroh().await?;
69 println!("dial me: {}", bot.endpoint_id());
70
71 // P2P
72 // bot.connect_iroh("…endpoint id…").await?;
73
74 // Real BEAM via EPMD + TCP
75 // bot.connect_tcp("app@localhost", None).await?;
76
77 // Real BEAM via TLS
78 // bot.connect_tls("app@host", "host", 4370, "host", None).await?;
79
80 // Powers
81 // let procs = bot.processes("app@localhost").await?;
82 // bot.send("app@localhost", "logger", term::atom("hi").into()).await?;
83 // let v = bot.eval("app@localhost", "1 + 1").await?;
84
85 Ok(())
86}
87```
88
89## Powers
90
91| Power | Gates |
92|--------------|--------------------------------------------|
93| `RPC` | `rpc`, `ping` |
94| `SEND` | `send` to registered names |
95| `INTROSPECT` | `processes`, `applications`, `ets_tables`, … |
96| `CLUSTER` | `connect_iroh`, `connect_tcp`, `connect_tls` |
97| `LISTEN` | `listen_iroh`, `accept_tls` |
98| `EVAL` | `eval` (scan/parse/eval over RPC) |
99| `TRACE` | reserved for dbg/recon-style tracing |
100
101Presets: `Powers::all()`, `Powers::worker()`, `Powers::observer()`.
102
103## Security model
104
1051. **iroh path** — every connection is QUIC with TLS 1.3; peers authenticate by ed25519 `EndpointId`. The Erlang **cookie** still gates the dist handshake.
1062. **TLS path** — rustls (ring) over TCP; optional mutual TLS via `TlsConfig::server(..., Some(client_ca))` and `TlsConfig::client_with_ca(...)`.
1073. **TCP path** — plain sockets for local/dev; cookie is the only auth. Prefer TLS or iroh off-localhost.
1084. **Powers** — application-level capability checks before sensitive ops.
109
110Dev-only: `TlsConfig::client_insecure()` skips cert verification (self-signed labs).
111
112## Protocol notes
113
114- ALPN: `erl-iroh/1`
115- Handshake: OTP 23+ (`HANDSHAKE_23`, protocol v6), including `NAME_ME` dynamic names
116- RPC: `SPAWN_REQUEST` → `erpc:execute_call/4` (same approach as `erl_rpc`)
117- Ticks every 15s to keep links alive
118
119Real OTP nodes do not speak iroh natively. To bridge:
120
121- run a bot with `LISTEN` + `connect_tcp`/`connect_tls` as a gateway, or
122- put a TLS terminator / TCP proxy in front of `erl` distribution ports.
123
124## Layout
125
126```
127src/
128 lib.rs crate root, ALPN
129 bot.rs Bot + BotBuilder SDK
130 powers.rs capability bitflags
131 session.rs handshake, channel, ticks, RPC
132 stream.rs clonable futures-io duplex (iroh/tls/tcp → erl_dist)
133 transport/
134 iroh.rs Endpoint bind / dial / accept
135 tcp.rs TCP + EPMD PORT_PLEASE2
136 tls.rs rustls client/server
137 term.rs ETF helpers
138 error.rs
139examples/
140 bot.rs listen forever
141 peer.rs dial + send
142 two_bots.rs in-process mesh smoke test
143```
144
145## License
146
147MIT OR Apache-2.0