A from-scratch BitTorrent engine library in Rust — v1/v2/hybrid torrents, DHT, uTP, MSE/PE encryption, web seeding, NAT traversal. The engine behind the IronTide client.
Find a file
Alan Gaudet 3b01a7828e
Some checks are pending
CI / Test & Clippy (push) Waiting to run
verify-loom / Loom model check (irontide-core) (push) Waiting to run
docs: archive the M297 handoff, write the M298 handoff
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.
2026-07-26 10:36:30 -04:00
.config test(m240): adopt cargo-nextest — config + local verification 2026-06-01 22:07:43 -04:00
.github/workflows chore: pin workspace to Rust 1.97.0 (M289) 2026-07-18 13:12:29 -04:00
benchmarks/baselines/sim feat: M285 — uTP measurement repair (v1.6.1) 2026-07-16 19:20:20 -04:00
crates test: reliability regression suite + R4 arming + D2 measurement (M298) 2026-07-26 10:35:59 -04:00
docs docs: archive the M297 handoff, write the M298 handoff 2026-07-26 10:36:30 -04:00
fuzz feat(fuzz): restore fuzz_tracker seed corpus — 4 curated seeds (M271) 2026-07-01 23:46:52 -04:00
scripts feat: M285 — uTP measurement repair (v1.6.1) 2026-07-16 19:20:20 -04:00
tools/realworld-bench test: reliability regression suite + R4 arming + D2 measurement (M298) 2026-07-26 10:35:59 -04:00
.gitignore feat(ci): track scripts/sim-perf.sh — /scripts/* gitignore carve-out (M271) 2026-07-01 23:46:13 -04:00
AGENTS.md docs: update README and CHANGELOG for M298 2026-07-26 10:36:04 -04:00
Cargo.lock feat: actor-thread occupancy instrumentation (M293) 2026-07-25 13:54:53 -04:00
Cargo.toml feat: realworld-bench T1 — dual-source package spike + isolation gate 2026-07-24 15:22:05 -04:00
CHANGELOG.md docs: update README and CHANGELOG for M298 2026-07-26 10:36:04 -04:00
CLAUDE.md docs: make AGENTS.md the real context file and rotate out the milestone log 2026-07-25 03:09:04 -04:00
CONTRIBUTING.md docs: GitHub Actions account-block discovery — CONTRIBUTING + DAT D13 (M271) 2026-07-02 00:49:29 -04:00
GEMINI.md docs: make AGENTS.md the real context file and rotate out the milestone log 2026-07-25 03:09:04 -04:00
LICENSE Add GPL-3.0 license 2026-02-27 08:39:43 -05:00
README.md docs: update README and CHANGELOG for M298 2026-07-26 10:36:04 -04:00
ROADMAP.md docs: update README and CHANGELOG for M298 2026-07-26 10:36:04 -04:00
rust-toolchain.toml chore: pin workspace to Rust 1.97.0 (M289) 2026-07-18 13:12:29 -04:00
SECURITY.md docs(M238): lib best-practice layout — live badges, docs.rs metadata, CONTRIBUTING/SECURITY, README de-stale 2026-05-29 13:05:41 -04:00

IronTide

A from-scratch BitTorrent engine library in Rust, built for feature parity with libtorrent-rasterbar.

CI crates.io docs.rs License

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 Settings surface, 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 lifecycle
  • stream.rs — streaming playback while the torrent is still downloading
  • create.rs.torrent creation (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
  • SessionActor owns 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 live Settings surface.
  • 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 thiserror enums, no anyhow; the facade wraps them in one irontide::Error.
  • bytes::Bytes on 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 testingirontide-sim runs 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 1476 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 AV — 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 (M292M304) 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 20252026 Alan Gaudet