You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Now that Rings has signed online-node registration, add an onion-routing layer that can build multi-hop private paths over the existing WebRTC overlay without turning every online node into an exit.
This RFC proposes three separate concepts:
Presence directory: the existing signed online-node registry (ONLINE_NODES_TOPIC) remains the liveness and coarse candidate source.
Onion relay: a node that opts into forwarding onion cells but does not provide egress. Relay-only nodes must be valid first-class participants.
Onion exit: an application-layer capability advertised through a separate exit registry. Exit policy and service exposure must not be encoded as core DHT routing behavior.
The implementation should live in crates/node / extension protocol code, not in rings-core Chord routing. Chord remains the storage and discovery substrate; onion routing is an application protocol above it.
Current code context
Relevant surfaces on current master:
crates/node/src/online.rs defines signed OnlineNodeDescriptor values with did, public_key, network_id, node_type, capabilities, optional endpoint_hint, heartbeat_at_ms, expires_at_ms, and a signature.
crates/node/src/registration.rs already generalizes periodic DHT-backed registration through RegistrationTask and DhtRegistrationPublisher.
crates/node/src/processor.rs publishes and looks up online nodes with publish_online_node_descriptor() / lookup_online_nodes(include_expired).
crates/core/src/message/protocols/relay.rs / crates/core/src/message/payload.rs implement the existing transparent overlay relay path. That path records forwarding path metadata and is not an onion route.
crates/node/src/extension/protocols/relay.rs implements application TCP/UDP relay/tunnel behavior. It is useful exit-side prior art, but its peer identity/session semantics are direct-peer semantics, not anonymous onion-circuit semantics.
crates/core/src/message/e2e.rs implements direct endpoint ElGamal E2E frames. That is endpoint encryption, not layered hop encryption, and it deliberately is not an AEAD/KEM-DEM construction.
Problem assessment
The existing online registry solves only the first directory problem: discovering live signed nodes on the same network. It does not answer:
whether a node has installed the onion protocol namespace;
whether a node consents to relay onion cells;
whether a node consents to act as an exit;
which exit services or targets are allowed;
what abuse/rate-limit policy applies to exit traffic;
how a sender builds a route without leaking the whole route to the ordinary MessageRelay.path machinery.
The current overlay send path also has an important privacy boundary: if an onion packet is sent to the next hop through ordinary Chord forwarding, intermediate Chord nodes may observe a transparent route to the guard. Onion cell forwarding should therefore prefer or require direct next-hop transport connections after the circuit is selected. Chord/DHT should be used for directory lookup and bootstrapping, not as the per-hop onion forwarding path.
Goals
Add an onion routing protocol that supports relay-only nodes and exit-capable nodes as distinct roles.
Keep exit advertisement as an application-layer registry, separate from core presence and core Chord routing.
Build routes from live signed presence descriptors plus signed exit descriptors.
Provide a concrete path-selection algorithm with safe small-network behavior.
Use layered per-hop authenticated encryption, not the transparent MessageRelay path and not the existing direct E2E stream format.
Preserve native/browser portability where possible, with exit transports explicitly declaring what runtime they support.
Keep the first implementation testable with dummy transport and small deterministic topologies.
Non-goals
Do not claim Tor-equivalent anonymity in v1. WebRTC metadata, timing, message size, Sybil behavior, and endpoint hints still matter.
Do not make every online node an exit.
Do not put exit policy into rings-core or Chord routing.
Do not route onion data by exposing the full path in MessageRelay.path.
Do not implement public internet egress by default. Exit should be explicit, opt-in, policy-bound, and disabled by default.
Registry design
Presence directory
Keep the existing ONLINE_NODES_TOPIC as the liveness source. The onion route builder uses lookup_online_nodes(false) to obtain live, same-network candidates and verify descriptors.
Presence should not become the exit policy registry. OnlineNodeDescriptor.capabilities may be useful for coarse capability labels, but exit policy is too specific and too high-risk to live there.
Exit registry
Add an application-layer DHT registry for onion exits, for example ONION_EXITS_TOPIC = "onion_exits", owned by the onion extension/node layer.
Suggested descriptor shape:
pubstructOnionExitDescriptorBody{pubdid:Did,pubpublic_key:VerificationPublicKey,pubnetwork_id:u32,pubnode_type:OnlineNodeType,pubservices:Vec<OnionExitService>,pubpolicy:OnionExitPolicy,pubstarted_at_ms:u128,pubheartbeat_at_ms:u128,pubexpires_at_ms:u128,pubversion:String,}pubstructOnionExitDescriptor{// same body fieldspubsignature:MessageVerification,}
Policy should be explicit and signed. At minimum:
service names the exit is willing to expose;
transport kind: TCP, UDP, WebTransport, HTTP-like request, or protocol-specific service;
allowlist/denylist shape for target addresses when raw socket egress is supported;
max circuit count / stream count / bytes per time window;
optional node/runtime constraints, e.g. native-only exit for OS sockets, browser WebTransport-only exit for browser.
The registry publisher should reuse the existing RegistrationTask / DhtRegistrationPublisher pattern:
heartbeat refresh publishes the newest signed exit descriptor;
stale self descriptors are tombstoned on refresh;
expired descriptors are filtered by default on lookup;
Relay-only nodes simply do not publish an exit descriptor.
Relay discovery
Initial relay candidate discovery can use live presence descriptors plus a successful onion-protocol probe/handshake. If relay participation needs explicit public opt-in beyond installing the onion protocol, add a second application-layer onion_relays registry with a smaller descriptor. That should still be in the onion application layer, not rings-core.
Route-selection algorithm
Inputs:
requested exit service or policy requirement;
live online-node descriptors from lookup_online_nodes(false);
live exit descriptors from lookup_onion_exits(service, include_expired = false);
local peer quality measurements where available;
configured path length, defaulting to 3 hops: guard, middle, exit.
Algorithm:
Verify every presence and exit descriptor signature and network_id.
Filter exits by requested service/policy, liveness, runtime support, and local denylist.
Filter relay candidates by liveness, same network, not self, not the selected exit, and onion protocol support/probe success.
Weight candidates with local PeerQuality: prefer healthy, keep unknown eligible, avoid degraded unless the network is too small.
Sample without replacement. No DID may appear twice in a route.
Prefer 3-hop routes when enough distinct candidates exist.
Permit 2-hop or 1-hop development routes only with explicit config such as allow_short_onion_paths; never silently weaken path length in production mode.
Establish direct transport to the guard before sending the first onion cell. Each relay should establish or verify direct transport to its next hop before forwarding.
On failure, tear down the circuit and rebuild with a different candidate set. Do not patch a live circuit into a new route in v1.
It must not learn the origin client, final exit service, full route, or inner payload. The guard necessarily sees the client as previous. The exit necessarily sees the requested exit service and the previous relay, but should not learn the original client from the onion layer.
Cryptography requirements
Do not reuse message::e2e as the onion layer. It is endpoint-oriented ElGamal stream encryption and relies on the signed MessagePayload envelope for integrity. Onion layers need per-hop authenticated encryption and key separation.
Recommended implementation plan:
Define an OnionCipher abstraction first so the protocol state machine can be tested independent of the concrete crypto.
Use per-circuit ephemeral key agreement and derive separate forward/backward keys per hop.
Use AEAD for each layer/cell, with associated data covering protocol version, circuit id, direction, hop counter, and command type.
Include a maximum hop count and TTL to prevent loops and unbounded state.
Pad cells to configured buckets in v1; fixed-size Tor-style cells can be a later hardening step.
Concrete crypto choice should be decided before implementation. Candidates:
secp256k1 ECIES/HKDF/AEAD using existing dependency surface where suitable;
X25519 plus ChaCha20-Poly1305 if adding dependencies is acceptable and wasm support is clean.
The chosen construction must be available in native and wasm builds or be cleanly feature-gated.
Direct next-hop forwarding requirement
The current extension Scope::send(to, payload) sends over the normal overlay envelope path. For onion forwarding, the implementation needs a stricter capability:
Scope::ensure_connected(peer) or equivalent, backed by Processor::connect_with_did;
Scope::send_direct(peer, payload) or a send mode that fails if the direct transport is unavailable.
This keeps onion cells from being accidentally forwarded through transparent Chord relay paths after route construction.
If the first milestone cannot add Scope capabilities, implement the route builder in a node-owned OnionHandle that can use processor-level connect/send operations directly, and keep intermediate relay forwarding behind a narrowly-scoped internal capability.
Exit handling
Exit is an application protocol on top of onion circuits. It should be opt-in and disabled by default.
The exit module should map ExitOpen { service, ... } to local application behavior only after validating the signed exit policy advertised in the exit registry. Possible v1 scopes:
expose a named local TCP/UDP service using logic similar to the existing relay engine;
expose a WebTransport-backed service in browser/wasm contexts;
support protocol-specific request/response services before raw public internet egress.
Do not directly reuse the existing relay protocol as-is for anonymous exits without auditing identity semantics. Its session keys and owner checks are direct-peer-oriented. Reuse lower-level framing/engine pieces where practical, but the onion exit must treat the previous hop as transport peer and the onion circuit as the authenticated application context.
Implementation plan
Suggested PR sequence:
Add onion module skeleton and pure types: descriptors, route selection, circuit ids, commands, errors.
Add OnionExitDescriptor, signing/verification, OnionExitRegistration, and lookup_onion_exits API/RPC DTOs.
Add route builder using lookup_online_nodes(false) plus exit registry lookup, including small-network behavior and peer-quality ordering.
Add direct-next-hop capability or an internal node-owned send path for onion forwarding.
Add onion protocol state machine with pure transition tests: create, created, data, end, destroy, expiry.
Add crypto implementation behind OnionCipher, with test vectors and tamper/replay tests.
Add native relay-only forwarding path.
Add exit service binding for one minimal service type, preferably a named local TCP echo service for testability.
Add browser/wasm compile path and decide whether browser is relay-only in v1.
Add docs and examples showing relay-only node, exit node, and client route construction.
privacy assertion in integration tests: intermediate relays cannot decode final service request, exit cannot recover client DID from the onion payload, and no full route appears in MessageRelay.path;
circuit teardown removes state on timeout/error;
wasm/browser build check for relay-only path if exit is native-only in the first milestone.
Open questions
Should onion relay opt-in be advertised through a separate onion_relays registry, or is presence plus protocol probe enough for v1?
What concrete AEAD/key agreement should be used across native and wasm?
Should v1 support long-lived streams immediately, or start with one-shot request/response over circuits?
What is the first safe exit service type: named local TCP service, WebTransport service, or protocol-specific request/response?
What policy language is sufficient for exit allowlists and rate limits without making the descriptor too large?
Should exit registry topics be global (onion_exits) or service-scoped (onion_exits::<service>) once registry size grows?
Acceptance criteria
The RFC-level design is implemented without modifying rings-core Chord routing semantics.
Online presence remains a liveness directory; exit capability is advertised through a separate application-layer registry.
Relay-only nodes can participate without publishing exit descriptors.
A client can build a route from live relays and a policy-matching live exit.
Onion cells are forwarded hop-by-hop without exposing the full path through ordinary transparent relay metadata.
Exit behavior is opt-in, policy-bound, and disabled by default.
The implementation includes deterministic tests for route selection, descriptor filtering, forwarding, and exit policy enforcement.
Summary
Now that Rings has signed online-node registration, add an onion-routing layer that can build multi-hop private paths over the existing WebRTC overlay without turning every online node into an exit.
This RFC proposes three separate concepts:
ONLINE_NODES_TOPIC) remains the liveness and coarse candidate source.The implementation should live in
crates/node/ extension protocol code, not inrings-coreChord routing. Chord remains the storage and discovery substrate; onion routing is an application protocol above it.Current code context
Relevant surfaces on current
master:crates/node/src/online.rsdefines signedOnlineNodeDescriptorvalues withdid,public_key,network_id,node_type,capabilities, optionalendpoint_hint,heartbeat_at_ms,expires_at_ms, and a signature.crates/node/src/registration.rsalready generalizes periodic DHT-backed registration throughRegistrationTaskandDhtRegistrationPublisher.crates/node/src/processor.rspublishes and looks up online nodes withpublish_online_node_descriptor()/lookup_online_nodes(include_expired).crates/rpc/src/protos/rings_node.rsexposesLookupOnlineNodesRequest/Response.crates/node/src/extension/ext/*provides namespace-scoped protocol routing:Envelope,Protocol,Interpret,Extensions, andScope.crates/core/src/message/protocols/relay.rs/crates/core/src/message/payload.rsimplement the existing transparent overlay relay path. That path records forwarding path metadata and is not an onion route.crates/node/src/extension/protocols/relay.rsimplements application TCP/UDP relay/tunnel behavior. It is useful exit-side prior art, but its peer identity/session semantics are direct-peer semantics, not anonymous onion-circuit semantics.crates/core/src/message/e2e.rsimplements direct endpoint ElGamal E2E frames. That is endpoint encryption, not layered hop encryption, and it deliberately is not an AEAD/KEM-DEM construction.Problem assessment
The existing online registry solves only the first directory problem: discovering live signed nodes on the same network. It does not answer:
MessageRelay.pathmachinery.The current overlay send path also has an important privacy boundary: if an onion packet is sent to the next hop through ordinary Chord forwarding, intermediate Chord nodes may observe a transparent route to the guard. Onion cell forwarding should therefore prefer or require direct next-hop transport connections after the circuit is selected. Chord/DHT should be used for directory lookup and bootstrapping, not as the per-hop onion forwarding path.
Goals
MessageRelaypath and not the existing direct E2E stream format.Non-goals
rings-coreor Chord routing.MessageRelay.path.Registry design
Presence directory
Keep the existing
ONLINE_NODES_TOPICas the liveness source. The onion route builder useslookup_online_nodes(false)to obtain live, same-network candidates and verify descriptors.Presence should not become the exit policy registry.
OnlineNodeDescriptor.capabilitiesmay be useful for coarse capability labels, but exit policy is too specific and too high-risk to live there.Exit registry
Add an application-layer DHT registry for onion exits, for example
ONION_EXITS_TOPIC = "onion_exits", owned by the onion extension/node layer.Suggested descriptor shape:
Policy should be explicit and signed. At minimum:
The registry publisher should reuse the existing
RegistrationTask/DhtRegistrationPublisherpattern:Relay-only nodes simply do not publish an exit descriptor.
Relay discovery
Initial relay candidate discovery can use live presence descriptors plus a successful onion-protocol probe/handshake. If relay participation needs explicit public opt-in beyond installing the onion protocol, add a second application-layer
onion_relaysregistry with a smaller descriptor. That should still be in the onion application layer, notrings-core.Route-selection algorithm
Inputs:
lookup_online_nodes(false);lookup_onion_exits(service, include_expired = false);Algorithm:
network_id.PeerQuality: prefer healthy, keep unknown eligible, avoid degraded unless the network is too small.allow_short_onion_paths; never silently weaken path length in production mode.Pseudocode:
Onion protocol shape
Add a new namespace, for example
onion, installed throughOnionHandle::install(&provider.extensions(), config).The protocol should be circuit-based, not a single transparent relay payload:
CreateCircuit: sent from client to guard with a layered route blob.Created: returned backward when all hops are installed.Data: stream/datagram payload over an established circuit.End: close a stream or whole circuit.Destroy: tear down state after error/timeout.Each hop stores only local circuit state:
An intermediate relay should know only:
It must not learn the origin client, final exit service, full route, or inner payload. The guard necessarily sees the client as previous. The exit necessarily sees the requested exit service and the previous relay, but should not learn the original client from the onion layer.
Cryptography requirements
Do not reuse
message::e2eas the onion layer. It is endpoint-oriented ElGamal stream encryption and relies on the signedMessagePayloadenvelope for integrity. Onion layers need per-hop authenticated encryption and key separation.Recommended implementation plan:
OnionCipherabstraction first so the protocol state machine can be tested independent of the concrete crypto.Concrete crypto choice should be decided before implementation. Candidates:
The chosen construction must be available in native and wasm builds or be cleanly feature-gated.
Direct next-hop forwarding requirement
The current extension
Scope::send(to, payload)sends over the normal overlay envelope path. For onion forwarding, the implementation needs a stricter capability:Scope::ensure_connected(peer)or equivalent, backed byProcessor::connect_with_did;Scope::send_direct(peer, payload)or a send mode that fails if the direct transport is unavailable.This keeps onion cells from being accidentally forwarded through transparent Chord relay paths after route construction.
If the first milestone cannot add
Scopecapabilities, implement the route builder in a node-ownedOnionHandlethat can use processor-level connect/send operations directly, and keep intermediate relay forwarding behind a narrowly-scoped internal capability.Exit handling
Exit is an application protocol on top of onion circuits. It should be opt-in and disabled by default.
The exit module should map
ExitOpen { service, ... }to local application behavior only after validating the signed exit policy advertised in the exit registry. Possible v1 scopes:Do not directly reuse the existing
relayprotocol as-is for anonymous exits without auditing identity semantics. Its session keys and owner checks are direct-peer-oriented. Reuse lower-level framing/engine pieces where practical, but the onion exit must treat the previous hop as transport peer and the onion circuit as the authenticated application context.Implementation plan
Suggested PR sequence:
onionmodule skeleton and pure types: descriptors, route selection, circuit ids, commands, errors.OnionExitDescriptor, signing/verification,OnionExitRegistration, andlookup_onion_exitsAPI/RPC DTOs.lookup_online_nodes(false)plus exit registry lookup, including small-network behavior and peer-quality ordering.OnionCipher, with test vectors and tamper/replay tests.Test plan
Minimum tests before enabling by default:
MessageRelay.path;Open questions
onion_relaysregistry, or is presence plus protocol probe enough for v1?onion_exits) or service-scoped (onion_exits::<service>) once registry size grows?Acceptance criteria
rings-coreChord routing semantics.