The M297 handoff moves to docs/sessions/ because the arc it describes has shipped; the live file at docs/SESSION_NOTES.md now covers M298. Two things in it are worth reading before the next milestone starts. M299's verdict gate is where the Phase W loop note says the loop hard-pauses, and its campaigns are measured in elapsed days on a quiet host rather than hours of work, so the next session should budget for waiting rather than for typing. And D2 leaves M298 unexplained: three hypotheses excluded, a real 7/45-vs-0/42 split, and a per-run peer-role census as the carried-forward measurement. Section 6 tags two memory candidates. One is that a quiet-box precondition is not enforceable through a Makefile target that builds inside itself — checking load first is necessary and not sufficient, which cost two discarded readings here. |
||
|---|---|---|
| .config | ||
| .github/workflows | ||
| benchmarks/baselines/sim | ||
| crates | ||
| docs | ||
| fuzz | ||
| scripts | ||
| tools/realworld-bench | ||
| .gitignore | ||
| AGENTS.md | ||
| Cargo.lock | ||
| Cargo.toml | ||
| CHANGELOG.md | ||
| CLAUDE.md | ||
| CONTRIBUTING.md | ||
| GEMINI.md | ||
| LICENSE | ||
| README.md | ||
| ROADMAP.md | ||
| rust-toolchain.toml | ||
| SECURITY.md | ||
IronTide
A from-scratch BitTorrent engine library in Rust, built for feature parity with libtorrent-rasterbar.
This repository is the engine only — a library you embed. The desktop GUI, CLI, Web UI, and *arr-ecosystem (qBittorrent-v2 API) integration live in the companion application repo, irontide-client, which consumes this crate from crates.io. If you want a torrent app rather than a torrent library, start there.
Table of Contents
Features
- BitTorrent v1, v2, and hybrid (BEP 52) — Merkle verification, v2 piece layers, and v1/v2/hybrid torrent creation
- Magnet links with peer metadata exchange (BEP 9) and file selection (BEP 53)
- Peer discovery — Kademlia DHT, PEX, Local Service Discovery, HTTP/UDP trackers with scrape
- Transports — TCP and uTP (BEP 29, LEDBAT congestion control), MSE/PE encryption, optional I2P
- Explicit disk engine — vectored positional I/O with preallocation control, a bounded read cache, hash-on-arrival verification, and an optional direct-I/O mode; one universal path on every platform
- NAT traversal — UPnP IGD, NAT-PMP, and PCP with auto-renewal
- Web seeding (BEP 17/19) and fast resume with per-torrent state files
- Live-reconfigurable settings — one strongly-typed
Settingssurface, no restart for most knobs - Broad BEP coverage — 35 BEPs implemented; the full implemented/declined protocol table lives in
docs/bep-coverage.md
The engine is an actor-model tokio workspace exposed through a single facade crate. For the complete API, see docs.rs/irontide.
Install
[dependencies]
irontide = "1"
tokio = { version = "1", features = ["full"] }
IronTide targets the Rust 2024 edition; the minimum supported Rust version is 1.95 (pinned as rust-version in Cargo.toml).
Cryptographic backend
SHA-1 / SHA-256 hashing runs through a pluggable backend, chosen at compile time by a Cargo feature. The three are mutually exclusive — enable exactly one:
| Feature | Backend | Notes |
|---|---|---|
crypto-aws-lc (default) |
aws-lc-rs |
Assembly-optimized BoringSSL fork |
crypto-ring |
ring |
Bundled BoringSSL assembly |
crypto-openssl |
system OpenSSL | FFI to the platform library |
To swap the default for another backend, turn defaults off and pick one:
[dependencies]
irontide = { version = "1", default-features = false, features = ["crypto-ring"] }
Usage
Download a magnet link end to end:
use irontide::prelude::*;
#[tokio::main]
async fn main() -> irontide::Result<()> {
let session = ClientBuilder::new()
.download_dir(".")
.enable_dht(true)
.start()
.await?;
let magnet = Magnet::parse("magnet:?xt=urn:btih:...")?;
let info_hash = session.add_magnet(magnet).await?;
loop {
let stats = session.torrent_stats(info_hash).await?;
if stats.is_finished || stats.is_seeding {
break;
}
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
session.shutdown().await?;
Ok(())
}
Add a .torrent file with per-torrent options, and reconfigure the running
session — most settings apply live, without a restart:
let info_hash = AddTorrentParams::from_file("ubuntu.iso.torrent")
.download_dir("/mnt/isos")
.add_to(&session)
.await?;
let mut settings = session.settings().await?;
settings.max_peers_per_torrent = 256;
session.apply_settings(settings).await?;
Examples
Runnable examples live in crates/irontide/examples/:
download.rs— magnet download driving the full session lifecyclestream.rs— streaming playback while the torrent is still downloadingcreate.rs—.torrentcreation (v1, v2, and hybrid, with pad files)dht_lookup.rs— peer discovery over the Kademlia DHT
Run one with cargo run --example download.
Architecture
Runtime model
IronTide is an actor system on tokio: each concern owns its state and is driven by message passing — no shared locks on the data hot path.
your app
│ ClientBuilder::start()
▼
SessionHandle ──────────── commands (mpsc) ──────────────┐
▲ ▼
│ alerts (broadcast) SessionActor
│ session-wide state: listener, DHT,
└───────────────────────────────── trackers, queue, bans, settings
│ one per torrent
▼
TorrentActor
piece selection, choking, verification,
per-peer lifecycle, fast resume
│ one set per peer
▼
per-peer tasks (reader / requester
inline futures + writer task)
│ 16 KiB blocks
▼
disk writer → hash-on-arrival →
piece verified → Have broadcast
SessionActorowns everything session-wide: the TCP/uTP listeners (one UDP socket demuxed across DHT, uTP, and UDP trackers), peer discovery, the torrent registry and queue, connection caps, IP filtering and bans, and the liveSettingssurface.TorrentActor(one per torrent) owns piece selection (rarest-first with BEP 40-aware eviction, endgame stealing), the choker, block dispatch with a BDP-adaptive pipeline (per-peer in-flight depth priced from the peer's own rate × RTT), verification (SHA-1, SHA-256 Merkle, or hybrid dual-verify), and fast-resume persistence.- Per-peer tasks run the wire protocol on a zero-copy ring-buffer codec;
writes go straight from the receive buffer to disk via vectored positional
I/O (
pwritev), and pieces hash on arrival rather than in a second read-back pass. - Backpressure is explicit end to end: bounded channels with overflow policies, a global request budget, and choke hysteresis — a slow consumer disconnects rather than stalling the swarm.
Crate map
The workspace is 18 crates (17 on crates.io), split along compilation
boundaries so editing one subsystem rebuilds that crate plus the facade, not
the world. You depend on the irontide facade only; everything below it
is internal and re-exported through the facade where public.
layer 0 irontide-bencode BEP 3 wire format (serde, sorted-key dicts)
layer 1 irontide-core hashes, metainfo v1/v2, magnets, Merkle, piece math
layer 2 irontide-wire peer protocol: messages, handshake, extensions
irontide-tracker HTTP + UDP announce/scrape
irontide-dht Kademlia DHT (BEP 5) + BEP 32/42/43/44
irontide-storage piece storage, verification, disk I/O
irontide-utp uTP transport (BEP 29, LEDBAT)
irontide-nat UPnP IGD / NAT-PMP / PCP port mapping
layer 3 irontide-format shared human-readable formatting
irontide-settings the strongly-typed Settings SSOT + validation
irontide-peer-io ring-buffer codec, vectored read, writer task
layer 4 irontide-session-types cross-actor payloads, stats, ban/ip-filter infra
irontide-engine-support errors, I2P SAM, transport abstraction, rate limits
layer 5 irontide-engine the per-torrent actor + engine infrastructure
layer 6 irontide-session-core registries, queue, resume, settings-apply
layer 7 irontide-session SessionActor/TorrentActor runtime + public handle
layer 8 irontide ◀ the facade you depend on (ClientBuilder, prelude)
(internal) irontide-sim in-process swarm simulator (not published)
Design rules
- Typed errors everywhere — per-crate
thiserrorenums, noanyhow; the facade wraps them in oneirontide::Error. bytes::Byteson the data path — blocks move by reference count, not memcpy, from socket to disk.- Sans-IO protocol cores — the MSE handshake, DHT lookup, uTP congestion control, and codecs are pure state machines with CI purity guards, so they are testable without sockets and reusable on the simulator.
- One universal disk path — vectored positional writes on every platform (the mmap/io_uring/IOCP special cases were measured and deleted).
- Simulation-first regression testing —
irontide-simruns the real session actors over an in-process network with byte-exact committed baselines; loom/fuzz/perf-smoke lanes run in CI alongside the 2,852-test library battery.
Protocol-level design notes live in docs/bep-coverage.md;
the settings schema is documented in docs/configuration.md.
Performance
The engine is benchmark-driven: it is measured against qBittorrent,
libtorrent-rasterbar, and rqbit on live public swarms, and losses on contested
axes become roadmap milestones that iterate until won. The reproducible
harness and pinned fixtures live in benchmarks/; the
dated evidence documents live in
docs/investigations/ and the client repo.
Revision-level A/B campaigns — current worktree vs the published crates.io
baseline, counterbalanced paired runs on live swarms with quiet-host gates
and a fixed verdict band — run through
tools/realworld-bench/.
Headline results from the close of the performance programme (2026-07-05/06, live-swarm, interleaved same-day A/Bs; all figures date-stamped and regime-qualified in the linked evidence):
| comparison | result |
|---|---|
| vs rqbit 8.1.1 (3-torrent live set, n=3) | all-torrents-complete 0.77× (faster, 3/3 rounds); throughput parity (0.99×); CPU lower (0.89×); peak RSS +7.8 % while holding 3.3× the concurrent peers with heap at parity |
| vs qBittorrent + libtorrent (21-download battery) | 21/21 completions with a ~60× peak-RSS advantage (130 MiB total vs ~7.9 GiB for the reference pair) |
| engine 1.5.0 vs 1.4.0 (7-torrent live parallel add, n=4/side) | download +7.9 %, peak RSS −4.3 %, CPU −6.4 % — no-regression gate for the final release line |
| client build: fat LTO vs default (same engine, n=4/side) | download +5.6 %, file-backed RSS −14.6 %, CPU flat — binary-text footprint is a build-profile concern, not an engine one |
Live-swarm numbers move with the swarm; the linked evidence records the swarm regime for every claim and uses drift-cancelling interleaved controls.
The first realworld-bench battery — 47 measured runs over seven Linux-ISO
fixtures, 2026-07-25 — is written up in
docs/investigations/2026-07-25-t9-realworld-baseline-findings.md.
It peaked at 64.6 MB/s on a 6.66 GB payload under 71 MiB resident, and left
three items open: peer discovery takes 14–76 s on roughly a third of runs,
which costs more measured throughput than anything else in the battery;
download_payload_rate reports exactly zero for whole runs that move
hundreds of megabytes; and one run in 47 stopped taking bytes for 868 s while
holding 52 seeds, cause unknown.
Building from source
git clone https://codeberg.org/alan090/irontide.git
cd irontide
cargo build --release
cargo test --workspace
Status
Feature-complete and maintained. Phases A–V — 291 milestones, from the bencode layer to the benchmarked performance programme — completed on 2026-07-18 with the v1.7.0 release (17 crates on crates.io, 35 BEPs, Rust 1.97.0 toolchain).
Phase W (M292–M304) is open, on reliability. A battery of paired runs
against live public swarms surfaced four defects the sim harness could not
reach; the phase fixes those and the actor-boundary discipline behind them
before returning to cross-client measurement. The full build history and
measurement record is in ROADMAP.md; every release is logged
in CHANGELOG.md. The engine's consumer-facing development
continues in
irontide-client.
Contributing
Development happens on Codeberg (primary) with a mirror on GitHub. Before submitting changes:
cargo test --workspace
cargo clippy --workspace --all-targets -- -D warnings
Clippy must pass with zero warnings. Commit format: feat|fix|refactor|docs: description (M<N>).
License
GPL-3.0-or-later — Copyright 2025–2026 Alan Gaudet