# iroh — Repo Map (Rank 1 candidate)

Source: `docs/research/inspiration/repos/n0-computer__iroh/` (workspace, ~4.1 MB).
Version: `iroh = 1.0.0-rc.0` (workspace), MSRV `1.91`, edition 2024.

## Brief refs: [^1][^2][^3][^4][^5][^6][^7][^8][^9]

Rank 1 candidate from `docs/research/SPT Networked Messaging Research Brief.md:13`.
Notable mismatch vs. brief: the **`discovery-local-network` feature flag no longer
exists in 1.0**. LAN/mDNS discovery has been factored out into the external crate
`iroh-mdns-address-lookup` (and `iroh-mainline-address-lookup` for DHT). The
in-tree subsystem is now `address_lookup` (`iroh/src/address_lookup.rs:46-50`).
SPT must depend on that companion crate (or implement its own `AddressLookup`)
for LAN-first discovery.

## Workspace layout

From `Cargo.toml:1-9`:

- `iroh-base/` — pure types: `EndpointId` (= `PublicKey`), `SecretKey`,
  `EndpointAddr`, `TransportAddr`, `RelayUrl`. Zero net deps. Features:
  `default = ["relay"]`, optional `key` (enables ed25519 + url + derive_more).
- `iroh-dns/` — DNS/pkarr publishing primitives used by `iroh::address_lookup`.
- `iroh-dns-server/` — runnable DNS server (only needed if you operate your own
  global pkarr/DNS endpoint discovery; SPT can ignore for LAN-first).
- `iroh/` — main crate. QUIC endpoint, relay client integration, address
  lookup, hooks, `Router` (multi-ALPN dispatcher).
- `iroh/bench/` — benchmarks (skip).
- `iroh-relay/` — relay client + server library + `iroh-relay` binary (CLI).

Patched deps (workspace `Cargo.toml:45-48`): `noq`, `noq-proto`, `noq-udp`
pinned to `n0-computer/noq` `main`. **`noq` is n0's vendored fork of `quinn`**
— the renamed QUIC stack iroh now uses (`endpoint.rs:1078` references
`self.inner.noq_endpoint().connect_with(...)`).

## Integration points for SPT

### Endpoint + identity (NodeId → EndpointId)

`iroh-base/src/key.rs:30,70,261` — `pub type EndpointId = PublicKey;`, `SecretKey`.
`SecretKey::generate()` for keypair (used in `iroh/examples/connect.rs:38`).
`PublicKey` is an Ed25519 compressed-Edwards-Y; serializes via `serde`.

`iroh/src/endpoint.rs:1110` — `Endpoint::id(&self) -> EndpointId`
`iroh/src/endpoint.rs:1126` — `Endpoint::addr(&self) -> EndpointAddr` (current)
`iroh/src/endpoint.rs:1200` — `Endpoint::watch_addr() -> impl Watcher<EndpointAddr>`
`iroh/src/endpoint.rs:1283` — `Endpoint::online().await` waits for at least one
relay handshake. **Pends forever if no relays configured** (docs:1247-1265).

### Endpoint construction (Builder)

`iroh/src/endpoint.rs:114` — `pub struct Builder`. Required: `crypto_provider`
(satisfied automatically by presets `N0` or `Minimal`).

`iroh/src/endpoint.rs:880` — `Endpoint::builder(preset) -> Builder`
`iroh/src/endpoint.rs:885` — `Endpoint::bind(preset) -> Result<Endpoint, BindError>`
`iroh/src/endpoint.rs:208` — `Builder::bind() -> Result<Endpoint, BindError>`
`iroh/src/endpoint.rs:505` — `.secret_key(SecretKey)`
`iroh/src/endpoint.rs:516` — `.alpns(Vec<Vec<u8>>)` — must set ≥1 to accept
`iroh/src/endpoint.rs:538` — `.relay_mode(RelayMode)`
`iroh/src/endpoint.rs:586` — `.address_lookup(impl AddressLookupBuilder)`
`iroh/src/endpoint.rs:566` — `.clear_address_lookup()` (start from `N0` minus DNS)
`iroh/src/endpoint.rs:344` — `.bind_addr(addr)` (advanced; pick listen port)
`iroh/src/endpoint.rs:424` — `.bind_addr_with_opts(BindOpts)` (per-iface bind)

