Lost-Cluster

A small amount of data that does not belong to any file

Juggernaut

Juggernaut

Juggernaut is a BitTorrent research client in one PHP file. It takes a magnet link or an info hash, finds peers, retrieves the torrent metadata, downloads and verifies every piece, and then shuts everything down again. It is also an experiment in reciprocity. The client watches what each peer gives back and tries to learn how much upload bandwidth is needed to keep useful data coming in.

The file is a whopper monolith, almost 700kb, 18,000 lines long, feature rich. This is probably too much for a single PHP file, but it works faster than anything else. It is also considerably easier to copy to another machine than a small ecosystem of packages, extensions, configuration files and dependencies. Choose your own adventure.

Download

Usage

You need PHP 8.1 or newer. The script uses readonly properties and expects a command-line PHP installation with stream sockets. cURL is used for concurrent HTTP tracker requests when it is available.

php juggernaut.v1.php <magnet link or info hash>

For example:

php juggernaut.v1.php 1A4D8227CE373254EC20C34DEF0AAF37F11DCFAF

The same 40-character hash can be supplied without the magnet:? part. Magnet links may include trackers, a display name and explicit peer addresses. The script only accepts one torrent per invocation. There is no argument for changing the download directory or the research policy, so those things live in the constants near the beginning of the file. This is not a command-line interface. It is a configuration scavenger hunt.

Finding the swarm

The magnet link does not contain the file list or the piece hashes. It only identifies the torrent. Juggernaut starts by collecting possible peers from the trackers in the link, a public tracker list, explicit peers and the distributed hash table. HTTP, HTTPS and UDP trackers are supported. Failed public tracker-list downloads do not stop the run. There are usually enough other ways for a torrent to become sociable. The client is designed to be agressive in gathering peers and metadata. The DHT starts with the familiar public bootstrap nodes. It maintains routing buckets, sends parallel lookups and keeps tokens for peer announcements. Once a peer connection supports the extension protocol, PEX can add more contacts from the peer's own view of the swarm. Tracker discovery, DHT lookups and PEX continue while the download is running.

The client does not decide that a swarm is healthy merely because it has collected a large address book. It counts established connections, unchoked peers and peers that have supplied useful data recently. With too few useful suppliers it enters an aggressive discovery mode. When the market looks healthier, tracker and DHT activity slows down to normal or maintenance rates. I used the word “market” in the code because “a pile of IP addresses” sounded less scientific.

Getting through the router

Juggernaut opens a non-blocking TCP listener on an operating-system-selected port. It then tries to make that listener reachable from the internet through PCP, NAT-PMP or UPnP. If a mapping succeeds, the mapped external port is advertised to trackers and DHT peers. If it does not, the local listener port is advertised and everyone can hope the router is feeling cooperative. The listener accepts inbound peers as well as making outbound connections. It has temporary headroom above the normal connection target, which gives incoming handshakes a chance to finish before the client decides that it has enough friends. Mappings are renewed during a long session and removed during completion cleanup.

Metadata first, files later

The magnet link does not contain the file list or the piece hashes. Juggernaut requests metadata with the ut_metadata extension. Metadata is exchanged in blocks of up to 16 KiB, with a 10 MiB maximum. The assembled info dictionary is hashed with SHA-1 and compared with the hash from the magnet link. A mismatch is rejected before any torrent paths are created. The storage layer handles both single-file and multi-file torrents. It maps a torrent byte range onto one or more files, so a piece can cross a file boundary without upsetting the bookkeeping. Paths are checked component by component. Symbolic-link files are refused, and resolved paths must remain below the selected storage directory.

By default, files are created below the current working directory. Existing files are opened without truncating them, but the script does not scan them and rebuild a verified-piece database. A restart therefore does not resume from the last known good state. It starts with files that happen to be there and a memory that says they are not verified. This is safer than trusting old bytes, although it is less convenient.

