The access control logic in iroh-rings did not start as a standalone library. It was born inside this project, tightly coupled to ringdrop’s own types and assumptions. Only later, once the model had stabilized, did I extract it into a separate crate through a generalization refactor — and that separation turned out to be one of the better decisions I made along the way.

This post is about the project that came first.

That is ringdrop.

The interesting part is probably how three independent layers compose together into something useful and coherent. Each layer does one thing and hands off to the next.

Three layers, one node

The stack looks like this:

    ┌──────────────────────────────────────────────────┐
    │  iroh-rings  (who can access what)               │
    ├──────────────────────────────────────────────────┤
    │  iroh-blobs  (what is stored, how to transfer)   │
    ├──────────────────────────────────────────────────┤
    │  iroh        (how peers connect)                 │
    └──────────────────────────────────────────────────┘

iroh handles connectivity: QUIC transport, NAT hole punching, peer discovery, connection migration. Two nodes behind separate NATs can establish a direct data path — the relay is used for initial signaling, but once the hole is punched traffic flows peer-to-peer, as I described in the hole punching post.

iroh-blobs handles storage: content-addressed blobs identified by their BLAKE3 hash, encoded with BAO for efficient partial verification. I wrote about BAO earlier — the short version is that you can verify any chunk of a file without downloading the whole thing first.

iroh-rings handles access control: ring membership, resource associations, and the authorization check before any blob is served.

Node<R> is the struct that owns all three. It is generic over R: Registry, meaning you can swap the backend without touching anything else — the access control policy is entirely determined by whatever Registry you pass in.

RingGate: enforcement at the right layer

The RingGate is where access control is enforced. It sits between the incoming blob request and the blob store: before a blob is served to a remote peer, the gate checks the registry.

The check happens at request time, not at import time. This is intentional — ring membership can change after a file is imported. If I add a peer to a ring, they should immediately be able to access all resources associated with that ring, without re-importing anything. The gate reads the current state of the registry on every request.

A failed check is silent from the requester’s perspective: the request is dropped, no error detail is returned. Leaking why access was denied would tell an unauthorized peer which resources exist — which is information you probably do not want to give out.

ShareTicket: out-of-band sharing

To download a file, a peer needs a ShareTicket. It encodes everything required: the blob hash and the address of the node serving it.

The idea is similar to sendme, the file transfer tool by the iroh team: a self-contained ticket is all a receiver needs to locate and request the content. The difference is what happens when the request arrives — ringdrop’s RingGate checks the registry before serving anything, silently rejecting peers that do not belong to an authorized ring. The ticket gets you to the door; the ring decides whether it opens.

The ticket is handed out of band — over a chat message, an email, whatever. The design is deliberate: ringdrop does not have a discovery mechanism. If you have the ticket, you know the resource exists and where to get it. If you do not have the ticket, you cannot even ask. Combined with the ring check, this gives two independent layers of access control: possession of the ticket, and membership in the right ring.

Daemon and IPC

A file drop node needs to keep running between operations. I did not want to re-initialize the iroh endpoint and reload the blob store on every CLI invocation — that would be slow and would lose in-memory connection state.

So ringdrop runs as a daemon: a background process that owns the Node and listens on a local TCP socket. The CLI tool (rdrop shell command) connects to it, sends a single newline-terminated JSON request, and reads back a stream of JSON events until the operation completes or fails.

The protocol is intentionally thin. Each Op variant maps to one operation — import a file, create a ring, add a peer, download a blob. The daemon dispatches the request to the node and streams progress events back. The CLI renders them.

I chose TCP over a Unix socket for platform portability (ringdrop runs on Linux, macOS, and Windows), and JSON over a binary protocol because the message volume is low and debuggability matters more than throughput in this phase. Running nc localhost <port> and typing a request by hand is my escape hatch during development :)

CLI

A typical session with rdrop looks like this:

# Start the daemon (once, keeps running)
rdrop daemon start

# Create a ring named "team" and add a peer to it
rdrop ring create team
rdrop ring add team <peer-endpoint-id>

# Import a file and associate it with the ring
rdrop import ./report.pdf --ring team

# The share ticket is printed — send it out of band
# rdrop://abcdef20...

# On the other side, a peer with the ticket downloads the file
rdrop receive <ticket>

The peer on the other side must be a member of the team ring on the serving node, otherwise the download is silently rejected.

What’s next

There is plenty still to build. If you want to see what’s coming or have a use case to propose, the issue tracker is the right place.

Contributions are very welcome — whether that’s code, documentation, or just opening an issue with something you’d find useful.

Refs

GitHub repo: https://github.com/rikettsie/ringdrop

Crate: https://crates.io/crates/ringdrop

Docs: https://docs.rs/iroh-rings/latest/ringdrop/

iroh-rings GitHub repo: https://github.com/rikettsie/iroh-rings