Presets (`iroh/src/endpoint/presets.rs`):
- `presets::Empty` — nothing set (will fail bind on its own).
- `presets::Minimal:59` — only sets `crypto_provider` (ring or aws-lc-rs).
- `presets::N0:112` — Minimal + `PkarrPublisher::n0_dns()` +
  `DnsAddressLookup::n0_dns()` + `RelayMode::Default` (n0 production relays).
- `presets::N0DisableRelay:174` — N0 minus relays (publishes IPs only).

For SPT: prefer `presets::Minimal` + custom relay/lookup so we never silently
phone home to n0.computer.

### Dial by pubkey

`iroh/src/endpoint.rs:980` —
```rust
pub async fn connect(&self, addr: impl Into<EndpointAddr>, alpn: &[u8])
    -> Result<Connection, ConnectError>
```
`EndpointId: Into<EndpointAddr>` (`iroh-base/src/endpoint_addr.rs:155`), so
`ep.connect(remote_id, ALPN).await` works **if** address lookup can resolve
the id to a relay/IP. Otherwise pass a full `EndpointAddr` with
`TransportAddr::Relay(url)` and/or `TransportAddr::Ip(socketaddr)`.

`iroh/src/endpoint.rs:1020` — `connect_with_opts(addr, alpn, ConnectOptions)`
for 0-RTT (`Connecting::into_0rtt`) and per-connection `QuicTransportConfig`.

Self-connect is rejected: `endpoint.rs:1042`
(`ensure!(endpoint_id != self.id(), SelfConnect)`).

### Accept + per-ALPN dispatch

`iroh/src/endpoint.rs:1092` — `Endpoint::accept() -> Accept<'_>` (single loop).
`iroh/src/protocol.rs:1-60` — `Router::builder(ep).accept(alpn, handler).spawn()`
multiplexes incoming connections by ALPN. Handler trait: `ProtocolHandler` with
`async fn accept(&self, conn: Connection) -> Result<(), AcceptError>`.
See `iroh/examples/echo.rs:83-112` for the canonical impl.

### Address lookup (the renamed "discovery")

`iroh/src/address_lookup.rs` — module docs explain the model.
- `pkarr::PkarrPublisher` (`iroh/src/address_lookup/pkarr.rs`) — publishes our
  `EndpointAddr` to a pkarr relay. `PkarrPublisher::n0_dns()` targets
  `iroh.link`. **For SPT, replace with your own pkarr relay or skip entirely.**
- `dns::DnsAddressLookup` (`iroh/src/address_lookup/dns.rs`) — resolves
  `EndpointId` via DNS (pkarr-backed).
- `memory::MemoryLookup` (`iroh/src/address_lookup/memory.rs:74`) — manual
  in-process map. **Recommended for SPT**: hand it `EndpointAddr`s discovered
  out-of-band (SQLite spool, manual paste, mDNS bridge).
  Usage: `mem.add_endpoint_info(EndpointAddr { id, addrs: [...] })`.

**LAN discovery is out-of-tree**: `iroh-mdns-address-lookup` and
`iroh-mainline-address-lookup` (`iroh/src/address_lookup.rs:46-50`). They
implement the `AddressLookup` trait and plug in via `.address_lookup(...)`.
Not vendored in this repo — pull from crates.io.

### Relay config (default + custom)

`iroh/src/lib.rs:282,290` re-exports `RelayMode`, `RelayConfig`, `RelayMap`.

`iroh/src/endpoint.rs:137` — `From<RelayMode> for Option<TransportConfig>`,
variants: `Disabled | Default | Staging | Custom(RelayMap)`.

`iroh/src/defaults.rs:20-80` — n0 production relays
(`use1-1.relay.n0.iroh-canary.iroh.link`, west/eu/ap variants). Staging set
at `defaults.rs:87+`.

`iroh-relay/src/relay_map.rs:30` — `RelayMap` is `Arc<RwLock<BTreeMap<RelayUrl, Arc<RelayConfig>>>>`.
Build with `RelayMap::try_from_iter(["https://relay.example.org"])` (`:64`),
or `RelayMap::empty()` + `.insert(url, config)`.

