A Mega.nz download manager that runs inside a container, behind a PIA WireGuard tunnel, with adaptive concurrency and auto-rotating exits. Built because the official client and most third-party tools can't sustain parallel public-share downloads without throttling or stalling.
- Single binary deployment. Container brings up its own WireGuard interface in its netns; nothing on the host is touched apart from a published HTTP port and the download bind-mount.
- Resumable, chunked, integrity-checked. Range fetches, AES-CTR streaming decryption, per-chunk checkpoints in SQLite, and a full Mega meta-MAC verification pass after the file lands.
- Adaptive parallelism. AIMD probing tunes global and per-host concurrency to wherever the knee of the throughput curve is, without a hard-coded cap.
- Self-healing tunnel. When ≥80 % of a download's active chunks stall, the runner rotates to a fresh PIA region (round-robin, no repeats until the full deck is exhausted), then recreates the workers so wedged TCP sockets on the dead tunnel are abandoned.
- Public file links — both
mega.nz/file/ID#KEYand legacy#!ID!KEY. - Public folder links —
mega.nz/folder/ID#KEY(with optional/file/NODEHANDLEsuffix). Folders are walked at add time and expanded into one queued download per file; per-file keys are cached so each download resolves with a single Mega API call instead of re-walking the folder. - Mega API retry-with-backoff on transient integer errors (
-3EAGAIN,-4ERATELIMIT,-6ETOOMANY) — adding a many-file folder no longer trips the rate limiter. - Integrity verification. Mega's per-chunk MAC is recomputed locally
after every file (in a worker thread, with a live progress bar) and
compared against the meta-MAC embedded in the file key. Mismatches
surface as
errorin the UI with computed vs. expected values. - Web UI. Paste a link or batch of links, watch real-time throughput, pause / resume / delete per download, manual VPN rotate button.
- Authenticated Mega accounts: not supported. Public shares only.
git clone <this-repo> megaduck && cd megaduck
cp .env.example .env
# Edit .env: put your PIA creds in MEGADUCK_PIA_USER / MEGADUCK_PIA_PASS,
# or leave blank to run without a VPN (dev mode).
podman-compose up -d --build
# Open http://localhost:8080The container needs NET_ADMIN and /dev/net/tun so it can bring up its
own WireGuard interface inside its netns. The compose file already grants
both.
Without PIA creds, the VPN controller logs intended rotations instead of
running wg-quick, so all the adaptive logic still exercises end-to-end.
sudo ./scripts/install-host.shTemplates systemd/megaduck.service with the absolute path of the repo,
installs it to /etc/systemd/system/, enables it, and starts it. The unit
just shells out to podman-compose up -d --build; the container's own
restart: unless-stopped policy keeps it alive across crashes. Uninstall
with sudo ./scripts/uninstall-host.sh.
All settings are environment variables prefixed MEGADUCK_ and are
documented in .env.example. The defaults are tuned for a
gigabit symmetric link.
| Variable | Default | What it does |
|---|---|---|
MEGADUCK_PIA_USER / _PASS |
empty | PIA credentials. Blank disables the VPN. |
MEGADUCK_PIA_REGIONS |
10 US/CA regions | Comma-separated PIA region codes to round-robin through. |
MEGADUCK_PIA_INITIAL_REGION |
us_atlanta |
Region brought up by the container entrypoint. |
MEGADUCK_HOST_PORT |
8080 |
Host port the web UI is published on. |
MEGADUCK_DOWNLOAD_DIR |
/mnt/bulk/downloads/megaduck |
Where files land. |
MEGADUCK_STATE_DIR |
/var/lib/megaduck |
SQLite + scratch state. |
MEGADUCK_MAX_GLOBAL_CONCURRENCY |
64 |
Hard ceiling for the AIMD controller. |
MEGADUCK_CHUNK_SIZE |
16 MiB |
Range-fetch size per chunk. |
MEGADUCK_STALL_WINDOW_S |
60 |
Idle/low-bps duration before a chunk is "stalled". |
MEGADUCK_VPN_ROTATION_COOLDOWN_S |
300 |
Minimum time between rotations. |
MEGADUCK_MIN_BYTES_BETWEEN_ROTATIONS |
5 GiB |
Don't rotate until at least this much data has moved (overridden by force-rotate when throughput is near zero). |
MEGADUCK_VERIFY_MAC |
true |
Run the Mega meta-MAC check after every file. |
Per-chunk throughput meters feed an aggregate EMA over the probe window
(MEGADUCK_PROBE_INTERVAL_S, default 15 s). Each tick:
- If saturated (all slots busy) and <25 % of slots are starved, add a slot.
- If a previous expansion failed to improve aggregate throughput by
MEGADUCK_PROBE_IMPROVEMENT_THRESHOLD(default 7 %) or per-slot rates collapsed, drop a slot and freeze probing for two windows. - Per-host: if ≥2 of a host's slots fall below
MEGADUCK_MIN_SLOT_THROUGHPUT_BPStwo ticks running, shrink that host's cap; if all are healthy and saturated, grow it.
Result: parallelism climbs until either the server, the path, or your link stops paying for additional connections, and we stay just below that knee.
A per-download _stall_watcher task wakes every stall_window_s / 2
seconds and counts how many of the download's chunks have either gone idle
for stall_window_s or are pulling under stall_min_bps. If the stalled
fraction crosses stall_fraction_to_rotate (default 80 %), it asks the PIA
controller to rotate.
Rotation:
- Pops a region from a deck-shuffled queue. The deck is reshuffled only when empty, guaranteeing every other region is tried before any repeats.
- Re-runs PIA's
run_setup.shinside the container netns with the new region. The default route, DNS, and WireGuard config all swap in place. - Marks the download for auto-restart and hard-cancels its task — the
workers' httpx streams are wedged on the dead tunnel's TCP and would
never recover otherwise. The done-callback then schedules a fresh
start()after a short grace period.
Rotations honour a global cooldown and a minimum-bytes-since-last-rotation guard, both bypassed when aggregate throughput is essentially zero (we're dead in the water; rotate now).
python -m venv .venv && source .venv/bin/activate
pip install -e .
MEGADUCK_DOWNLOAD_DIR=$PWD/downloads \
MEGADUCK_STATE_DIR=$PWD/.state \
megaduckThe VPN controller becomes a no-op in this mode.
megaduck does not use network_mode: host. All VPN-related state lives
in the container's network namespace.
| Action inside the container | Host impact |
|---|---|
Creates a WireGuard pia/wg0 interface |
None (per-netns) |
| Adds default route via the VPN | None |
Rewrites /etc/resolv.conf to PIA DNS |
None |
Sets net.ipv4.conf.all.src_valid_mark=1 |
Container-netns sysctl only |
| Adds nftables/iptables rules | Per-netns, not visible to host |
Publishes MEGADUCK_HOST_PORT (default 8080) → container 8765 |
One port on the host |
The only host-visible side effects are the published port and the
bind-mount on MEGADUCK_DOWNLOAD_DIR. If the host itself runs a VPN, the
container's traffic exits the bridge, hits the host's default route, and
gets nested-encapsulated by PIA inside the container's netns. The two
WireGuard interfaces live in different netns and use different routing
tables, so there's no contention.
No kill-switch is installed: if the container's tunnel drops, traffic falls back through the bridge → host default route. Integrity-MAC verification still detects any tampering, and the stall watcher rotates the tunnel back up within ~30 s.
megaduck/
app.py FastAPI HTTP API + WebSocket status feed
config.py Pydantic settings (all MEGADUCK_* env vars)
db.py aiosqlite wrapper, schema migrations
bandwidth.py Per-slot meters + global aggregate
concurrency.py AIMD controller, per-host slot pool
downloader/
mega.py Mega.nz protocol: parsing, key crypto, API, MAC
runner.py Chunked downloader, stall watcher, auto-restart
vpn/
pia.py Region-rotation controller (deck-shuffle)
web/
templates/index.html
static/{app.js,app.css}
docker/ Dockerfile + entrypoint (PIA setup → app)
scripts/ install-host.sh / uninstall-host.sh
systemd/megaduck.service Templated systemd unit
tests/ (empty placeholder)
MIT.