Skip to content

feat: add DHT nodes from BEP-5 .torrent files to the routing table#583

Open
chantra wants to merge 5 commits into
ikatson:mainfrom
chantra:bep005_nodes_torrent
Open

feat: add DHT nodes from BEP-5 .torrent files to the routing table#583
chantra wants to merge 5 commits into
ikatson:mainfrom
chantra:bep005_nodes_torrent

Conversation

@chantra

@chantra chantra commented May 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Addresses the review on #583. BEP-5 nodes embedded in a .torrent are the closest DHT contacts, not BitTorrent peers — they must be queried over DHT to discover peers. The previous approach injected them into initial_peers, treating them as BT peers. This PR:

  • Adds a public DhtState::add_bootstrap_nodes(Vec<(String, u16)>) API to inject nodes into the routing table at runtime (best-effort, background), resolving hostnames as well as IPv4/IPv6 literals (per the spec, <host> may be a DNS name like your.router.node).
  • Refactors the existing bootstrap_hostname logic onto DhtState (find_nodes_in_routing_table + resolve_and_find_nodes) so bootstrap and the new API share one code path.
  • Wires .torrent nodes in librqbit's add_torrent to the DHT via that API instead of initial_peers (ignored with a debug log if DHT is disabled).

The initial BEP-5 commit was amended to remove the now-obsolete initial-peers wiring, keeping history clean.

Test plan

  • cargo test -p librqbit-dht --lib — new tests:
    • bootstrap_populates_routing_table (locks in existing bootstrap behavior)
    • add_bootstrap_nodes_populates_table (A learns a node only via the new API)
    • add_bootstrap_nodes_empty_is_noop
  • cargo test -p librqbit test_create_torrent_with_bootstrap_nodes
  • cargo test -p librqbit-core dht_node::tests
  • cargo clippy -p librqbit-dht --lib --tests and cargo clippy -p librqbit --all-targets clean
  • cargo fmt --all -- --check clean

Assisted by AI

@chantra

chantra commented May 6, 2026

Copy link
Copy Markdown
Contributor Author

@ikatson I am not super psyche about the current DhtNode serde, but did not find a good way to have it working using the existing bencode crate. Mostly due to serialize_tuple . Let me know if there is ways you would prefer this written.

};

