Skip to content

Repository files navigation

ElixirTorrent

GitHub release Changelog Hex.pm HexDocs Hex.pm Downloads License

build codecov BEPs Last commit

GitHub Web UI macOS

OpenSSF Scorecard OpenSSF Best Practices

Elixir OTP

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.

About

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)

What you get

  • 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.

Installation

def deps do
  [
    {:elixir_torrent, "~> 0.6.4"}
  ]
end
mix deps.get

Requires 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)

Quick start

From a .torrent file

{: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.

From a magnet link

{: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.

Session persistence

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 shutdown

Stopping 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 files

Configuration

Every 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: :any

dial_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.

Public API

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)

ElixirTorrent Web (desktop app)

Need a client rather than a library? ElixirTorrent Web is the official Phoenix LiveView UI for this engine, shipped as a native macOS app.

Development

mix test          # full suite; runs with no network access
mix quality       # compile --warnings-as-errors + dialyzer + credo --strict + sobelow

mix 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.torrent

The escript's interactive loop does not expose magnet links yet — use download_magnet/2 from your own application for those.

Contributing

Contributions are welcome — see CONTRIBUTING.md. Security reports go through SECURITY.md, not public issues.

Released under the MIT License.

About

BitTorrent client engine

Resources

Contributing

Security policy

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages