Skip to content

refactor: pure core for admission and authorization, and expose requires a grantee - #10

Merged
cablehead merged 6 commits into
mainfrom
refactor/sans-io-core
Aug 18, 2026
Merged

refactor: pure core for admission and authorization, and expose requires a grantee#10
cablehead merged 6 commits into
mainfrom
refactor/sans-io-core

Conversation

@cablehead

@cablehead cablehead commented Aug 18, 2026

Copy link
Copy Markdown
Owner

Extracts the security decisions from peer.rs into a pure core, and builds a
test suite around them. 19 tests to 52, all under a second, no network.

Includes one behaviour change: expose no longer grants to every known peer
when you leave --to off. See step 3.

Why

peer.rs is 1010 lines and holds every security decision in the system. It had
no tests, because testing it meant standing up two daemons, two iroh endpoints,
a relay, and a network.

The 19 existing tests all lived in grants, enroll, surface, and
resolver. Those are the four modules that happen to be pure. Deleting the
authorization check at peer.rs:370 would have kept the suite green.

Where the test list came from

docs/scenarios.md is added here. It works two flows end to end, a shared build
box and a laptop booting a vibenv, and states what has to be true for each. Four
of those statements became the test list:

  • a link is mutual, and one side saying yes is not enough
  • an invitation is not access, so being admitted grants no port
  • naming is local, and neither end has to agree with the other
  • a peer arrives by code or by key, and the record keeps them apart

Writing it is also what turned up the ambient grant in expose. The file
describes a proposed CLI (invite/accept) that nothing implements yet, and is
marked as such. That rename is separate work.

What changed

1. Injectable endpoint. Daemon::new hardcoded presets::N0: public relays
and n0 DNS discovery, so any test that built a daemon reached the network. Split
into new (production behaviour unchanged) and with_endpoint, which takes an
endpoint the caller already bound.

2. Pure core. src/core/session.rs owns the grant table, the enrollment
tokens, and the admitted peer set. It performs no IO: methods take an event and
return actions the shell carries out. Four decisions moved across:

  • admission of an inbound connection
  • the verdict on an enrollment claim
  • authorization of a tunnel request
  • what ports a peer is told about

peer.rs no longer holds Grants or Tokens. 20 unit tests cover the
decisions directly, in microseconds.

3. expose requires --to or --all. It used to default to every known
peer, which is a default-allow inside a system whose ADR opens with "default
deny, no grant, no access". Adding a peer so you could reach its ports silently
made it a recipient of everything you exposed later. --all still exists and
still means every peer known at that moment, not a standing rule. Six parse
tests pin the argument group, including that repeated --to is accepted and
--to x --all is not.

4. Live tests over loopback. Seven tests run two real daemons with relays
disabled and a MemoryLookup seeded with each other's bound sockets. They cover
what the pure core cannot reach: bytes crossing a granted tunnel, revocation
tearing a binding down, an unenrolled peer being refused, and --host being
honoured.

Two bugs found on the way

A peer added while its daemon was down was lost. add_peer dialed before
creating any record, so a failed dial left nothing behind: no peer, no retry
loop. docs/scenarios.md promises the opposite. Fixed by recording first and
letting the connection loop dial, which also made the first dial immediate
instead of waiting out a backoff.

A transient bind failure left a port dark forever. bind_one logged and
dropped on the theory that the next announce would retry, but a peer whose port
set does not change never re-announces. Now retried five times, 200ms apart.

Verification

52 tests, 0.55s, 10 consecutive runs with no flakes.

The suite was mutation-checked rather than assumed. Replacing the grant check
with if true fails 5 tests. Setting BIND_ATTEMPTS to 1 fails the bind-retry
test. Both were confirmed, then reverted.

Zero egress is inferred from iroh's config surface (relays disabled, address
lookup cleared, port mapping disabled), not observed: unshare -n was not
permitted in this environment and strace was absent.

Not in scope

The CLI rename worked out in docs/scenarios.md (invite/accept, dropping
ticket and label) is separate work. This branch keeps today's command names
apart from the expose grant change.

Phase 2 of the sans-io migration is also separate: moving reconciliation and
projection into the core, which is what allows bind_lock to be deleted.

@cablehead cablehead changed the title refactor: pure core for admission and authorization, plus a test suite refactor: pure core for admission and authorization, and expose requires a grantee Aug 18, 2026
@cablehead
cablehead marked this pull request as ready for review August 18, 2026 15:45
@cablehead
cablehead merged commit 728a587 into main Aug 18, 2026
1 check passed
@cablehead
cablehead deleted the refactor/sans-io-core branch August 18, 2026 15:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant