Skip to content

fix(p2p): repair peer discovery — tracker domain, KV binding, loopback bind, relay - #22

Merged
SaharBarak merged 5 commits into
mainfrom
fix/discovery-tracker
Aug 1, 2026
Merged

fix(p2p): repair peer discovery — tracker domain, KV binding, loopback bind, relay#22
SaharBarak merged 5 commits into
mainfrom
fix/discovery-tracker

Conversation

@SaharBarak

Copy link
Copy Markdown
Collaborator

Why

Discovery has never worked. Not "worked poorly" — it was structurally impossible for two peers to find each other over the WAN. Four independent blockers, each sufficient alone:

  1. Wrong tracker domain. The shipped default was https://usefolklore.com, which we do not own; it 301s to lovefolklore.com. Every install announced into an unrelated website and parsed HTML as a peer list.
  2. TRACKER_KV never bound. /tracker/* answered 500 {"error":"tracker KV not bound"}. The binding lived only in dashboard state that was never configured.
  3. Daemon bound to loopback. peer.listen_host defaulted to 127.0.0.1. The daemon is the only consumer of that key (CLI and MCP nodes hardcode loopback at their own createNode call), so the default made it announce /ip4/127.0.0.1/… — an address every other peer dials against itself — and silently disabled UPnP.
  4. No relay. Even with 1–3 fixed, CGNAT and symmetric-NAT peers (most home machines) can never accept an inbound dial.

What changed

Tracker. Default URL → https://usefolklore.sh. KV namespace created and declared in wrangler.toml, so the binding travels with the repo rather than living in dashboard state. Merging this is what activates it — until the Pages build runs, the tracker still 500s.

Bind. peer.listen_host defaults to 0.0.0.0. Exposed surface is the libp2p TCP listener only — noise-encrypted, peer-id-authenticated, rate-limited. No plaintext endpoint is opened.

Relay (WebSockets on 443). @libp2p/websockets is wired into transports on every node — a leaf never listens on ws but must be able to dial the relay's wss address. New peer.ws_port and peer.announce config keys cover the edge-fronted case: the container listens plain ws on 8080, TLS terminates at the edge, and announce publishes the …/tcp/443/wss form. 443 is chosen over raw TCP because it is the one port hostile networks allow, and because an anycast/shared IPv4 forwards only 80/443 — raw TCP would need a dedicated IP.

folklore relay is a new command that runs only the libp2p relay server. daemon _run would relay too, but it boots ingestion, embedders and consolidation to do it — that OOM-killed the 512MB relay box, and it would file graph data on a machine documented to hold none.

The relay is deployed and verified from outside Fly: wss dial 1.3s, reservation granted, leaf advertises /dns4/folklore-relay.fly.dev/tcp/443/wss/p2p/<relay>/p2p-circuit/p2p/<leaf>. Shipped as the default FOLKLORE_RELAYS.

So it cannot fail silently again

  • Peers filter loopback, link-local and LAN-private addresses out of announces (announceableAddrs); circuit addresses always survive. A node with nothing publishable falls back to a read-only fetch — it still discovers, it just stops poisoning the directory.
  • The tracker rejects non-routable pointers server-side; it is open to anyone, so the client-side rule needs a server half.
  • folklore doctor probes the tracker. On this branch before deploy it prints the exact bug this PR fixes:
    [WARN] peer discovery (tracker)     https://usefolklore.sh → HTTP 500 (tracker KV not bound)
    

Dependency note

uint8arraylist is pinned to ^3 via overrides. @libp2p/websockets pulls v3 while several older libp2p packages still declare ^2; two copies of the type break every it-length-prefixed call site at compile time.

deploy/relay/Dockerfile now fetches by ref-or-SHA instead of clone --branch: the old form is a byte-identical RUN line on every build, so the builder reused a stale checkout and deployed old code (it shipped a binary without the relay command during this work).

Tests

  • announceableAddrs filtering, including the escape hatch for a self-hosted LAN tracker
  • sanitizeAddrs rejects loopback / 0.0.0.0 / link-local / IPv6 link-local
  • trackerTick with only non-routable addrs falls back to fetch and stays out of the directory
  • live two-node WebSocket relay reservation (real libp2p, loopback)
  • config defaults: 0.0.0.0 bind, tracker domain, ws_port/announce round-trip, default relay is wss-on-443

…oopback bind, no relay

Discovery has never worked. Four independent blockers, each sufficient on its
own to keep the swarm empty:

1. The default tracker URL pointed at usefolklore.com — a domain we do not own,
   which 301s to an unrelated site. Every install announced into someone else's
   website and got HTML back. Now https://usefolklore.sh, the deployed Pages
   domain.

2. TRACKER_KV was never bound, so /tracker/* answered 500 "tracker KV not
   bound". The namespace is now declared in wrangler.toml so the binding
   travels with the repo instead of living only in dashboard state.

3. peer.listen_host defaulted to 127.0.0.1. The daemon is the only consumer of
   that key (CLI and MCP nodes hardcode loopback at their own createNode call),
   so the default made the daemon announce /ip4/127.0.0.1/… — an address every
   other peer dials against itself — and silently disabled UPnP. Now 0.0.0.0.

4. No relay existed, leaving CGNAT and symmetric-NAT peers unreachable even
   with 1-3 fixed. The relay now listens for WebSockets behind a TLS edge on
   443 rather than raw TCP on 4103: 443 is the one port hostile networks allow,
   and it removes the dedicated-IPv4 requirement of a raw-TCP listener.
   @libp2p/websockets is wired into transports on every node (a leaf must be
   able to DIAL wss even though it never listens), with peer.ws_port and
   peer.announce for the edge-fronted case.

Hardening so the failure cannot recur silently: peers filter loopback,
link-local and LAN-private addresses out of announces (circuit addresses always
survive), the tracker rejects non-routable pointers server-side, and
`folklore doctor` probes the tracker — it reports the exact 500 above rather
than leaving discovery to fail invisibly inside a best-effort loop.

uint8arraylist is pinned to ^3 via overrides: @libp2p/websockets pulls v3 while
several older libp2p packages still declare ^2, and two copies of the type
break every it-length-prefixed call site at compile time.
…emory stack

`daemon _run` boots ingestion, embedders and consolidation to relay a
connection, which OOM-kills a 512MB relay box and would file graph data on a
machine documented to hold none. The relay command starts only the libp2p node
with circuitRelayServer, reading the same peer.* config (ws_port, announce,
listen_host), and runs until SIGTERM.
folklore-relay.fly.dev is live (circuit-relay-v2 over wss on 443). Verified
from outside Fly: dial 1.3s, reservation granted, leaf advertises
/dns4/folklore-relay.fly.dev/tcp/443/wss/p2p/<relay>/p2p-circuit/p2p/<leaf>.

Shipped as the default so a fresh install is dialable with zero configuration —
a CGNAT peer with no relay is discoverable via the tracker but unreachable,
which is the same empty swarm by another route. FOLKLORE_RELAYS overrides;
setting it empty is an explicit opt-out.
@SaharBarak
SaharBarak merged commit 5c90649 into main Aug 1, 2026
3 checks passed
@SaharBarak
SaharBarak deleted the fix/discovery-tracker branch August 1, 2026 07:34
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