Repository · workstreams
Workstream: UrlResolver
Status: In progress — a few items are complete, most have shipped in part, and NetLab integration has not started; each unchecked item says what remains · Component: Maximize developer productivity
Goal
Make UrlResolver discover its co-located peers with zero configuration. When two or more resolvers run on the same local network, they should find one another and add each other as peers automatically — without a shared bootstrap node, without a public coordinator, and without any internet access at all. This is the foundation everything else in component 1 talks over, so making it self-organizing on a LAN removes friction from local clusters, offline development, and in-topology testing alike.
Current state
This exists as the UrlResolver project — the P2P fabric underpinning every url:// service — layered over the lower-level UrlProtocol. UrlProtocol today discovers and reaches peers through:
- Gossip-based announcement — services are propagated across the mesh with TTL-based gossip. The Pluggable Network Layer workstream makes the network layer a constructor parameter (libp2p default) with an in-process test implementation. UrlProtocol now gates large-mesh behavior in ordinary CI; equivalent resolver-facing scenarios remain the workstream's final graduation item. The HTTPS Transport workstream plans a second production link type beneath that same seam, so a
url://service can be hosted behind plain HTTPS — on Cloud Run, or behind any HTTPS proxy. - Link-state routing, with a distance-vector fallback — multi-hop forwarding, with every node acting as a universal relay (no dedicated relay infrastructure). Each node computes shortest-path next hops over flooded link-state advertisements; the older distance-vector table is still consulted when the link-state map has no route. (What remains before distance-vector can go.)
- Relay-assisted NAT traversal — peers that cannot be dialed directly register with a relay and are reached over a circuit. An established direct connection is already used ahead of a relay, and traffic moves off a relay when one appears; a cold connection still races a relay dial against the direct one. (What remains.)
- LAN discovery over link-local multicast — on by default beneath
UrlResolver(): resolvers on one segment beacon to each other and add each other as peers with no bootstrap configured. (What remains.) - Bootstrap peers — to join the wider mesh, a node connects to peers it already knows: hardcoded nodes on
wasmserver.com, or explicitbootstrapPeers/ NetLab's injectedBOOTSTRAP_IP/BOOTSTRAP_PORT/BOOTSTRAP_SEED. How persisted peers are scored, diversified across networks, and ranked for join-time dialing is designed in UrlResolver's peer-management design.
The gap this workstream set out to close: every discovery path other than the LAN beacon assumes the node can already reach something — a public bootstrap node, or an address that was handed to it out of band. Before LAN discovery, two resolvers sitting on the same LAN with no shared bootstrap and no internet had no way to find each other: each one tried to dial the hardcoded public nodes, failed (or succeeded only via a detour through the public internet), and never noticed the peer one hop away on the same switch. NetLab works around this for tests by making one in-topology host the bootstrap/root and injecting its address into the other hosts as environment variables — which works precisely because the test author knows every address up front.
Where that stands: the discovery mechanism exists and is on by default, but it is proven only beneath the surface users meet. Its no-bootstrap test drives the discovery component directly rather than two resolvers started the ordinary way; every NetLab scenario still injects a bootstrap address; and the high-level UrlResolver constructor offers no way to turn discovery off or tune it. The cases nobody can hand-wire — an offline developer with a laptop and a phone, a self-hosted cluster on a home or office LAN, an air-gapped deployment — are therefore implemented but not yet demonstrated.
Standing design principle: scores and rankings, never cliffs or calendar cutoffs
A general principle for all UrlResolver/UrlProtocol work in this workstream, called out explicitly so the tempting shortcut stops recurring: never encode a judgment as a hard threshold or a wall-clock window — encode it as a continuous score or a relative ranking. The canonical anti-example is "a peer qualifies for bootstrap only if seen in the last 30 days": it reads sensibly and fails catastrophically (offline 31 days → the entire persisted pool is rejected at once, and the node cannot rejoin the network the pool exists to rejoin). Fixed constants also silently mis-calibrate as the network ages — a 30-day tenure bar means something entirely different in a two-week-old mesh than in a five-year-old one. Prefer the shapes the peer-management design uses and motivates in detail: percentile tenure (rank among known peers, no constant), rank-decayed evidence (recency by observation sequence, indifferent to idle gaps), and greedy rankings (dial ordering rather than eligibility cutoffs). In design review, treat any new fixed calendar window or hard score threshold as a likely bug: ask what happens at the boundary and who calibrated the constant, then replace it with a score or a ranking.
The plan
Add periodic LAN peer discovery over link-local multicast (mDNS-style) to UrlProtocol/UrlResolver, on by default and overridable via configuration.
- [ ] Periodic link-local multicast announcements (mDNS-style). A resolver periodically emits a small link-local multicast beacon advertising its own addressing hints — peer ID and listen multiaddresses — and listens for the same from other resolvers on the segment, building on the
MDnsDiscoverysupport already shipped in the jvm-libp2p fork with a project-specific service tag. Multicast is the default (it is what libp2p standardizes, is confinable per-interface, and is the only mechanism with an IPv6 analogue — IPv6 has no broadcast); plain UDP broadcast is offered as a configuration alternative for networks where multicast is unavailable. The beacon deliberately does not carry the announcing node's service list — once the first connection is up, the existing gossip layer propagates services, so services-in-beacon would be redundant. This is a discovery beacon only; it carries addressing hints, not data or trust. Remaining: the plain-UDP-broadcast alternative; discovery today is multicast only. - [ ] "On a LAN" means the beacon reaches the peer — nothing more. Two resolvers are on the same LAN exactly when their discovery beacons reach one another: a shared broadcast/multicast domain gives co-located nodes a privileged ability to connect directly that nodes outside the segment lack, regardless of how many NAT layers or unusual topologies sit above them. There is therefore no a-priori "is a LAN present?" detection to get wrong — the resolver emits beacons on every up, multicast-capable interface (loopback included, so resolvers on the same host always find each other), and a LAN exists precisely when someone answers; an unanswered beacon on a peer-less segment is harmless. Because a peer nested behind NATs may not itself know which of its addresses is usable on the shared segment, the beacon datagram's source address is the primary dial hint, with the advertised multiaddresses as secondary. The privileged direct link this discovers is exactly the edge the link-state layer then advertises. Remaining: discovery, interface selection, and the source-address dial hint are in place; the last sentence is not — a LAN-discovered link is not yet advertised by the link-state layer.
- [ ] Add discovered nodes as peers. On receiving a peer's broadcast, the resolver adds the announcing node as a peer and establishes a connection through the existing libp2p path (Noise handshake, and W3Wallet authorization where the service requires it). From there, normal gossip, routing, and relay take over — LAN discovery only solves finding the first local peer, it does not replace any existing mechanism. A forged beacon therefore grants nothing: dials to discovered peers are identity-pinned (the dial carries the expected peer ID and the Noise handshake fails on a key mismatch), so the worst a forged beacon can do is waste a dial — a beaconing attacker gains only what any connected mesh peer already has. The remaining exposure, unverified service announcements, is pre-existing and mesh-wide (announcements gossip multi-hop from anywhere) and is closed independently by verified hierarchical name resolution — LAN discovery is not sequenced behind that work. Accepted beacons are de-duplicated by peer ID, and beacon-triggered dials are rate-limited (bounded dial rate per interface, backoff for repeatedly failing peers, a cap on concurrently pending discovery dials) so a beacon flood cannot become a dial storm. The per-peer backoff for repeatedly failing discovered peers need not be bespoke bookkeeping: the peer-management design's check/reliability series already tracks exactly this — while its authenticated-attribution principle ensures forgeable, unauthenticated beacons can never affect any peer's reputation (they are governed by these mechanical limits alone). Remaining: the dial budget is global rather than per interface, and two beacons for one peer can each start a dial while the first is still in flight.
- [ ] On by default, configurable off. Local discovery is enabled by default, so co-located resolvers cluster with no setup. Configuration parameters can disable it outright or tune it: the announce/query interval, the UDP port, the multicast group address (or the plain-UDP-broadcast alternative for networks where multicast is unavailable), and which interfaces/address ranges (e.g. RFC 1918 / link-local only) it is allowed to use — interface/range confinement is a policy knob, not part of the definition of "on a LAN" above. Environments that must not emit discovery traffic (locked-down production segments, shared cloud networks) turn it off via configuration; the default favors the zero-config local case, and is largely self-limiting in shared cloud networks anyway since major-cloud VPCs drop multicast on the wire. Remaining: the off switch and tuning exist on
UrlProtocolbut are not reachable through the high-levelUrlResolverconstructor. - [x] Compose with bootstrap, not compete with it. A node may have both a configured bootstrap peer and LAN discovery active; the two sets of peers are merged and de-duplicated by peer ID so a locally discovered peer and a bootstrap-supplied one are never tracked twice.
- [ ] NetLab integration. Let in-topology scenarios rely on zero-config LAN discovery instead of injected
BOOTSTRAP_*environment variables where the topology is a single broadcast domain, and add a NetLab scenario that asserts two resolvers on the same virtual LAN discover each other with no bootstrap configured (verifying along the way that NetLab's virtual switches forward link-local multicast). This dovetails with NetLab's "in-topology discovery needs no public DHT" direction. Remaining: all of it — every NetLab scenario still injectsBOOTSTRAP_*. - [ ] End-to-end tests. Per the testing standards: two in-process resolvers on a shared loopback/multicast fixture that discover each other purely via the local-discovery beacon path (no bootstrap, no public mesh), plus a test asserting discovery is suppressed when disabled by configuration. Remaining: the existing no-bootstrap test drives the discovery component directly; it must run two resolvers through their default startup path.
UrlResolver implements the remote-command observable
A second, separate UrlResolver concern (distinct from LAN discovery above): UrlResolver implements the remote-proxy observable surface that a live projection consumed over a remote url:// SJVM proxy connection hands back. The interface itself is Observable's: RemoteCommandDerivedObservable — the cancelable, status-bearing handle a command returns (REMOTE_OBSERVABLES.md §5.1) — is declared in observable-core alongside the RemoteObservable marker, with the CommandState/CommandStatus/CommandFailure value types beside it. Because DerivedObservable is deliberately final (the framework's factories own its lifecycle invariants), the handle exposes its reactive surface by composition (val state: DerivedObservable<CommandState> plus cancel()) rather than by extending anything. A service author's shared Api module therefore declares fun rename(...): RemoteCommandDerivedObservable against lightweight observable-core alone — no separate resolver API artifact is needed, consistent with the standard layered architecture.
- [x] Implement the interface in the resolver. The existing concrete
RemoteDerivedObservable(foundation.url.resolver.sandbox) — itself a wrapper that backs aDerivedObservable, not a subclass of it — stays a resolver internal; the command path implementsobservable-core'sRemoteCommandDerivedObservableover the same machinery (command RPC, status push, cancellation acknowledgement), with the server-side command bookkeeping tracked in the Observables workstream's command-lifecycle item. Shared work item with that workstream.
Prefer direct mesh connections; fall back to a relay only when no direct path exists
A third, separate UrlResolver/UrlProtocol concern (distinct from the LAN discovery and observable-interface concerns above): a direct peer-to-peer (mesh) connection is the desired state for any pair of nodes that can establish one, and a hub/relay is a fallback of last resort — used only when there is genuinely no way to connect the two nodes directly.
Today UrlProtocol leans on its universal relay architecture: every publicly routable node acts as a relay, and a NAT-ed node is reached by routing through a guard/relay peer that holds a persistent connection to it. That keeps NAT-ed nodes reachable, but it over-uses the relay — it can keep a third node in the data path for a pair that could have talked directly. The first half of the fix has shipped: an established direct connection is used ahead of any relay, in both directions, and a pair that started on a relay moves off it. What remains is the cold path, where a relay dial still competes with the direct one.
The decisive observation is that a relay is only necessary when both endpoints are non-public. Outbound dials from behind a NAT succeed; only inbound dials to a NAT-ed peer fail. So whenever either peer has a publicly routable address, the non-public peer can dial the public peer directly, and — because a libp2p connection is bidirectional and long-lived — that single direct connection then carries traffic both ways. From that point forward the relay is not needed for that pair at all, and once such a direct connection exists it must not remain in the path.
- [ ] Go direct whenever either peer is public. When connecting two peers and at least one advertises a publicly routable address, the non-public peer establishes a direct connection to the public peer instead of registering with / routing through a relay for that pair. Reuse the existing public-address detection (
PublicAddressDiscovery, the observed address learned via Identify, and thePUBLIC_IPoverride) to decide which side is dialable. The peer-management design's direct-reachability (reach) score is the natural substrate for this decision: it accumulates rank-decayed first-hand evidence of direct-vs-relay-only dialability per peer, giving upgrade/downgrade decisions hysteresis instead of re-deriving NAT-ness per connection. Remaining: on a cold connection the direct dial and a relay dial are started together and the first to be ready wins, so a public + NAT-ed pair can still begin on a relay. - [x] Use the direct connection in both directions. Because the libp2p connection is bidirectional and persistent, the public peer reaches the (otherwise un-dialable) NAT-ed peer back over the same connection the NAT-ed peer opened — no relay circuit or guard-peer forwarding is used for either direction once the direct link is up.
- [ ] Relay strictly as the both-NAT-ed fallback. Reserve relay/guard forwarding for the case where neither peer is publicly routable (and as a transient bootstrap before addresses are known). When a public endpoint exists for a pair, that pair never depends on a relay. Remaining: the same cold-path race — a relay is still a candidate when a public address is known.
- [x] Drop the relay once a direct path appears. If a pair starts out talking over a relay (e.g. before either side's address is known), upgrade to a direct connection as soon as one becomes possible and stop routing that pair's traffic through the relay. A relay must not linger in the data path after a direct mesh link is available.
- [ ] End-to-end tests. Per the testing standards, in a NetLab (or loopback) topology: a public + NAT-ed pair must communicate over a direct connection with zero relay hops; a both-NAT-ed pair must fall back to the relay; and a pair that begins on a relay and later gains a direct path must be shown migrating off the relay. Remaining: the both-NAT-ed and migration cases are covered; the public + NAT-ed case is covered only for an already-established direct connection, not a cold dial.
Removing a relay from a pair that can talk directly is lower latency and one fewer node carrying traffic it never needed to — which directly relieves load on the public wasmserver.com relay nodes — and a pair that can connect directly is not left depending on a third party, in keeping with the decentralized-by-default philosophy. The routing substrate that makes this selection correct is the link-state change immediately below.
Replace distance-vector routing with a link-state routing protocol
UrlProtocol originally routed purely on distance-vector principles: a node only learns, from its neighbors, a distance and a next hop toward each destination, and NAT-ed destinations are reached by routing toward whichever hub/relay holds a connection to them. A node never sees the whole graph — it routes "by rumor," trusting its neighbors' summarized distances — so it cannot tell that two of its known peers are actually one cheap hop apart, and it tends to funnel traffic toward the public hubs that happen to be on the advertised paths. This is the structurally wrong shape for this network.
The answer is a link-state routing protocol (the OSPF family — emphatically not source-routed onion/circuit routing, which predetermines a multi-hop path for anonymity). Each node measures and advertises only the links it can directly observe — its peers and the distance/latency/cost to each — and floods those link-state advertisements across the mesh. Every node thereby assembles a complete, identical map of the topology and runs a shortest-path computation (Dijkstra / SPF) over it, so each node independently knows, for every destination, which single neighbor a packet should be handed to for the shortest path — and forwards hop by hop, with no node dictating the downstream route. Link-state forwarding has since shipped as the primary path, with the distance-vector table kept as a fallback; the items below record what is done and what must still happen before distance-vector can be removed.
- [ ] Advertise directly-observed links, not summarized distances. Each node's advertisement describes its own adjacencies — the peers it is directly connected to and the measured distance/latency/cost of each link — rather than its computed distance to far-off destinations. These advertisements flood across the mesh on the existing gossip substrate. Remaining: advertisements carry adjacencies and measured costs, but are built from relay registrations and the active relay connection rather than from every direct connection, so LAN-discovered peers are missing from them.
- [ ] Every node builds the full link-state map and computes shortest paths itself. From the flooded advertisements each node assembles the complete topology graph and runs a shortest-path computation (Dijkstra / SPF) to derive a next-hop table — for any destination, the one neighbor to forward to for the cheapest path. Replaces the distance-vector next-hop exchange described in the network architecture. Remaining: the distance-vector table and its exchange are still active as a fallback; this item is done when they are removed.
- [ ] Reachability comes from the graph — not from self-known addresses or advertised circuits. With hop-by-hop forwarding over the link-state map, a node no longer needs to discover its own public IP address to be reachable, and it never advertises a relay circuit address (
/p2p-circuit/...) as its location — today's obligation for a NAT-ed node to broadcast a path to itself goes away entirely. A node is reachable because the peers it is connected to advertise their links to it, and every other node computes a next hop toward its peer ID over those links. Address discovery (the Identify-observed address,PUBLIC_IP, LAN beacons) remains useful for forming direct links — deciding who can dial whom — but once links exist, routing needs only the topology graph and peer IDs; no node ever describes a multi-hop path to itself. Remaining: relay registration still advertises/p2p-circuit/addresses, and a node's advertised adjacencies do not yet include every peer it found by LAN discovery. - [ ] Cost-aware paths, not hub-funneled ones. Because every node sees real per-link cost, the shortest path is chosen on actual latency/cost instead of being funnelled toward whichever public hub the advertised routes pointed at. This is what makes "prefer direct mesh connections" an emergent property: a direct one-hop link is simply the cheapest path, so it wins automatically, and a relay is selected only when it is genuinely the only path. Remaining: lowest-cost path selection is in place and tested; "a relay only when it is genuinely the only path" is not, because a cold connection still races a relay dial against the direct one.
- [ ] LAN-direct links survive even behind a shared NAT. Two nodes on the same LAN sitting behind one shared NAT are unreachable to essentially every other node on the internet, yet they can connect to each other directly. Distance-vector routing toward a public hub cannot express that edge; link-state can, because each of the two nodes advertises the directly-observed link to the other, and both compute the direct LAN hop as the shortest path between them — no detour out through the shared NAT and back. This is the routing-layer counterpart of the LAN peer discovery goal above (discovery finds the peer; link-state then routes to it directly). Remaining: the routing table handles the case when handed the adjacency, but LAN-discovered peers do not yet enter a node's advertisements on their own.
- [ ] Keep flooding bounded and convergence prompt. Link-state floods and topology churn must stay bounded on the existing gossip transport (sequence-numbered/aged advertisements, dampened re-flooding) so a large or flapping mesh does not amplify traffic — consistent with the bounded-gossip work already done in UrlProtocol/UrlResolver. Peers that violate this discipline (re-flooding stale-sequence advertisements, ignoring damping intervals) feed the peer-management design's spam reputation as a moderate-severity signal, so persistent LSA amplifiers are ostracized network-wide rather than only rate-limited locally. Remaining: size, sequence, and age bounds are in place; dampened re-flooding and feeding violations into spam reputation are not.
- [ ] End-to-end tests. Per the testing standards, in NetLab topologies: a multi-hop topology asserts every node converges on the same shortest-path next hop for each destination; a topology where a low-cost direct edge and a longer hub path both exist asserts traffic takes the direct edge; and a shared-NAT LAN topology asserts the two co-located nodes route to each other directly while remaining unreachable from outside. Remaining: the three cases have protocol-level tests but no NetLab topology.
Verify service ownership through hierarchical, delegated name resolution
A fourth UrlResolver/UrlProtocol concern, and a security one: by default a service is still "owned" by whoever announces it. A peer gossips a ServiceAnnouncement for a service identifier and the resolver routes to whichever peer announced it (findRouterForService). The only authorization that exists is ServiceRegistry — a hard-coded table of Ed25519 keys for a handful of known services — and it is consulted only when the name happens to be in that table (the if (serviceMetadata != null) guard in both UrlResolver and UrlProtocol), with a "*" wildcard that accepts any peer for demo/test entries. For every service not in the hard-coded table, the announcing peer was trusted unconditionally. This was never the intention. Signed delegation chains are now checked on the live resolution path as well, but only for a TLD the resolver holds a root-signed descriptor for. A name that is neither under such a TLD nor listed in the hard-coded table remains free to claim.
The intended design is for names to resolve as a hierarchy, like DNS: a dotted name (a.b.c) is walked one segment at a time from the left — the leftmost segment is the top-level domain, the reverse of DNS's written order — and each level is authoritative only for delegating its own children — handing back a descriptor that names the next authority down the chain, and ultimately the peer / public key authorized to serve the leaf. Ownership then becomes verifiable: a peer can serve only a name its parent delegated to it, and the resolver verifies the delegation chain rather than trusting a bare announcement. A gossip announcement is demoted to a hint about where a peer might be reachable — never proof that it owns the name. The chain is anchored in a single root that UrlResolver ships hard-coded (detailed below), so every delegation ultimately verifies back to a key the resolver already trusts.
Resolution is executed by the name nodes' implementations themselves: each level of the hierarchy is code — the root nodes ship in the resolver itself, and TLD resolvers arrive as signed code over gossip (below) — and each level's implementation decides how its own children are delegated. A name node can therefore be entirely offline: a delegation scheme implemented purely in code resolves with no network connectivity at all, so offline and air-gapped clusters resolve any name whose chain of authorities is locally implemented. Only a name node whose implementation consults a remote name server becomes unresolvable when the network is partitioned from that server — exactly DNS's behavior, and no worse. (The first such node is the protocol TLD planned in the HTTPS Transport workstream: url://protocol.https.icann.com.mydomain.www/ (the DNS labels root-first, like every other segment) hands the rest of the name to the public DNS hierarchy and lets the hostname's WebPKI certificate say who may serve it. It is also the one exception to gossip-distributed TLDs below: it ships pinned inside the resolver, because it must resolve for a resolver that has never met a peer.) This keeps verified naming compatible with the offline-first goal of the LAN discovery plan above.
What exists today falls short of this in one fundamental way. The verifier now on the live path applies one fixed rule at every level: each level is a signed data record naming the keys that may sign its children, and a single algorithm checks each child against its parent's keys. No level can supply its own logic, so a level that validates its children some other way — through DNS and certificate chains, a registrar, or hard-coded keys of its own — cannot be expressed. Resolution as code per level, the shape described above, is not built, and the per-level code interface below is not connected to the verifier. Both are needed: the fixed-rule verifier is the special case where a level's code simply checks signatures. The building blocks:
NamespaceSegmentResolveris exactly the hierarchical model —resolve("com.example.service")walks segment by segment, each resolver handing back a child resolver for the next segment from a root registry.VpnNamespaceSegmentResolveris a working instance of the descriptor idea: it walks thevpnnamespace by fetching aSubdomainRegistrationdescriptor per subdomain from a registrar, and each descriptor carries thepeerId(andresolverData) needed to take the next step — "each node in the hierarchy returns a descriptor describing the next node."ServiceRegistryalready models cryptographic ownership (authorized Ed25519 keys per name,isAuthorizedPeerId) — but as a flat, hard-coded table rather than something derived from a delegation chain.
The plan threads these together into the authoritative resolution path:
- [ ] Resolve through the hierarchy, not the announcement. Make hierarchical, segment-by-segment resolution (
NamespaceSegmentResolver) the path thatopenSandboxedConnection/findRouterForServiceuse to decide who may serve a name, instead of trusting whichever peer gossiped aServiceAnnouncement. Remaining: the decision is made by the fixed-rule descriptor walk, not by per-level code —NamespaceSegmentResolveris not on this path at all; and a name under a TLD with no descriptor is still served by whoever announces it, unless the hard-codedServiceRegistrytable happens to list it. - [x] Each level returns a signed delegation descriptor. Generalize the
vpnregistrar'sSubdomainRegistrationinto a standard, signed descriptor: each authority signs the delegation of each child name, and the leaf descriptor binds the name to the public key(s) authorized to serve it. Resolution verifies the signature at every hop, so a forged or self-asserted descriptor cannot insert an unauthorized owner. - [ ] Authorization comes from the verified chain, not a hard-coded table. Replace the flat
ServiceRegistrylookup with the authorized key(s) carried by the verified leaf descriptor —isAuthorizedPeerIdis then checked against keys the chain actually delegated, for every name rather than only the handful in the table. Confine the accept-anyone"*"wildcard to explicitly-scoped test/demo namespaces. Remaining: the"*"wildcard is confined to an explicit sample name, but the hard-codedServiceRegistrytable is still consulted alongside the chain. - [x] Root of trust: a hard-coded TLD-signing certificate. UrlResolver ships with a single hard-coded public-key certificate authorized to sign top-level domains — the DNS-root-hints analogue, and the key every delegation chain ultimately verifies back to. (Further down the chain, where a leaf name is tied to a user-held identity, W3Wallet keys remain the natural per-owner anchor.) (Shipped as a raw Ed25519 public key rather than a certificate.)
- [ ] Distribute TLD resolvers over signed gossip; drop unverified ones on sight. A top-level domain's
NamespaceSegmentResolver— including its code — is globally distributed and stored via gossip rather than hard-coded per TLD, so the set of TLDs can grow without shipping a new UrlResolver. Each node verifies a gossiped TLD against the hard-coded root certificate's signature before using or storing it, and immediately drops any TLD whose signature does not verify — it is neither used for resolution nor re-propagated, so a forged or unsigned TLD can never spread across the mesh. Remaining: what is gossiped is a signed descriptor (keys and mode), not a resolver's code — distributing code waits on resolution as code per level, above; and although an unverified descriptor is never stored or used, the direct peer-gossip path still forwards it onward — only the relay path withholds it. - [ ] End-to-end tests. Per the testing standards: a peer announcing a name it was never delegated is rejected; a peer presenting a valid signed delegation chain is accepted and served; resolution walks several hierarchy levels; a gossiped TLD resolver whose signature does not verify against the hard-coded root is dropped — neither used nor re-propagated to other peers — while a validly root-signed TLD resolver is accepted and used; and the
"*"wildcard cannot authorize a peer outside an explicit test scope. Remaining: the invalid-signature test checks that the descriptor is not stored; it does not check that it is not forwarded to another peer.
Name ownership is the foundation of trust for every url:// connection — if any peer can claim any name, a client can be silently routed to an impostor. Verified hierarchical resolution closes that hole while keeping authority delegated downward from roots rather than centralized, mirroring DNS and fitting the decentralized-by-default philosophy.
Why it accelerates developers
- Sovereignty and offline-first. It directly serves the philosophy of "run it yourself, offline if you like" — a cluster of self-hosted services on a private LAN forms a working mesh with no dependency on any
wasmserver.combootstrap node or the public internet. - Zero-config local clusters. Standing up several services on one machine, or across a few machines on the same network, requires no bootstrap wiring — they simply find each other.
- Cleaner tests. NetLab scenarios that today inject bootstrap addresses can instead exercise the same self-organizing discovery real deployments use, making the tests both simpler and more faithful.
- It protects the foundation. UrlResolver is load-bearing for every
url://service; a robust, well-tested local-discovery path makes the layer everything else depends on easier to bring up correctly.
Graduation
UrlResolver already exists as a first-class project, so this workstream graduates when its planned capabilities ship and are covered by the end-to-end tests above:
- Zero-config LAN discovery over link-local multicast (mDNS-style; plain UDP broadcast available via configuration) is on by default, configurable off, and used by at least one NetLab scenario.
- Direct (mesh) connections are preferred, with a relay used only as a fallback when neither peer is public.
- Routing is link-state — every node holds the full topology and forwards along the shortest path — replacing distance-vector.
- Name resolution is hierarchical and verified, so a peer can serve only a name its parent delegated to it.
At that point, document the mechanisms on the existing UrlResolver project page — no new project page — by adding a "local discovery" section and a proper configuration section: today the page shows bootstrapPeers only inside a getting-started code example, so the graduation write-up documents bootstrapPeers properly for the first time alongside the new local-discovery parameters (interval, port, multicast group / broadcast alternative, interface and address-range confinement, and the off switch).