Custom relay flow (matches `iroh/examples/connect.rs:48-51`):
```rust
let map = RelayMap::try_from_iter(["https://my.relay"]).unwrap();
Endpoint::builder(presets::Minimal).relay_mode(RelayMode::Custom(map)).bind()
```

### Streams (QUIC, via `noq` = forked quinn)

`iroh/src/endpoint/connection.rs` exposes:
- `:856` `open_bi() -> OpenBi<'_>` (initiator opens send+recv)
- `:841` `open_uni() -> OpenUni<'_>`
- `:877` `accept_bi() -> AcceptBi<'_>` (peer reads our stream)
- `:862` `accept_uni()`
- `:945` `send_datagram(Bytes)` — unreliable, single-packet
- `:883` `read_datagram() -> ReadDatagram<'_>`
- `:586` `remote_id() -> EndpointId` — authenticated remote pubkey
- `:893` `closed().await -> ConnectionError`
- `:935` `close(VarInt, &[u8])` — async-completed via `endpoint.close().await`

**Stream creation gotcha** (`iroh/src/lib.rs:150-158`): `open_bi` is lazy.
The remote `accept_bi` only resolves once we write data on the stream.

`SendStream` / `RecvStream` come from `noq` (re-exported at
`iroh/src/endpoint.rs:88-100`). API mirrors `quinn`: `write_all`, `finish`,
`read_to_end(limit)`, `read_chunk(limit)`, `read_exact`.

## Examples worth copying

All in `iroh/examples/`. Run via `cargo run --example <name>`.

- `connect.rs` — dial a known `EndpointAddr` over `RelayMode::Default`; prints
  CLI args so it pairs with `listen.rs`. **Best starting point for SPT dial.**
- `listen.rs` — accept loop with `RelayMode::Default`, spawn-per-connection,
  bi-stream echo. **Best starting point for SPT inbox.**
- `echo.rs` — same flow but via the `Router` + `ProtocolHandler` API; cleaner
  if SPT will host multiple ALPNs.
- `echo-no-router.rs` — same minus the `Router` (manual `accept` loop), useful
  to see what `Router` hides.
- `connect-unreliable.rs` / `listen-unreliable.rs` — QUIC datagram path (no
  reliability, low latency). Relevant for SPT pulse/ring beacons.
- `0rtt.rs` — `Connecting::into_0rtt()` for sub-RTT reconnects; read the
  security caveats (`endpoint.rs:1006-1009`).
- `transfer.rs` — large-file pump with stats; useful template for
  `EndpointInfo` propagation under load.
- `auth-hook.rs` — gate connections via `EndpointHooks::before_connect`
  (rejects per-peer). Matches SPT's perch-id allowlist need.
- `incoming-filter.rs` — drop connections pre-handshake (cheaper than
  auth-hook for flood protection).
- `screening-connection.rs` — read the first byte then accept/reject;
  pattern for SPT to peek at envelope before commiting state.
- `remote-info.rs` — pull `RemoteInfo` (path stats, RTT, last-seen) via
  hooks. Maps to SPT live-agent liveness telemetry.
- `monitor-connections.rs` — observe path changes; relevant for the
  Psyche orphan-detection use case.
- `custom-transport.rs` — implement `CustomTransport`. Requires
  `unstable-custom-transports` + `test-utils` features. **Path for SPT
  to layer mDNS/TCP under iroh without rewriting the QUIC layer.**
- `pq-only-key-exchange.rs` / `prefer-pq-key-exchange.rs` — X25519MLKEM768;
  needs `tls-aws-lc-rs` (ring backend does not support PQ).
- `search.rs` — multi-stream request/response protocol — useful template
  for SPT's `deliver`/`ring`/`reply` verbs.

## Binary size knobs

`iroh/Cargo.toml:150` — `default = ["metrics", "fast-apple-datapath", "portmapper", "tls-ring"]`.

For a leaner SPT build (`Cargo.toml:151-164`):
- `default-features = false` then pick only what you need.
- `metrics` — pulls `iroh-metrics`; off saves a chunk + atomic counter code.
- `portmapper` — UPnP/NAT-PMP/PCP; off if SPT will use relay-only or
  explicit user port-forwarding.
