A complete BitTorrent client engine for Elixir/OTP — downloads, seeds, resumes, and traverses NAT, behind a small public API you can embed in your own application.
This is a fully functional BitTorrent client that actually downloads torrents. It started as a course project for Functional Programming with Elixir at Sofia University. After the course ended, development continued in spare time until it was ready to publish on Hex.
The whole stack is Elixir on OTP primitives — wire protocol, DHT, trackers, piece picking, storage, encryption — with 23 BEPs implemented, encryption on by default, IPv4/IPv6 dual stack, and interop verified against Transmission, qBittorrent and libtorrent.
{:ok, pid} = ElixirTorrent.download("/path/to/file.torrent")
{:ok, stats} = ElixirTorrent.stats(pid)- Download and seed — full BEP 3 peer protocol, fast extension (BEP 6), endgame, and automatic superseeding (BEP 16) when you are the only complete seed.
- Find peers four ways — trackers (HTTP + UDP, multitracker tiers), DHT (BEP 5, dual-stack via BEP 32/42), peer exchange (BEP 11), and LAN discovery (BEP 14). HTTP web seeds (BEP 19) act as a fifth data source.
- Magnet links — metadata fetched from peers over
ut_metadata(BEP 9/10) and verified against the info hash before a normal session starts. - BitTorrent v2 (BEP 52) — pure-v2 torrents download and resume through their SHA-256 Merkle path; hybrid torrents join both swarms on the DHT.
- Encryption — MSE/PE is full and bidirectional, so encryption-only peers connect and ISP protocol throttling has nothing obvious to match on.
- Works from behind a NAT — uTP (BEP 29) alongside TCP, uTP hole punching (BEP 55), NAT-PMP/PCP/UPnP port mapping, and STUN-based NAT-type detection.
- Resumable sessions — bitfield and counters checkpoint every 30 s, so a crash costs a hash-check of what was in flight, not a full re-scan.
- Built to stay small — piece read/write/verify processes start on demand and idle-terminate instead of one live process per piece, and a measured per-family dial throttle stops the engine burning connections on an address family that is not reaching anyone.
Full per-BEP status, including the known gaps: PROTOCOL.md.
def deps do
[
{:elixir_torrent, "~> 0.6.4"}
]
endmix deps.getRequires Elixir 1.20+. The engine is an OTP application — start it before first use (or list it in your supervision tree's dependencies):
Application.ensure_all_started(:elixir_torrent){:ok, pid} = ElixirTorrent.download("/path/to/file.torrent")
[hash] = ElixirTorrent.list()
# Write files under a specific directory (session state still uses File.cwd!/0):
{:ok, pid} = ElixirTorrent.download("/path/to/file.torrent", download_dir: "/Downloads")
{:ok, stats} = ElixirTorrent.stats(pid, [:name, :speed, :downloaded, :bytes_size])
# stats.name, stats.speed.download, stats.speed.upload, …
files = ElixirTorrent.list_files(hash)
# Each entry has :path, :progress, :complete?, etc.Poll stats/2 while the download runs. When you are done, stop_and_serialize/1
keeps the progress and remove/2 drops it.
{:ok, pid} =
ElixirTorrent.download_magnet(
"magnet:?xt=urn:btih:…&tr=udp%3A%2F%2Ftracker.example.com%3A1337%2Fannounce"
)The engine parses the URI, announces to its tr= trackers and/or asks the DHT for
peers, fetches the info dictionary over BEP 9, checks SHA1(bencode(info)) against
the magnet's hash, and only then starts a normal session.
A magnet needs at least one tr= tracker or DHT enabled; a trackerless magnet
with DHT off returns {:error, :missing_trackers}. Other common failures:
:no_peers, :timeout, :metadata_unavailable, :info_hash_mismatch.
Progress survives restarts. Each session is a file under
{File.cwd!()}/.elixir_torrent/state/{hex_info_hash}.term holding the bitfield, byte
counters, and peer status. Call download/2 with the same .torrent and the engine
loads the session, verifies pieces against what is on disk, and resumes from there.
ElixirTorrent.stop_and_serialize(hash) # one torrent
ElixirTorrent.stop_all_and_serialize() # everything, e.g. on application shutdownStopping this way is a protocol-clean shutdown, not a socket drop: active piece
requests are cancelled, peers get BEP 3 cancel/not interested/choke before the
connection closes, and each tracker receives an event=stopped announce so the swarm
stops handing your address to other peers.
To drop a torrent instead of pausing it:
ElixirTorrent.remove(hash) # deletes the session file
ElixirTorrent.remove(hash, delete_data: true) # …and the downloaded filesEvery subsystem that talks to the network can be switched off independently — useful for embedded use, private-tracker-only setups, or a test suite that must not touch the wire.
config :elixir_torrent,
listen_port: 6881
config :elixir_torrent, :dht,
enabled: true, # BEP 5 DHT
routing_store: true, # persist the routing table between runs
bootstrap_routers: [{"router.bittorrent.com", 6881}]
config :elixir_torrent, :lsd, enabled: true # BEP 14 LAN multicast discovery
config :elixir_torrent, :nat, enabled: true # NAT-PMP/PCP/UPnP + STUN detection
config :elixir_torrent, :network, dial_scope: :anydial_scope: :this_host restricts outbound connections to addresses this machine
owns (loopback and its own interfaces), which is how the test suite runs the full
engine against loopback fixtures without a single packet leaving the host.
Full reference: hexdocs.pm/elixir_torrent/ElixirTorrent.html
| Function | Description |
|---|---|
download/2 |
Start a download from a local .torrent path; optional download_dir: |
download_magnet/2 |
Start from a magnet URI (metadata fetch + normal session) |
stats/2 |
Runtime stats map (:name, :speed, :downloaded, :bytes_size, …) |
list/0 |
Info hashes for all active torrent processes |
list_files/1 |
Per-file paths and download progress |
stop_and_serialize/1 |
Graceful stop + persist session |
stop_all_and_serialize/0 |
Graceful stop + persist for every torrent |
remove/2 |
Stop and drop from session; optional delete_data: true |
get/2 |
Low-level field access (prefer stats/2) |
version/0 |
Version-derived client peer ID prefix (ET0-6-4, BEP 20) |
Need a client rather than a library? ElixirTorrent Web is the official Phoenix LiveView UI for this engine, shipped as a native macOS app.
mix test # full suite; runs with no network access
mix quality # compile --warnings-as-errors + dialyzer + credo --strict + sobelowmix quality is the same gate CI runs. Sobelow does security-focused static
analysis at --threshold medium, filtering low-confidence file-path findings from
the engine's own already-sanitized path handling.
The package also builds a small escript for ad-hoc testing:
mix escript.build
./elixir_torrent
# then type: download /path/to/file.torrentThe escript's interactive loop does not expose magnet links yet — use
download_magnet/2 from your own application for those.
Contributions are welcome — see CONTRIBUTING.md. Security reports go through SECURITY.md, not public issues.
Released under the MIT License.