marque / hound
Public BitTorrent tracker (announce/scrape) for Marque platform
Requires
- php: ^8.3
- illuminate/routing: ^13.0
- illuminate/support: ^13.0
- marque/threepio: ^3.1
- marque/trove: ^4.5
Requires (Dev)
- laravel/pao: ^1.1
- mockery/mockery: ^1.6
- orchestra/testbench: ^11.0
- pestphp/pest: ^4.7
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Public BitTorrent tracker for the Marque platform. Open announce and scrape — no accounts, no announce keys, no ratio.
Hound is the deliberately small half of the tracker pair. Where marque/bloodhound knows who every peer is and holds them to a ratio, hound knows nothing about anybody. That is not a missing feature — a public tracker has no accountable user, so most of what bloodhound does has nothing to attach itself to.
Starting from scratch?
Hound is the tracker half only — no frontend, no upload UI. For a complete public tracker, install the set:
composer require marque/trove marque/hound marque/disguise
That resolves marque/threepio and marque/deck for you.
Running a private tracker with accounts and ratio instead? Use marque/bloodhound and marque/guise in place of hound and disguise.
⚠️ Never install hound alongside bloodhound. Hound registers an open
announceroute with no key and no authentication. A private tracker that merely has hound invendor/is carrying a keyless announce endpoint beside its authenticated one, and anyone who finds it can transfer without ever touching ratio accounting. Pick one tracker package.
Installation
Requires marque/trove and marque/threepio, which supplies the protocol primitives and Redis peer storage.
composer require marque/hound
Publish the config:
php artisan vendor:publish --tag=hound-config php artisan vendor:publish --tag=threepio-config
Hound ships no migrations of its own — it reads and writes trove's torrents table and
keeps live peers in Redis.
Endpoints
| Endpoint | Name | Purpose |
|---|---|---|
GET /announce |
tracker.announce |
Peer announces (started, completed, stopped) |
GET /scrape |
tracker.scrape |
Swarm statistics |
No announce key in the URL. That is the whole difference from bloodhound, whose
routes are /announce/{announce_key}. Anyone holding the .torrent can announce.
The paths are configurable, for migrations — a public tracker moving onto Marque cannot change the URL its circulating .torrent files announce to:
HOUND_ANNOUNCE_PATH=announce.php HOUND_SCRAPE_PATH=scrape.php
Route names stay tracker.announce and tracker.scrape whatever you set. There are no
key options here — hound is keyless by design; if you need announce keys you want
bloodhound, which has the full set.
Both routes run outside the web middleware group — no session, no CSRF, no cookies —
and behind threepio's BlockBrowsers middleware, which rejects anything that looks like
a web browser rather than a BitTorrent client.
Announce flow
- Required parameters validated (
info_hash,peer_id,port,uploaded,downloaded,left) - Torrent looked up by info_hash — an unregistered hash is refused
- Port checked against threepio's blacklist
- IP peer count checked against the limit
- Peer upserted in Redis with no user (
userId: null), or removed on astoppedannounce - Swarm counts projected onto the
torrentsrow when they've changed - Bencoded peer list returned
There is no user lookup, no anti-cheat pass, no byte accounting and no ledger. A public
announce touches the database only to identify the torrent, to increment
times_completed on a completed event, and, when the swarm has actually moved, to
update its counts.
Scrape
Accepts one or more info_hash parameters and returns complete / downloaded /
incomplete per torrent. Capped at 50 hashes per request; extras are dropped rather
than erroring. Unknown hashes are silently omitted from the response, per convention.
What hound does not do
Worth stating plainly, because the absences are design decisions rather than gaps:
| Not here | Why |
|---|---|
| Announce keys / user identity | Public tracker; peers are anonymous by definition |
| Ratio, upload/download accounting | Nothing to attribute bytes to |
| The announce ledger | See bloodhound's README — it is a private-tracker feature |
| Anti-cheat, client whitelisting | Cheating presumes a user with something to gain |
| Snatch records | No user to record a snatch against |
times_completed means something different here
Hound increments times_completed on every completed event it sees. It is a blind
increment, and it has to be: with no user attached to an announce, a client restarting
mid-download is indistinguishable from a second person finishing.
So on a public tracker the number means "completed events seen", not "distinct completions". Bloodhound's figure for the same column means the latter. The two are not comparable across the packages — that is the honest best a tracker with no accountable user can do.
Swarm counts on the torrent row
Live peers are in Redis, but a catalogue needs to filter and sort on swarm state — "hide
dead torrents", "sort by seeders" — and SQL cannot query Redis. So each announce also
writes seeders and leechers onto the torrents row, skipping the write entirely when
neither has changed (which is most announces).
A stopped announce removes the peer, so a client that leaves properly is counted out
straight away. A peer that vanishes without sending stopped (client killed, machine off)
is caught by hound:sync-swarm-counts, which hound schedules hourly. It sweeps each
torrent's expired peers out of Redis and writes the settled counts back to the row. That
needs Laravel's scheduler running.
A vanished peer counts as expired once peer_expiry has passed since its last announce
(an hour by default), and the next hourly sweep removes it. So a torrent whose swarm
vanished keeps its last-known counts for up to about two hours. The scheduler is a hard
requirement: only the sweep removes a peer that vanished without saying stopped, so
without it the counts never fall. A missed run only delays the correction.
php artisan hound:sync-swarm-counts
Configuration
Hound's own config is deliberately thin — the shared protocol settings (intervals, peer expiry, Redis, port blacklist, response format) live in threepio.
Published to config/hound.php:
Uploads: the private flag
| Key | Default | Description |
|---|---|---|
uploads.private_flag |
disallow |
disallow, require, warn_if_private, warn_if_public or allow |
Hound binds trove's TorrentFilePolicyInterface. With disallow, a torrent carrying the
private flag is refused, and the uploader is told to recreate it with "private"
unticked: the flag stops clients using DHT and peer exchange, which a public swarm relies
on. An unrecognised value is treated as disallow.
Downloads, for guests and members alike, get hound's own announce URL and a comment
linking the torrent's page, around the untouched info dictionary. Everything else the
uploader's client wrote, announce-list included, is dropped.
IP limiting
The primary abuse prevention for a public tracker, and with no accounts it is close to the only one available.
| Key | Default | Description |
|---|---|---|
ip_limiting.enabled |
true |
Enable the per-IP peer cap |
ip_limiting.max_per_ip |
50 |
Max distinct peer IDs from one IP |
HOUND_IP_LIMITING=true HOUND_MAX_PER_IP=50
At the cap, every announce from that IP gets a bencoded Too many connections from your IP failure, including re-announces from peers already in the swarm and stopped (so
those peers leave only when they expire). The count is of distinct peer IDs across all
torrents, so a client that uses one peer ID for every torrent counts once. Raise it if you
expect legitimate NAT'd or institutional traffic. Before threepio 3.2.1, dead peers could
stay in the IP count for good, locking a busy IP out. Hound accepts any threepio 3.x, so
make sure you're on 3.2.1 or later for the fix (#10804).
Logging — planned, not yet built
These keys ship in config/hound.php but nothing reads them yet. Setting them does
nothing today (#10809).
| Key | Default | Description |
|---|---|---|
logging.enabled |
false |
Announce logging, once built |
logging.channel |
stack |
Laravel log channel, once built |
HOUND_LOGGING=false HOUND_LOG_CHANNEL=stack
This is Laravel logging, not bloodhound's announce ledger — there is no queryable history table and no reconciliation. Off by default because announce volume makes for large logs fast.
Inherited from threepio
Set these in config/threepio.php, not here:
| Key | Default | Description |
|---|---|---|
announce_interval |
1800 |
Seconds between announces (sent to clients) |
min_announce_interval |
300 |
Minimum allowed interval |
peer_expiry |
3600 |
Seconds before inactive peers are dropped |
max_peers_per_announce |
50 |
Ceiling on peers returned, regardless of numwant |
peer_response_format |
auto |
auto, compact, or dictionary |
blacklisted_ports |
(see config) | Direct Connect, Kazaa, eMule, Gnutella, WinMX, legacy BT range |
redis.connection |
default |
Laravel Redis connection name |
redis.prefix |
marque: |
Key namespace |
Behind a proxy
Hound rejects private and reserved IP ranges in production only — under any other environment they pass, so local and staging work without special handling.
If you run behind Cloudflare, a load balancer or any reverse proxy, configure Laravel's trusted proxies before going live. Without it every peer registers with your proxy's address, which collapses them into one IP and trips the IP limit immediately.
Requirements
- PHP 8.3+
- Laravel 13+
- Redis
- marque/trove
- marque/threepio
License
MIT. See LICENSE.