- `fast-apple-datapath` — macOS private API for batched sendmsg; off on
  Windows-first builds.
- `tls-ring` vs `tls-aws-lc-rs` — pick **one** crypto backend. `ring` is
  smaller and the default; `aws-lc-rs` only required for PQ KEX. Both pull
  C code; not pure-Rust.
- `platform-verifier` — only if you want OS trust store for relay TLS.
- `qlog` — leave off in release.
- `unstable-custom-transports` — only if shipping custom transport plug-in.

Workspace `Cargo.toml:19-26` defines `[profile.optimized-release]` with
`lto = true`, `panic = 'abort'`, `incremental = false`. The brief notes a
stripped binary lands at 5–15 MB (`SPT Networked Messaging Research Brief.md:156`).

Mandatory transitive deps (`iroh/Cargo.toml:23-90`): `tokio` (with at least
`io-util`, `macros`, `sync`, `rt`, `net`, `fs`, `io-std` on non-wasm),
`rustls 0.23`, `webpki-roots 1`, `ed25519-dalek 3.0-pre.7`, `hickory-resolver`,
`reqwest 0.13` (rustls-no-provider, stream), `bytes`, `derive_more`, `n0-future`,
`netwatch`. **`hickory-resolver` and `reqwest` are the heaviest** — both are
load-bearing for the address-lookup/relay stack and not behind a feature flag.

## Gotchas

- `iroh/src/address_lookup.rs:46-50` — **`discovery-local-network` no longer
  exists**; LAN/mDNS lives in external crate `iroh-mdns-address-lookup`.
- `iroh/src/lib.rs:150-158` — `open_bi` is lazy; remote `accept_bi` blocks
  until we actually write.
- `iroh/src/endpoint.rs:1244-1265` — `online()` pends forever with no
  relays configured **and** no WAN. Defer or skip on offline-first paths.
- `iroh/src/endpoint.rs:1042` — `connect` to own `EndpointId` returns
  `SelfConnect` error (not the no-op SPT might expect).
- `iroh/src/endpoint.rs:894` — close ops log-warn-and-drop when endpoint
  already closed; do not rely on close errors for state machine transitions.
- `iroh/src/endpoint.rs:2570` — internal `// TODO: Maybe not panic if this
  is not true?` near a debug assertion — touchy area, do not subclass.
- `iroh/src/socket/transports/ip.rs:177` — `// TODO: update when UdpSocket
  under the hood rebinds automatically` — SPT may see network-change quirks
  on Windows interface flips until netwatch upgrades.
- `iroh/src/socket.rs:947` — `// NOTE: we can end up with a zero port if
  netwatch::UdpSocket::socket_addr fails` — guard SPT advertisement against
  port=0 addresses.
- `iroh/src/socket/transports/relay/actor.rs:1253` — hard-coded magic 64 for
  per-client relay send queue depth — relay overload tuning lever.
- Workspace pins `noq` / `noq-proto` / `noq-udp` to git `main`
  (`Cargo.toml:45-48`). **SPT must add the same `[patch.crates-io]` entries
  to its workspace** or cargo will resolve to nonexistent crates.io versions.
- `iroh/src/endpoint.rs:14` (lib re-export at `lib.rs:18`) uses `EndpointId`
  / `EndpointAddr` — note **the 1.0 rc renamed `NodeId` → `EndpointId` and
  `NodeAddr` → `EndpointAddr`** vs. the brief's vocabulary.
- Relay-server feature flag `iroh-relay/Cargo.toml:135-157` pulls in
  ACME (`tokio-rustls-acme`), `rcgen`, `tokio-websockets`, `serde_json`,
  `toml` — **only enable `iroh-relay/server` if SPT itself self-hosts a
  relay**; the client side is feature-gate-free.

## Relay self-hosting

`iroh-relay` crate is both library and binary:

- Binary: `iroh-relay/src/main.rs` (`iroh-relay/Cargo.toml:177-180`,
  `[[bin]] required-features = ["server"]`). Build with
  `cargo build -p iroh-relay --features server --release`.