Pieces, blocks and the end game

Each piece is split into blocks no larger than 16 KiB. Blocks are written as they arrive. When the final block of a piece is present, the entire piece is hashed. A failed hash resets the piece and records a verification failure. A successful hash marks the piece as verified and announces it to connected peers with a have message. Requests use a rarest-first order. A piece available from fewer connected peers is preferred, because losing its last supplier would be annoying. If two pieces have the same availability, the one with more completed blocks gets the earlier slot. This gives partially completed pieces a slight nudge towards becoming useful.

The number of outstanding requests is adaptive. A new connection starts with eight requests. Once the client has measurements, it estimates a depth that should cover about two seconds of transfer, taking observed latency into account. The configured limits are two to 64 requests per peer. A peer that is fast enough gets a larger window; a peer that is not contributing gets a smaller one.

The final blocks receive special treatment. When no more than 64 unfinished blocks remain, Juggernaut can request another copy of an old pending block from a different peer. At most two copies are allowed, and the original request must be at least 0.25 seconds old. The first valid copy wins and redundant requests are cancelled. This spends a little extra bandwidth to avoid waiting forever for one peer's final block. This strategy makes the client much faster than others.

Two ways to be a peer

The ordinary policy is called STANDARD. It ranks interested peers by their recent download contribution and gives upload slots to the better contributors. Fourteen regular slots are joined by one optimistic slot, which rotates every 30 seconds. This is the familiar “upload to people who upload to me, with one wild card” arrangement.

The other policy is called GREEDY, and it is enabled by default. It combines short-term and long-term useful download rates, giving the short-term rate 70% of the weight. It selects useful download peers, leaves 10% of its selection capacity for exploration and periodically replaces low-value connections with candidates that might be better. The name is descriptive rather than moral. The client is greedy about the download rate, not about taking without giving. It still has an upload budget, set to 10 MiB/s by default, and it attempts to spend that budget where the measured return looks best. A download can be up to 200% faster than traditional clients.

Finding an upload price

Some peers send useful data only after receiving data from us. Some peers send useful data for free. Some peers stop sending useful data when the upload allocation is reduced. Juggernaut records these observations as an estimated upload price. The price search lowers an allocation by 10% when a peer is responding, then raises it again when the response collapses. A collapsed response can mean that the remote peer choked us, that the useful byte count reached zero or that the current rate fell below half of the last good rate. The price is not a monetary price. No one is paying anyone. It is the observed upload rate associated with useful service from a peer, measured in bytes per second and occasionally in disappointment.

After collecting price observations, the client compares successive upload and download rates. The change in useful download rate is divided by the change in actual upload rate. The result is an estimate of marginal return, smoothed with an EWMA so one noisy sample does not rewrite history. Upload is allocated in 25 KiB/s quanta. Exploration receives its reserved share first. Known positive-return peers receive their floors, and any remaining budget is assigned to the peer with the best projected marginal return. If every projected return is absent or non-positive, the allocator leaves bandwidth unused. Spending everything is easy. Knowing why it was spent is the experiment.

Connection replacement uses the same information. A connection must be old enough to judge, and a candidate must clear the configured improvement threshold. Replacement counts are limited per interval, with a larger limit available when the candidate market and replaceable pool are both unusually large. Fresh exploration connections can also be given a short period of priority before the next round of judgement.

What it does not do

This is a BitTorrent v1 client. It accepts magnet links and info hashes, but not .torrent files directly. It uses TCP rather than µTP, has no peer-wire encryption, does not implement BitTorrent v2 or selective file downloading, and does not maintain persistent resume state. It also does not enforce the private-torrent flag while public discovery is enabled.

The memory limit is set to unlimited because apparently the correct amount of memory for research is “more”. Peer histories, connection buffers, routing state and torrent files can all grow during a run. The script is best treated as an experiment with a terminal attached, rather than as a replacement for a mature desktop client.