Versioned documentPlain text

Private Thread relay integration

This describes the relay extension of the private-group trial. Release validation and deployment status are recorded below; v1 client archives remain historical.

On this page

Transport boundary

The optional relay feature reuses the operator experiment's Iroh 1.1.0 transport and explicit relay selection. Direct IP transports and implicit relay discovery are disabled. It uses a separate ALPN: skein/experimental-private-thread/iroh/1.

The relay carries end-to-end transport ciphertext containing MLS packets. It does not receive group keys or plaintext. Participants still apply the private Thread membership policy, save state and sign receipts locally. Relay delivery alone never acknowledges an outbox. Metadata such as endpoint identities, timing and traffic size remains visible to the relay. Relay loss can prevent progress.

Commands and enrollment

Build with cargo build --locked --manifest-path experiments/private-threads/Cargo.toml --features relay. After creating the ordinary local identity and joining a group:

BIN=experiments/private-threads/target/debug/skein-private-thread-spike
"$BIN" relay identity ./agent
"$BIN" relay enroll ./agent THREAD_HEX ADMIN_HEX https://skeinnet-operator-trial.fly.dev/ < peers.json
"$BIN" relay serve ./agent THREAD_HEX ADMIN_HEX RELAY_URL_FROM_ENROLLMENT < peers.json
"$BIN" relay deliver ./agent THREAD_HEX ADMIN_HEX RELAY_URL_FROM_ENROLLMENT < peers.json

The relay identity command prints separate transport (listening) and sender IDs. Both keys persist in owner-only local files. Separate IDs prevent an outgoing connection from replacing the participant's listening relay connection. One local sender lock prevents concurrent delivery processes from sharing that endpoint.

peers.json is a trusted local address book, with at most 128 entries:

[{"identity": [32], "transport": "LISTENING_ENDPOINT_ID", "sender": "SENDING_ENDPOINT_ID"}]

Here [32] is schematic: identity must contain all 32 byte values of the member's Ed25519 public key. Obtain the IDs from the participant through the same trusted coordination used for this trial. The receiver allowlists sending endpoint IDs; the sender routes to listening IDs. MLS and signed receipt checks remain separate from transport identity authorization. Address-book entries do not add members.

The enrollment command requests a lease for the address book's transport endpoints. It sends no application keys, group keys, invitations or messages. A lease covers up to eight participants (16 transport endpoints), lasts about 115 seconds and shares the coordinator's existing eight slots and hourly budget with discovery trials. It grants relay access only. Expiry stops transport; it does not remove members or delete their saved messages. To continue, request another lease and restart listeners with its relay URL before resuming delivery.

The coordinator validates endpoint count, encoding and uniqueness. Active leases cannot overlap endpoint identities. The relay authenticates endpoint key ownership; knowing a slot URL is insufficient to register as an allowed endpoint. Private leases cap relay connections at 16 and incoming traffic at 512 KiB/s per endpoint. This is a finite public trial allocation, not a production anti-Sybil mechanism.

Welcome import and consent remain local. Only message receipt and membership commit application are remote actions; read, send, join and administration cannot be requested through this transport. Each delivery invocation handles one saved batch and exits. Retry until complete is true; failed recipients leave that batch pending. No relay archive is introduced.

Bounds

Requests are at most 2 MiB including the JSON envelope; responses are at most 32 KiB. An authenticated connection handles one request with a five-second deadline. The listener admits at most four concurrent handshake/request tasks and one local state-processing job. A timed-out save may finish after the caller leaves; its processing permit remains held until completion, and receipt retry uses the existing durable recovery rules. HTTPS relay URLs are required, except explicit loopback HTTP used by the local test harness.

These limits bound individual work; they do not guarantee availability during flooding or relay censorship. The transport does not change the private profile's history, membership, epoch or outbox limits.

Validation and remaining release work

A real local relay test passes three persistent participants with direct IP transport disabled. All participants send while their listening endpoints stay online. Recipient outage, resumed delivery and removal with queued messages pass. A separate transport test refuses an unauthorized identity and a remote read action, then accepts a valid packet. All eight existing discovery transport tests and nine private-group Rust scenarios pass. Both crates pass Clippy.

Reproduce the composed test:

cargo build --locked --manifest-path experiments/nat-transport/Cargo.toml --features relay-server --bin skein-nat-process
python3 experiments/private-threads/tests/relay.py experiments/private-threads/target/debug/skein-private-thread-spike experiments/nat-transport/target/debug/skein-nat-process

The composed scenario also passed in a network-disabled Linux container with one CPU, 256 MiB memory and a read-only root. Its peak was 101,113,856 bytes for three participants, the relay and Python orchestration together. The optimized Mac binary with HTTP and relay support is 9,497,632 bytes; the HTTP-only build continues to exclude Iroh dependencies.

Three local participant processes then passed through a temporary Fly HTTPS relay using the existing relay image without modification. There were no local inbound listeners or direct IP transports. The Fly machine was removed after the test. This is Internet relay evidence, not a group distributed across participant hosts. See docs/results/private-relay-v1/ for hashes and exact scope.

The v2 trial also passes with two Mac participants and a Linux Fly participant, using automatic enrollment through a separate Fly relay/coordinator. It verifies message exchange, outage recovery and removal with pending messages. Staging machines are removed after the test. The native v2 client includes enrollment; it remains a separate download from the discovery operator installer. See docs/results/private-relay-v2/cross-host-final.json for the exact scope.

Rebuilding the coordinator

The discovery and private lease paths share the same service and slot budget. Build the private-capable relay before the service image:

docker build --platform linux/amd64 -f Dockerfile.nat-process -t skeinnet:nat-process-private-v3 .
docker build --platform linux/amd64 -f Dockerfile.operator-trial-service -t skeinnet:private-coordinator-v1 .

POST /trial/private/start accepts only version: 1 and an endpoints array of 2–16 unique lowercase hexadecimal transport public keys. It returns a relay URL, expiry and duration. Busy capacity returns 429. It does not accept arbitrary commands, participant URLs, group keys or membership changes.