- CLI flags (`main.rs:42-54`): `--dev` (plain HTTP on port 3340, no TLS,
  **no QUIC address discovery**), `--config-path <toml>`.
- Config schema docs in `iroh-relay/README.md:30-65`. Example minimal
  TOML for TLS + QUIC discovery is in README:51-58. Workspace ships
  `example.config.toml` (currently 2 lines — just a `[[relays]]` URL,
  not a server config; the real config lives in README + main.rs CLI parser).
- Library entry: `iroh-relay/src/server.rs:102` `pub struct ServerConfig`
  with fields `relay: Option<RelayConfig>`, `quic: Option<QuicConfig>`,
  `metrics_addr`. `RelayConfig::new(http_bind_addr)` at `server.rs:146`.
- Access control: `AccessConfig::Restricted(callback)` lets SPT gate by
  `EndpointId` per `ClientRequest` (`server.rs:157-205`). Default
  `AccessConfig::Everyone`.
- TLS: Manual / LetsEncrypt / Reloading certs (`main.rs:56-62`,
  `server.rs:42-44`, `tokio-rustls-acme` integration).
- QUIC address discovery port: `DEFAULT_RELAY_QUIC_PORT` exported from
  `iroh-relay/src/defaults.rs` (re-exported at `iroh/src/defaults.rs:7`).
- Dockerfile: `docker/Dockerfile` (rust:alpine + cargo-chef) — produces a
  small static binary suitable for self-hosting alongside SPT.

In-process test relay: `iroh::test_utils::run_relay_server()` requires the
`test-utils` feature on `iroh` (`iroh/Cargo.toml:153`). Disable client TLS
verification with `Endpoint::ca_roots_config(CaRootsConfig::insecure_skip_verify())`
(`iroh-relay/README.md:73-78`).

## Tests to study

- `iroh/tests/integration.rs:1-154` — **only** integration test in tree.
  End-to-end client/server: bind two endpoints with `RelayMode::Staging`,
  wait for `online()`, dial by `server.id()` (pure endpoint-id), bi-stream
  echo. Polls a `PkarrResolver::n0_dns()` to confirm address publication
  before connect (`integration.rs:87-116`). Hits real n0 staging
  infra — SPT will want a vendored local-only equivalent.
- `iroh/examples/echo.rs:71-112` — closest in-tree pattern to SPT's
  perch-listener loop (single ALPN, accept-then-spawn).
- `iroh-relay/tests/` — relay protocol-level tests (look for
  `tokio-websockets` patterns). The relay client APIs are exercised via the
  in-tree `noq`/quic path inside `iroh/src/socket/transports/relay/`.
- `iroh-relay/src/server/testing.rs` (gated on `test-utils` feature) — helpers
  for spinning up an in-process relay; same code path
  `iroh::test_utils::run_relay_server` calls.

## Suggested SPT integration shape

1. Vendor `iroh = { version = "1.0.0-rc.0", default-features = false, features = ["tls-ring"] }`
   and mirror the workspace `[patch.crates-io]` for `noq*`.
2. Use `presets::Minimal` (or hand-build a `Builder`) — never `presets::N0`
   (avoids silent n0.computer dependency).
3. Identity = persisted `SecretKey` (32 bytes) in `%LOCALAPPDATA%\spt\owlery\`
   — replaces today's `info.json` perch identity for cross-host messaging.
4. Discovery = `MemoryLookup` seeded from the SPT SQLite spool +
   (optionally) `iroh-mdns-address-lookup` for LAN.
5. Relay = `RelayMode::Custom(RelayMap::try_from_iter([user_relay_url]))`,
   default to `RelayMode::Disabled` for LAN-only mode.
6. Wire `Router::builder(ep).accept(b"spt/deliver/1", ...).accept(b"spt/ring/1", ...)`
   one ALPN per SPT verb — mirrors the existing command vocabulary.
7. Map SPT envelope to a single `open_bi` per message; finish on send-side
   to terminate stream; receiver reads with `read_to_end(MAX_ENVELOPE)`.
8. Optional self-hosted relay: build `iroh-relay` with `--features server`
   and ship a sibling binary next to `owl.exe`.
