Report fake BitTorrent upload to trackers, without connecting to any peer or transferring any data.
Mirage is a command-line tool that announces fake BitTorrent upload to trackers in order to increase a user's ratio on private or public trackers. It reads real .torrent files, emulates a legitimate BitTorrent client at the wire level (peer_id, key, User-Agent, announce parameters), models a full leech-then-seed lifecycle, and sends periodic HTTP and UDP tracker announces declaring a time-varying upload curve whose declared integral exactly matches the area under the curve displayed in the live dashboard.
It does not connect to peers, does not transfer data, and does not modify any torrent file. The only network traffic it generates is tracker announces.
- Emulates a real client at the wire level (peer_id, key, User-Agent, announce parameters), auto-detecting the locally installed Transmission version
- Models a full leech-then-seed lifecycle, so upload is never declared on a file that was never downloaded
- Time-varying upload curve with per-torrent harmonics and jitter to defeat pattern fingerprinting; declared upload always matches what the dashboard shows
- Optional swarm-proportional cap, so Mirage never declares near-line-speed to an almost empty swarm
- HTTP (BEP-3) and UDP (BEP-15) trackers, every URL in the announce-list
- Live 10-tab dashboard with a plausibility linter, command palette, and ratio graph
- Atomic state persistence: download progress survives restarts
- Single static binary, zero runtime dependencies, no peer connections
Mirage is provided for educational and research purposes. Reporting upload you did not perform violates the terms of service of effectively every public and private tracker, and can result in a ban or other consequences on the trackers you use it against. You are solely responsible for how you use this tool and for complying with the rules of any tracker and with applicable law. The authors accept no liability. Use at your own risk.
cargo install mirage-tui # or grab a binary from the releases page
cp your.torrent ~/Downloads/Mirage/ # default torrent directory
mirage
Mirage loads every .torrent from its torrent directory. The default is
~/Downloads/Mirage (an absolute path, so it is the same folder however you
launch Mirage: terminal, Launchpad, or the macOS app). It picks a client to
emulate (auto detects your local Transmission) and opens the live dashboard.
The dashboard prints the exact directory on first run when it is empty, so you
always know where to drop files. No config file is required; see
Configuration to set a different torrent_dir.
Mirage uses the fake-torrent-client crate to impersonate a real torrent
client. The client config key selects which client to emulate. With the
default value "auto", Mirage reads the locally installed Transmission's
Info.plist to extract the exact version string, then builds a peer_id and
User-Agent that match that version character for character. If Transmission is
not found at /Applications/Transmission.app, it falls back to a built-in
Transmission 4.0.6 profile.
Any client profile supported by fake-torrent-client can be specified by name
(for example "Qbittorrent_4_4_2"). The key (the key= parameter in the HTTP
announce query) is refreshed periodically by a background task for clients whose
profile specifies a refresh interval.
Declaring upload on a file that was never downloaded is the most common ratio
cheating tell. Mirage prevents this by modeling a realistic leech phase. Each
torrent starts in a Downloading state. A simulated download rate (drawn
randomly from [min_download_rate, max_download_rate] at startup) is applied in
the scheduler's download tick, which fires approximately every 45 seconds. The
torrent declares downloaded and left values that grow toward length. Once
the simulated download completes, a completed event is sent and the torrent
transitions to the Seeding state, after which upload can be declared.
Download progress is persisted to disk after every scheduler tick (see State persistence below), so a restart resumes from the last saved position rather than re-downloading from scratch, which would itself be detectable.
Upload speed is modelled as a sum of four sinusoidal components:
- A slow mean-drift oscillation (period ~1200 s)
- Three texture oscillations (periods ~90 s, ~23 s, ~7 s)
Each component's phase and period are seeded by a per-torrent random value so no two torrents share a harmonic spectrum. The period is also jittered by up to +/-6% to defeat FFT-based fingerprinting.
The declared upload bytes sent to trackers are computed as the closed-form
integral of this curve over the window [last_announce, now]. The dashboard
displays the instantaneous value of the same curve. Because both the declared
integral and the displayed value derive from the same analytic function, the
total uploaded displayed in the dashboard always matches what was declared to
the trackers.
A global upload multiplier (stepped through [0.25, 0.5, 1.0, 2.0, 4.0, 8.0]
with the Up/Down arrow keys) scales the entire curve without breaking the
declared-equals-displayed invariant, because the same scalar is applied to both
the instantaneous value and the integral.
Upload is declared to zero (and can_upload() returns false) if:
- The torrent is still in the download phase
- The global pause flag is set
- The swarm reports no leechers (no one to upload to)
Mirage supports both HTTP (BEP-3) and UDP (BEP-15) trackers. All URLs from a
torrent's announce-list are contacted. Before the per-URL loop begins, the
upload delta is computed once from the speed curve integral, so every tracker in
the list receives the same declared upload window rather than each one getting a
progressively smaller window as last_announce is reset on each success.
The scheduler runs a single loop that:
- Snapshots the list of torrent handles under a short read lock, then drops it.
- For each torrent, acquires the torrent's own mutex and decides whether to announce (based on elapsed time vs interval, or download tick cadence).
- Calls
announce(), which fires HTTP and UDP announces in sequence. - Writes the JSON stats file and persists download state.
- Sleeps until the soonest next announce, with jitter.
The outer TORRENTS lock is never held during network I/O. This ensures that
adding or removing a torrent (which takes a write lock) is never blocked by an
in-flight announce.
On startup, announce(Started) is sent for all loaded torrents. On clean exit
(Ctrl+C, q key, SIGTERM, or closing the macOS Terminal window), announce(Stopped)
is sent for all torrents with an 8-second timeout, then download state is saved
and the process exits.
If you have a Rust toolchain, install the latest release with Cargo:
cargo install mirage-tui
The crate is published as mirage-tui (the name mirage was already taken on
crates.io). The installed binary is still mirage, so run it with:
mirage
Pre-compiled binaries for Linux, macOS, and Windows are available on the releases page.
| File | Platform |
|---|---|
mirage-linux-x86_64 |
Linux x86_64 (static, no dependencies) |
mirage-macos-aarch64 |
macOS Apple Silicon |
mirage-macos-x86_64 |
macOS Intel |
mirage-windows-x86_64.exe |
Windows x86_64 |
Mirage.app.zip |
macOS app bundle (double-click to launch) |
Windows note: the Windows binary builds in CI and the code is written to be cross-platform (UTF-8 console code page, truecolor detection for Windows Terminal and VS Code, window-resize handling). It has not been verified at runtime on a real Windows machine. Development and testing happen on macOS and Linux. Run a UTF-8 capable terminal (Windows Terminal or the VS Code terminal) for the intended box-drawing and colors. Reports of Windows issues are welcome.
On Linux and macOS, make the binary executable after downloading:
chmod +x mirage-linux-x86_64
./mirage-linux-x86_64
On macOS, unzip Mirage.app.zip and move Mirage.app to /Applications. It
opens the live dashboard in a Terminal.app window sized to 92x28 columns.
Requires Rust stable (edition 2024):
cargo build --release
The release binary is target/release/mirage.
To build the macOS app bundle:
cargo build --release && scripts/make_app.sh
# or, to install directly to /Applications:
scripts/make_app.sh --install
On first run, Mirage looks for a config file at the XDG config path:
~/.config/Mirage/config.toml
If the file does not exist, Mirage uses defaults. All fields are optional; any omitted field uses its default value.
# Client to emulate.
# "auto" detects the locally installed Transmission and emulates its exact version.
# Any ClientVersion name from the fake-torrent-client crate is also valid, e.g.:
# "Qbittorrent_4_4_2", "Transmission_3_00", "Deluge_2_1_1"
client = "auto"
# Listening port declared in announces (not actually bound).
# Default: random in 49152-65534.
port = 51413
# Upload speed band (bytes/s). The speed curve oscillates within this range.
min_upload_rate = 8192 # 8 KiB/s
max_upload_rate = 2097152 # 2 MiB/s
# Simulated download speed band (bytes/s). Used during the leech phase.
min_download_rate = 8192
max_download_rate = 16777216 # 16 MiB/s
# Number of peers requested from trackers.
# Default: client profile default (80 for Transmission).
numwant = 80
# Directory to watch for .torrent files.
# Defaults to ~/Downloads/Mirage. A relative value here is resolved against this
# config file's directory, not the launch directory, so the path is stable no
# matter how Mirage is started. Override with the TORRENT_DIR environment
# variable or this key.
torrent_dir = "/path/to/torrents"
# Write a PID file to the XDG runtime directory (mirage.pid).
use_pid_file = false
# Path to a JSON stats file written after each scheduler tick.
# Optional; no file is written if absent.
output_stats = "/tmp/mirage.json"
# Discreet desktop notification on each ratio milestone (1.0, 1.5, 2.0, 3.0,
# 5.0, 10.0). Off by default. When on, Mirage emits a terminal OSC 9 escape
# (shown natively by terminals that support it) and a best-effort osascript
# (macOS) / notify-send (Linux) notification. No terminal bell is ever used.
notify_milestones = false
# Swarm-proportional upload cap (KiB/s of fake upload budget per leecher).
# Optional; absent means the speed curve ignores swarm size (original behaviour).
# When set, each torrent's fake speed scales with its leecher count toward
# max_upload_rate, so Mirage never declares near-line-speed to an almost empty
# swarm (a common ratio-faking tell). Example: 64 = 64 KiB/s per leecher.
per_leecher_kib_s = 64A specific config file can be passed at launch:
mirage -c /path/to/config.toml
mirage --config /path/to/config.toml
The s key in the dashboard's Config tab saves the current in-memory config
(including live Speeds edits) back to the XDG config file.
MIRAGE_NO_UI disables the live dashboard even on an interactive terminal and
falls back to classic log output. Any value triggers it:
MIRAGE_NO_UI=1 mirage
RUST_LOG controls the log level in non-TUI mode. Valid values are error,
warn, info, debug, trace. Default is trace.
RUST_LOG=info mirage
In TUI mode the tracing subscriber is not initialised, so RUST_LOG has no
effect and log output does not corrupt the alternate screen.
Place .torrent files in torrent_dir (default ~/Downloads/Mirage). Mirage
scans that directory at startup and loads every .torrent it finds. Torrents
with no supported tracker URL are skipped.
When the directory is empty, the dashboard still opens and shows an onboarding
hint pointing at the watch directory; drop a .torrent in and the file watcher
loads it without a restart. In non-interactive mode (piped output or
MIRAGE_NO_UI), Mirage prints where it looked and exits instead.
The filesystem watcher monitors the directory while Mirage is running. Dropping
a new .torrent file into the directory triggers a Started announce and adds
the torrent to the live dashboard within about 500 ms. Deleting or moving a file
out of the directory triggers a Stopped announce and removes the torrent.
The watcher also handles atomic-rename additions (rsync default, Transmission
temp-file writes) which arrive as Modify(Name(To)) events rather than
Create events.
Duplicate torrents (same info-hash) are ignored silently and a note appears in the dashboard feed.
When stdout is an interactive terminal and MIRAGE_NO_UI is not set, Mirage
enters the alternate screen and displays a full-screen dashboard redrawn
approximately 2.5 times per second. The dashboard is composed of ten tabs
navigated by number keys 1-9 and 0 (the ratio graph), the Left/Right arrow
keys, or h/l.
The dashboard captures key input in raw mode. Bracketed paste is disabled so pasting text into the window does not trigger accidental commands.
SIGWINCH (window resize) triggers an immediate repaint. SIGCONT (resume after Ctrl+Z suspend) re-enters the alternate screen and raw mode that the shell may have dropped.
1 dash - The main overview. Shows the emulated client identity (name, peer_id, key), a table of all loaded torrents, and a scrolling feed of recent events (connects, announces, upload ticks, watcher events, errors).
The torrent table columns are: torrent name, seeders (S), leechers (L), current
upload speed, total uploaded, and a countdown to the next announce paired with a
progress bar. During the download phase, the progress bar shows download
completion percentage instead of the announce countdown, and the speed column
shows DL NN%.
2 tor - Full torrent list with state (downloading/seeding/error), seeder and leecher counts, and total uploaded per torrent.
3 trk - Per-torrent tracker host with current seeder and leecher counts.
Press g to toggle a by-tracker rollup: one row per host with the torrent
count, summed upload, summed speed, summed seeders/leechers, and error count,
sorted by upload. Press g again to return to the per-torrent view.
4 spd - Upload and download speed band editor, multiplier, and numwant. Use Up/Down to move between the six settings rows and +/- to double or halve values. numwant steps by 10.
5 cli - Client identity: name, peer_id, User-Agent, and key. Shows the exact
GET /announce?... query the tracker receives. The k key regenerates the
client (new key, new peer_id).
6 sch - Seed mode and global pause state.
7 net - Network settings: port, numwant, torrent directory, PID file status.
8 log - The in-process event ring (same events as the dashboard feed, kept for up to 50 entries).
9 cfg - Mirror of the active config.toml values. The s key saves the
current config to disk.
0 rto - Cumulative upload graph over the session, a summary line (uptime, total uploaded, peak speed), and an ETA to the next ratio milestone projected from the average credited rate (hidden until there is enough history).
The per-torrent detail card (Enter on a list row) has two sub-tabs: i info
(name, totals, trackers, state) and w wire (the last announce exchange: the
request URL or summary, the response status, and the response body).
Press ! from any tab to open the plausibility linter: a read-only card that
flags settings a private tracker might find implausible (upload far past the
torrent size, near-line-speed upload to an almost empty swarm). Press ! or
Esc to close.
Navigation:
| Key | Action |
|---|---|
| 1-9, 0 | Jump to tab (0 = ratio graph) |
| Left, Right or h, l | Previous / next tab |
| Up, Down or k, j | Select row on list and Speeds tabs |
| Up, Down | Walk the upload multiplier on non-list tabs |
| Enter | Open the detail card for the selected torrent |
| Esc | Back to Dashboard, or close the open overlay |
| ? | Toggle the help overlay |
| ! | Toggle the plausibility linter overlay |
| : | Open the command palette (fuzzy-search all actions) |
Actions:
| Key | Action |
|---|---|
| f | Force-announce the selected torrent (resets its countdown) |
| x | Remove the selected/marked torrent(s); asks y/Esc to confirm (announces Stopped) |
| p | Toggle global pause (all upload stops) |
| r | Resume (clear global pause) |
| Space | Toggle the multi-select mark on the selected row |
| a / A | Mark all visible rows / clear all marks |
| e | Export the session snapshot to JSON (and the clipboard) |
| g | On Trackers tab: toggle per-torrent / by-tracker view |
| i / w | In the detail card: info / wire sub-tab |
| + or = | On Speeds tab: double the selected rate; elsewhere: increase multiplier |
| - or _ | On Speeds tab: halve the selected rate; elsewhere: decrease multiplier |
| k | On Client tab: re-init the emulated client (new key) |
| s | On Config tab: save config.toml |
| q or Ctrl+C | Quit (announces Stopped for all torrents, saves state) |
Force-announce (f) works on both seeding and downloading torrents. It resets
last_announce far enough back that the scheduler's download-tick gate fires on
the next wake.
When a torrent is mid-announce, its row displays (announcing...) and its
info-hash is zeroed out. Pressing f or x on that row produces a feed
message rather than a silent no-op.
Download phase state (info-hash, length, downloaded bytes, seeding flag) is written to:
~/.local/state/Mirage/state.json
or, if the XDG state directory is not available:
<torrent_dir>/.mirage_state.json
The write is atomic: a temp file is written and fsynced, then renamed over the target. Mirage reads this file at startup and applies matching entries to loaded torrents, so a torrent that was 40% downloaded at last run continues from 40% rather than restarting.
A missing or corrupt state file is handled silently; all torrents restart their download phase from zero.
If output_stats is configured, Mirage writes a JSON file after each scheduler
tick (approximately every announce interval). The file contains a snapshot of
all active torrents:
{
"started": "2024-01-15T10:30:00Z",
"client": "transmission-4.0.6",
"torrents": [
{
"name": "ubuntu-20.04.iso",
"length": 3145728000,
"private": false,
"uploaded": 10737418240,
"seeders": 42,
"leechers": 7,
"next_upload_speed": 1048576,
"downloaded": 3145728000,
"left": 0,
"state": "seeding",
"urls": ["https://tracker.example.com/announce"]
}
],
"total_uploaded": 10737418240
}The file is written atomically (temp + rename). A write failure is reported in the dashboard feed.
cargo build # debug build
cargo build --release # release build, required for scripts/make_app.sh
cargo test # run the test suite
The test suite covers bencode parsing, torrent parsing from real .torrent files, the speed curve and integration, download phase state transitions, config loading, and the event ring.
src/
main.rs Process entry point, global state, shutdown coordination
config.rs Config struct, TOML loading, client initialisation
torrent.rs Torrent struct, speed curve, download phase, bencode parser
bencode.rs Bencode decoder and encoder
state.rs Download phase persistence (atomic JSON write)
engine.rs Dashboard command handler (ForceAnnounce, Remove, SaveConfig, ReinitClient)
control.rs Global pause flag and command channel
watcher.rs Filesystem watcher for dynamic torrent add/remove
directory.rs Startup torrent folder scan
transmission.rs Transmission version auto-detection (Info.plist / binary)
json_output.rs Session stats JSON file writer
utils.rs Byte formatting, percent-encoding, SHA1
announcer/
mod.rs Module root
scheduler.rs Main announce loop, download tick, sleep cadence
tracker.rs HTTP announce, bencode response parsing, URL builder
udp.rs UDP tracker announce (BEP-15)
ui/
mod.rs TUI entry point, render loop, SIGWINCH/SIGCONT handling
draw.rs Terminal I/O: alternate screen, raw mode, paint, OSC-52 clipboard
render.rs Frame builder: all ten tab views, overlays, ANSI output
overlay.rs Help, command palette, detail card, confirm-remove state
snapshot.rs Lock-safe state snapshots for the renderer
history.rs Time-series ring buffer for the ratio graph and speed meter
events.rs In-process event ring buffer
view.rs Tab and row selection atomics
notify.rs Ratio-milestone desktop notifications (OSC 9 / osascript / notify-send)
keys.rs Blocking key reader (OS thread)
scripts/
make_app.sh macOS .app bundle builder
assets/
Mirage.png App icon source
tests/
*.torrent Real .torrent files used by the test suite
See CONTRIBUTING.md.
