Skip to content

fix(dht): make bittorrent-dht bootstrap reliably (1 → 50+ nodes) - #232

Open
bluejorts wants to merge 1 commit into
QuixThe2nd:mainfrom
bluejorts:dht-fixes-isolated
Open

fix(dht): make bittorrent-dht bootstrap reliably (1 → 50+ nodes)#232
bluejorts wants to merge 1 commit into
QuixThe2nd:mainfrom
bluejorts:dht-fixes-isolated

Conversation

@bluejorts

@bluejorts bluejorts commented Jul 9, 2026

Copy link
Copy Markdown

Problem

Under Bun (the Docker image runtime), the DHT routing table never grows past a single node (itself), so Hydrabase peer discovery over the DHT is effectively non-functional. This reproduces on a bare host and in a bridge-networked container.

Root cause: usage, not the network

On the same machine and network, libtorrent (qBittorrent's engine) reaches 60–70 DHT nodes in ~20s, and a raw bittorrent-dht instance behaves identically under Node and Bun — so this is not a Bun/UDP problem and not a network/NAT problem. It's how the DHT is driven. Five compounding issues:

  1. dht.listen() is never called. bittorrent-dht's internal bucket-refresh / re-bootstrap-when-isolated maintenance loop only starts on the socket listening event, which requires an explicit listen(). Without it, the library's own self-healing never runs and a cold start stalls after one shallow bootstrap lookup.
  2. The 5s query timeout was ineffective. It was passed to krpc(), which ignores it; the socket used its 2s default and dropped slow-but-live routers.
  3. Only a single bootstrap lookup ran, then the DHT idled until the 15-minute reannounce.
  4. Low fan-out. k-rpc default concurrency/backgroundConcurrency (16/4).
  5. Cache persistence never fired — gated on nodes > 50, which never happened, so restarts were always cold.

The bootstrapNodes list was also mostly dead: dht.libtorrent.org was on the wrong port (:6881 instead of :25401), several routers are defunct, and the list included Hydrabase peer hostnames that don't speak the BitTorrent DHT.

Fix (src/backend/networking/dht.ts, config.ts, krpc.d.ts)

  • Call dht.listen() on a free ephemeral port (not node.port — the UTP transport owns that) to start the maintenance loop.
  • Move the timeout to krpcSocket({ timeout: 5000 }) so it actually applies.
  • Add a cold-bootstrap loop: re-seed routers and re-run lookup(ownId) every 10s until the table reaches 20 nodes.
  • Raise concurrency 16→32 and backgroundConcurrency 4→16 (and add backgroundConcurrency to the KRPCOptions type).
  • Persist the routing table on any new high ≥8 nodes (seeding cacheSize from the loaded cache) so restarts are warm.
  • Replace the bootstrap list with empirically-verified live routers (probed with find_node): dht.transmissionbt.com:6881, dht.libtorrent.org:25401 (correct port), the canonical bittorrent/utorrent routers, and a transmissionbt IP fallback.

Verification

Environment Before After
Real app, host (bun run) 1 node Ready with 76 nodes
Container, bridge networking 1 node Ready with 41 nodes (climbing)

No dependency changes; the fix is self-contained to the DHT wiring, config, and one type declaration.

🤖 Generated with Claude Code

The DHT routing table was stuck at a single node (itself) on cold start,
while libtorrent reached 67 nodes on the same network — so this was a
usage problem, not the network. Five compounding issues, all fixed:

- dht.listen() was never called, so the socket's 'listening' event never
  fired and bittorrent-dht's internal bucket-refresh / re-bootstrap-when-
  isolated maintenance loop never started. Now bind explicitly on a free
  ephemeral port (node.port belongs to the UTP transport).
- The 5s query timeout was passed to k-rpc, which ignores it; the socket
  used its 2s default and dropped slow-but-live routers. Moved to
  krpcSocket({ timeout: 5000 }).
- Only a single shallow bootstrap lookup ran, then the DHT idled until the
  15-minute reannounce. Added a cold-bootstrap loop that re-seeds routers
  and re-runs lookup(ownId) every 10s until the table reaches 20 nodes.
- Raised k-rpc concurrency (16→32) and backgroundConcurrency (4→16) for
  faster traversal; added backgroundConcurrency to the KRPCOptions type.
- Cache persistence only fired at >50 nodes (never reached), so restarts
  were always cold. Now persists whenever the table reaches a new high of
  >=8 nodes, seeding cacheSize from the loaded cache.

Bootstrap list: replaced the mostly-dead/mis-configured set (wrong
dht.libtorrent.org port :6881, defunct utorrent/bitcomet/aelitis routers,
and Hydrabase peer hostnames that don't speak the DHT) with the routers
empirically verified live: dht.transmissionbt.com:6881,
dht.libtorrent.org:25401 (correct port), the canonical bittorrent/utorrent
routers, and a transmissionbt IP fallback.

Verified: real app now reaches "Ready with 53 nodes" on startup.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013VjgRYtdZZk6LuvTmsmpSh
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