Repository navigation
v0.4.0 — a Shrine listens on a port of its own
Reaching a Shrine over TCP no longer requires the caller to write an accept loop, and mis-addressing one no longer looks like a broken rite.
What's new
node.HostShrine(signet, handler, feeds, relics, conduit);
var endpoint = node.ListenForPilgrims(); // 0 → an OS-assigned portA Pilgrim dials it and pins the address. Nothing else:
var vessel = await TcpVessel.ConnectAsync(host, port, cancellationToken: ct);
await using var shrine = await Pilgrimage.OverVesselAsync(vessel, signetSigil, network, suite, enableToll, ct);| API | Change |
|---|---|
CupriNode.ListenForPilgrims(port = 0) |
New. Binds a port serving the hosted Shrine; returns the bound endpoint. |
CupriNode.ShrineEndPoint |
New. The endpoint being served, or null. |
CupriNodeOptions.MaxConcurrentPilgrimages |
New, default 256. |
Additive only — no existing signature changed. The listener is closed when the node is disposed.
The problem it removes
A Shrine was reachable two ways: a browser DataChannel, which a node routes in by itself, and a vessel the caller owns. Over TCP only the second applied, so every consumer wrote the same VesselListener loop — and anyone who guessed instead found the node's L1 port, which does not serve Shrines and did not say so.
That port answers with the node's own Sigil. So pinning the site's Signet fails correctly, but pinning the node's Sigil succeeds — into a healthy session with no Shrine behind it, where every rite answers with a closed stream. It reads as a broken rite and is not one.
On the Shrine port that mistake cannot complete, because the host has no Sigil to offer there: the failure is loud, at the handshake. And a node hosting no Shrine never opens the port, so dialling it is refused outright rather than answered with nothing.
Why a separate port, not the L1 one
The identity to present must be chosen before the Noise handshake begins, so a shared port would need the client to signal "I am here for the site" in cleartext. That is the same pre-Noise selector multi-Shrine hosting refuses, so that the network never learns which site a connection is for. A dedicated port needs no selector at all.
Visits are bounded, not handshakes
MaxConcurrentPilgrimages holds a slot for the whole visit rather than just the handshake, because a visitor may keep a page fetch, a live feed and a raw session open for as long as it likes — that, not handshake capacity, is what a site spends. (The L1 loop frees its slot after the handshake, which is right for what it protects.) The slot is taken before the accept, so a flood queues in the kernel backlog instead of stalling the loop, and the subnet fence applies as it does on the L1 path.
Still supported
The caller-owned vessel path is not deprecated. It remains the entry for Tor, for tests, and for a WASM client that cannot bind a socket at all.
Full design: design/shrines.md → How a Pilgrim actually reaches a Shrine.
447 unit tests green.