// BEP-0005: inject nodes from .torrent as initial peers.
if !torrent.meta.nodes.is_empty() {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I'm reading the spec correctly, these nodes aren't peers necessarily, but the closest DHT nodes.
Means they need to be queried through DHT to find peers, but not (just) added to initial parts

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, those are nodes that can be queried over DHT. Are you asking because then they should be added to a separate variable?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose in some ways, it is similar to #485 . Would you prefer that the initial torrent nodes are added to the Session.dht? I have not found a way to insert a node in the DhtState struct. Anything I am missing?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ikatson I have been spending a bit more time on this and I believe my test worked initially because it probably leaked to the DHT_BOOTSTRAP.
As much as I can see, in its current state, there is no way tell the Session to add those nodes to the dht table, right?

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, and I think you need to add support for this to make this PR useful.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let me put this into draft mode for now. Thanks for the feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ikatson ok, finally got to the bottom of this PR.

It is composed of 5 commits.

Comment thread crates/librqbit/src/session.rs Outdated
@ikatson

ikatson commented May 6, 2026

Copy link
Copy Markdown
Owner

As to custom deserialize/serialize that's totally fine and how it's usually done anyway

@chantra
chantra requested a review from ikatson May 6, 2026 22:21
@chantra

chantra commented May 6, 2026

Copy link
Copy Markdown
Contributor Author

Re-requesting review in case this is needed to get it back on your radar.
I just realized tests are failing, will address in parallel.

@chantra
chantra force-pushed the bep005_nodes_torrent branch from bfc4040 to 2baf72d Compare May 6, 2026 22:43
@chantra
chantra marked this pull request as draft May 11, 2026 17:00
@chantra

chantra commented Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

hi, just an update, still want to work on this, but need to prioritize other stuff right now.

chantra added 5 commits July 9, 2026 13:40
Add DhtNode type with custom serde that serializes as a 2-element
bencode list ["<host>", <port>] per the BEP-5 spec. The Deserialize
impl drains the sequence after reading host/port to properly consume
the list end-marker, preventing a BytesRemaining(1) error when nodes
appear before other fields in the outer torrent dict.
- New `dht_node` module in librqbit_core with DhtNode struct
- Add `nodes: Vec<DhtNode>` field to TorrentMetaV1
- Wire nodes through CreateTorrentOptions and create_torrent()
- Add bencode roundtrip tests (single, vec, nested struct, empty vec)

Test Plan:
- `cargo test -p librqbit test_create_torrent_with_bootstrap_nodes` passes
- `cargo test -p librqbit-core dht_node::tests` passes
- `cargo clippy -p librqbit-core` clean
Add an in-crate test module to dht.rs (there was none) with a small
two-node loopback harness: `spawn_node` starts an in-process DhtState
bound to an ephemeral v4 loopback port, and `wait_for_node_in_table`
polls the routing tables for a given node id.

`bootstrap_populates_routing_table` seeds node B with a known node C,
then bootstraps node A off B and asserts A learns about C via the
find_node recursion. This pins down the existing bootstrap behaviour so
the upcoming refactor of `bootstrap_hostname` is provably non-regressing.

Test Plan:
- `cargo test -p librqbit-dht bootstrap_populates_routing_table` passes
- `cargo clippy -p librqbit-dht --lib --tests` clean
- `cargo fmt --all -- --check` clean
`bootstrap_hostname` lived on DhtWorker but only used `self.dht`, never
the worker socket. Extract its body into two private DhtState helpers:

- `find_nodes_in_routing_table(addrs)`: the v4/v6 find_node split.
- `resolve_and_find_nodes(addr)`: lookup_host + the above. Accepts any
  `ToSocketAddrs + Debug` ("host:port" string or (host, port) tuple) and
  derives the error label from the Debug repr, so no separate display arg.

`DhtWorker::bootstrap_hostname` now delegates to the DhtState helper. No
behaviour change; this deduplicates the find_node logic so an upcoming
public `add_bootstrap_nodes` can reuse it.

Test Plan:
- `cargo test -p librqbit-dht --lib` passes (incl. bootstrap_populates_routing_table)
- `cargo clippy -p librqbit-dht --lib --tests` clean
- `cargo fmt --all -- --check` clean
Expose a public, best-effort API to inject extra bootstrap nodes into the
routing table at runtime. Takes (host, port) pairs (host may be a
hostname or IPv4/IPv6 literal), resolves each via the shared
`resolve_and_find_nodes` helper, and runs find_node in a background task
so the nodes get queried over DHT rather than treated as peers.

Intended for BEP-5 `nodes` embedded in .torrent files, which are DHT
contacts, not BitTorrent peers.

Test Plan:
- `cargo test -p librqbit-dht --lib` passes, incl:
  - add_bootstrap_nodes_populates_table (A learns node C only via the API)
  - add_bootstrap_nodes_empty_is_noop
- `cargo clippy -p librqbit-dht --lib --tests` clean
- `cargo fmt --all -- --check` clean
When adding a .torrent that carries a BEP-5 `nodes` list, hand those
(host, port) contacts to the session DHT via `add_bootstrap_nodes`
instead of injecting them as initial peers. Per the spec these are DHT
nodes to be queried for peers, not BitTorrent peers, so this lets the
DHT discover peers from a trackerless torrent's embedded contacts.

Hostnames as well as IPv4/IPv6 literals are supported (resolution happens
in the DHT). If DHT is disabled, the nodes are ignored with a debug log.

Test Plan:
- `cargo test -p librqbit test_create_torrent_with_bootstrap_nodes` passes
- `cargo clippy -p librqbit --all-targets` clean
- `cargo fmt --all -- --check` clean
@chantra
chantra force-pushed the bep005_nodes_torrent branch from 2baf72d to 640f425 Compare July 9, 2026 12:22
@chantra chantra changed the title feat: add BEP-0005 DHT bootstrap nodes support in .torrent files feat: add DHT nodes from BEP-5 .torrent files to the routing table Jul 9, 2026
@chantra
chantra marked this pull request as ready for review July 9, 2026 13:57
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.

2 participants