This project implements a distributed BitTorrent system with P2P discovery using Docker, Go, and overlay networks.
git clone https://github.com/JabelResendiz/BitTorrent.git
cd BitTorrent/srcRun from the src directory:
docker build -t client_img -f client/Dockerfile .
docker build -t tracker_img -f tracker/Dockerfile .- Place the original file to share in:
archives/seeder/- Generate the
.torrentusingmktorrent
sudo apt install mktorrent
mktorrent -a http://tracker:8080/announce \
-o archives/torrents/video.torrent \
archives/seeder/video.mp4Where:
-aβ tracker URL (if using the overlay network, this does not matter)-oβ path where the .torrent will be saved- last argument β original file
Make sure this exists:
archives/torrents/video.torrentUsing the run_containers.sh script:
chmod +x script/run_containers.sh
./script/run_containers.shThis script:
- Creates the
bittorrentnetwork if it does not exist - Verifies that the
.torrentexists - Cleans up previous containers
- Starts the seeder
- Starts multiple leechers
- Each client starts with its corresponding port, hostname, and bootstrap
To run it without rebuilding the image, execute:
./script/run_containers.sh --no-buildIn one terminal:
go run tracker/cmd/main.goThis runs the tracker at:
localhost:8080Expected output:
tracker listening on :8080 interval=1800s data=tracker_data.jsonIn another terminal:
go run client/cmd/main.go \
--torrent="./archives/torrents/video.torrent" \
--archives="./archives/seeder"Typical output:
Tracker request: http://localhost:8080/announce?info_hash=...
Tracker response: map[complete:0 incomplete:1 interval:1800 peers:]Registered peers will appear in:
tracker_data.jsonExample seeder client:
docker network create bittorrent
docker run -d --name seeder --network bittorrent \
-v "$(pwd)/archives/seeder":/data \
-v "$(pwd)/archives/torrents":/torrents:ro \
-p 6000:6000 \
client_img \
--torrent=/torrents/video.torrent \
--archives=/data \
--hostname=seeder \
--discovery-mode=overlay \
--overlay-port=6000Example leecher client with bootstrap:
docker run -d --name leecher1 --network bittorrent \
-v "$(pwd)/archives/leecher1":/data \
-v "$(pwd)/archives/torrents":/torrents:ro \
-p 6001:6001 \
client_img \
--torrent=/torrents/video.torrent \
--archives=/data \
--hostname=leecher1 \
--discovery-mode=overlay \
--overlay-port=6001 \
--bootstrap=seeder:6000To allow several clients (leecher/seeder) to run on different machines in a distributed network, we use Docker Swarm and a shared overlay network.
From the src/ directory:
docker build -t client -f client/Dockerfile .
docker build -t tracker -f tracker/Dockerfile .You must copy these images to each PC, or upload them to a private registry if desired.
Initialize the cluster:
docker swarm initGet the token to add more nodes:
docker swarm join-token manager(Copy the resulting command to the other PCs.)
Then create the distributed overlay network:
docker network create --driver overlay bittorrent-netOn this PC, paste the command generated by join-token, for example:
docker swarm join --token <TOKEN>Then verify that the overlay network is available:
docker network lsYou will see something like:
bittorrent-net overlay swarmdocker service create \
--name tracker \
--network bittorrent-net \
--publish 8080:8080 \
trackerExample: run one seeder and two leechers distributed automatically by Swarm:
docker run -it --name seeder --network bittorrent-net \
-v "$(cd "$(dirname "$0")/.." && pwd)/archives/seeder":/data \
-v "$(cd "$(dirname "$0")/.." && pwd)/archives/torrents":/torrents:ro \
-p 6000:6000 \
client_img \
--torrent=/torrents/pelicula.torrent \
--archives=/data \
--hostname=seeder \
--discovery-mode=overlay \
--overlay-port=6000
docker run -it --name leecher1 --network bittorrent-net \
-v "$(cd "$(dirname "$0")/.." && pwd)/archives/leecher1":/data \
-v "$(cd "$(dirname "$0")/.." && pwd)/archives/torrents":/torrents:ro \
-p 6001:6001 \
client_img \
--torrent=/torrents/pelicula.torrent \
--archives=/data \
--hostname=leecher1 \
--discovery-mode=overlay \
--overlay-port=6001 \
--bootstrap=seeder:6000The replicas are distributed automatically across the Swarm PCs.
On any PC in the cluster:
docker node ls
docker service ls
docker service ps tracker
docker service ps seeder
docker service ps leecher- The clients and tracker (if needed) are running on several distributed machines
- They communicate through an overlay network
- Docker Swarm automatically balances and manages the nodes
.torrent files are bencoded.
A .torrent is a dictionary with these main keys (all strings are UTF-8):
- info: dictionary describing the torrent files
- single-file: one file
- multi-file: multiple files
- announce: tracker URL (string)
- announce-list: (optional) list of alternative trackers
- creation date: (optional) UNIX epoch date (integer, seconds since Jan 1, 1970)
- comment: (optional) free-form text from the author
- created by: (optional) client that generated the torrent
- encoding: (optional) string encoding in
info
- piece length: bytes per piece (integer)
- pieces: concatenation of 20-byte SHA-1 hashes
- private: (optional)
1: use listed trackers only0or absent: DHT, PEX, etc. may be used
A torrent that downloads a single file. The info dictionary directly contains the file size and name. All pieces correspond only to this file.
- name: file name (string)
- length: size in bytes (integer)
- piece length: piece size
- pieces: concatenated SHA-1 hashes
- md5sum: (optional) MD5 hash
"info": {
"name": "file.txt",
"length": 123456,
"piece length": 16384,
"pieces": "<concatenated SHA1 hashes>"
}A torrent containing several files inside a root directory. In this case, info contains a files list instead of a single length value.
"info": {
"name": "My_Folder",
"piece length": 32768,
"pieces": "<SHA1 concatenados>",
"files": [
{
"length": 1024,
"path": ["subdir1", "file1.txt"]
},
{
"length": 2048,
"path": ["subdir2", "file2.mp4"]
}
]
}- Size is usually a power of 2
- Historically: torrent β€ 75 KB
- Recommended: pieces β€ 512 KB for 8β10 GB torrents
- Common sizes: 256 KB, 512 KB, 1 MB
- All pieces have the same size except the last one
- In multi-file torrents, files are concatenated β pieces may cross file boundaries
- Each piece is represented by a SHA-1 hash (20 bytes) in
pieces
announce: tracker URL.announce list: (list of URL lists)creation date: datecomment: commentscreated by: authorinfo: dictionary with file details:length: file size in bytes.name: file name.piece length: size in bytes of each piece.pieces: concatenation of 20-byte SHA-1 hashes.private
{
"announce": "http://tracker.example.com/announce",
"announce-list": [
["http://tracker.example.com/announce"],
["http://backuptracker.example.net/announce"]
],
"creation date": 1695600000,
"comment": "Educational torrent example",
"created by": "ChatGPT TorrentMaker v1.0",
"info": {
"name": "file.txt",
"length": 123456,
"piece length": 16384,
"pieces": "<binary concatenation of 20-byte SHA1 hashes>",
"private": 0
}
}d
8:announce23:http://tracker.example.com/announce
13:announce-list
ll
23:http://tracker.example.com/announce
e
l
33:http://backuptracker.example.net/announce
e
e
13:creation datei1695600000e
7:comment27:Educational torrent example
10:created by27:ChatGPT TorrentMaker v1.0
4:infod
4:name8:file.txt
6:lengthi123456e
12:piece lengthi16384e
6:pieces40:<SHA1 piece 1><SHA1 piece 2>...
7:privatei0e
e
e
The tracker does not store βwho has each piece of the fileβ; it stores who is participating in that torrent in general.
π Specifically:
The torrent file is identified by its info_hash (SHA-1 of the info dictionary).
When a client announces to the tracker, it says:
βI am in the swarm for the torrent with info_hash = Xβ
It passes its peer_id, IP, port, and state (started, stopped, completed).
The tracker records: βPeer Y is in torrent Xβ.
Optionally, it counts how many peers have completed the file (seeders) and how many have not (leechers).
However, it does not know whether you have piece #5 or #200. Each peer knows that, and later reports it through a bitfield or have messages.
-
The tracker is an HTTP/HTTPS service that responds to HTTP GET requests. Requests include client metrics that help the tracker maintain general statistics about the torrent.
-
The response includes a list of peers that helps the client participate in the torrent.
The parameters used in the client's GET request to the tracker are:
-
info_hash: 20-byte SHA-1 hash (URL-encoded) of the
infokey's value in the metainfo file. This value is a bencoded dictionary, as defined by theinfokey. -
peer_id: 20-byte string (URL-encoded) used as the client's unique ID, generated at startup. It may have any value; there are no generation rules. It must be unique on the local machine (include process ID + timestamp).
-
port: the port number on which the client listens. Ports 6881β6889 are reserved for BitTorrent. If it cannot use one of them, some clients simply give up.
-
uploaded: total bytes uploaded by the client when sending the
startedevent to the tracker (in base-10 ASCII). -
downloaded: total bytes downloaded since the
startedevent (in base-10 ASCII). -
left: number of bytes the client still needs to download (in base-10 ASCII), that is, the amount remaining to have 100% of the torrent.
-
compact: if set to
1, the client accepts a compact response. In that case, the peer list is replaced by a 6-byte binary string per peer:- 4 bytes -> IP in network byte order
- 2 bytes -> port in network byte order
- Some trackers accept only compact = 1 and reject other requests
-
no_peer_id: indicates that the tracker may omit the
peer_idfield from the peer list. Ignored ifcompact = 1. -
event: if specified, it must be one of:
started: the first request to the tracker must include itstopped: when the client shuts down cleanlycompleted: when the torrent reaches 100% (but not if it was already complete at startup)empty: equivalent to omitting it (regular requests)
-
ip (optional): the client's real IP address. It may be in IPv4 or IPv6 format.
-
numwant (optional): number of peers the client wants to receive. It may be
0. If omitted, the usual default is50 peers. -
key (optional): extra identifier not shared with other peers. It allows a client to prove its identity if its IP changes.
-
trackerid (optional): if the tracker returned a
tracker_idin a previous announce, it must be sent again here.
GET /announce?info_hash=%12%34%56%78%9A%BC%DE%F1%23%45%67%89%AB%CD%EF%12%34%56%78%9A
&peer_id=-AZ2060-6wfG2wk6wWLc
&port=6881
&uploaded=0
&downloaded=0
&left=123456789
&compact=1
&event=started HTTP/1.1
Host: tracker.example.com:6969
User-Agent: MyBitTorrentClient/1.0
Connection: closeThe tracker responds with a text/plain document consisting of a bencoded dictionary with the following keys:
-
failure reason: if present, no other keys may be included. The value is a human-readable error message explaining why the request failed (string).
-
warning message (new, optional): Similar to failure reason, but the response is still processed normally. The warning message is displayed like an error.
-
interval: interval in seconds that the client must wait between regular requests to the tracker.
-
min interval (optional): minimum announce interval. If present, clients must not send announces more frequently than this.
-
tracker id: a string that the client must send in subsequent announces. If absent and a previous announce included a
tracker id, the old value must not be discarded; it must continue to be used. -
complete: number of peers with the complete file, that is,
seeders(integer). -
incomplete: number of non-seeder peers (
leechers). -
peers (dictionary model): the value is a list of dictionaries, each with the following keys:
- peer id: peer's self-selected ID, as described in the tracker request (string)
- ip: peer's IP address, either IPv6, IPv4, or a DNS name (string)
- port: peer's port number (integer)
-
peers (binary model): instead of using the dictionary model described above,
peersmay be a string consisting of multiples of 6 bytes. The first 4 bytes are the IP address and the last 2 bytes are the port number. Everything uses network byte order (big endian).
Response in "dictionary" mode (non-compact)
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 210
d
8:intervali1800e
8:completei12e
10:incompletei34e
5:peersl
d2:ip13:192.168.1.210:porti6881e7:peer id20:-ABCD1234567890ABCDEe
d2:ip13:192.168.1.211:porti6882e7:peer id20:-XYZ9876543210XYZabcde
ee
eResponse in "compact" mode (more commonly used in practice)
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 68
d8:intervali1800e8:completei12e10:incompletei34e5:peers12:\xC0\xA8\x01\xD2\x1A\xE1\xC0\xA8\x01\xD3\x1A\xE2eTrackers (optionally) support another type of request that queries the status of a particular torrent (or all torrents) managed by the tracker. This is known as the "scrape page" because it automates the otherwise tedious process of viewing the tracker's statistics page.
- It is useful when building a graphical interface or a console with detailed information.
- Internal decision optimizations (for more advanced clients): before joining a torrent, the client asks the tracker whether it is worth joining, avoiding a
torrentwith 0 seeders.
The scrape URL also uses the HTTP GET method, similar to the one described above. However, the base URL is different. To obtain it:
- Start with the announce URL.
- Locate the last '/' in it.
- If the text immediately after that '/' is not 'announce', the tracker is considered not to support the scrape convention.
- If it is, replace 'announce' with 'scrape' to obtain the scrape URL.
The scrape URL may include the optional info_hash parameter, a 20-byte value. This restricts the tracker's report to that particular torrent (otherwise, it returns statistics for all torrents managed by the tracker, which is not recommended because it uses more load and bandwidth).
The response to this HTTP GET method is a text/plain document consisting of a bencoded dictionary with the following keys:
- files: a dictionary containing one key/value pair for each torrent with available statistics
- Each key is a 20-byte binary
info_hash. - The associated value is another dictionary with:
- complete: number of peers with the complete file (seeds or seeders) (integer).
- downloaded: total number of times the tracker recorded a completion (
event=completed, meaning a client finished downloading the torrent). - incomplete: number of peers without the complete file (leechers) (integer).
- name (optional): internal torrent name, specified by the
namefield in the.torrentfile'sinfosection.
- Each key is a 20-byte binary
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 68
d5:filesd20:....................(info_hash)d8:completei5e10:downloadedi50e10:incompletei10eeeeGET /scrape?info_hash=%12%34%56%78... HTTP/1.1
Host: tracker.example.com:6969- a client must maintain state information for each connection it has with a remote peer.
- choked: indicates whether the remote peer has "choked" this client. When this happens, it tells the client that it will not respond to requests until it is unchoked. The client must not send block requests and must consider all pending requests discarded by the remote peer.
- interested: indicates whether the remote peer is interested in something this client offers. It notifies the client that the remote peer will start requesting blocks when the client stops choking it.
- am_choking: this client is choking the peer (initial = 1)
- am_interested: this client is interested in the peer (initial = 0)
- peer_choking: the peer is choking the client (initial = 1)
- peer_interested: the peer is interested in this client (peer_interested = 0)
- a block is downloaded when (am_interested = 1) and (peer_choking = 0)
- a block is uploaded when (peer_interested = 1) and (am_choking = 0)
It is a mandatory message and must be the first message transmitted by the client. It has a length of (49 + len(pstr)) bytes.
handshake: <pstrlen><pstr><reserved><info_hash><peer_id>
-
pstrlen: length of the
pstrstring, as a single raw byte -
pstr: protocol string identifier
-
reserved: 8 reserved bytes. All current implementations use 0. Each bit in these bytes may be used to change protocol behavior.
-
info_hash: the same
info_hashtransmitted in tracker requests (20 bytes) -
peer_id: 20-byte string used as the client's unique ID (the same one transmitted in tracker requests)
-
in version 1.0 of the BitTorrent protocol,
pstrlen=19andpstr="BitTorrent protocol" -
the connection initiator must transmit its
handshakeimmediately. The receiver may wait for the initiator'shandshakeif it can serve multiple torrents simultaneously. However, the receiver must respond as soon as it sees theinfo_hashportion of thehandshake(the peer ID will presumably be sent after the receiver sends itshandshake). The tracker's NAT-checking function does not send thepeer_idfield of the handshake. -
If a client receives a
handshakewith aninfo_hashit is not currently serving, it must close the connection. -
If the connection initiator receives a
handshakewhosepeer_iddoes not match the expected peer ID, it must close the connection. In other words, thepeer_idreceived by the tracker is expected to match the one in the handshake.
All integers in the peer wire protocol are encoded as four-byte values in big-endian. This includes the length prefix in all messages that follow the handshake.
The protocol consists of an initial handshake. Afterwards, peers communicate by exchanging messages prefixed with their length. The length prefix is an integer.
All remaining messages in the protocol take the form
<length prefix><message ID><payload>. The length prefix is a 4-byte big-endian value. The message ID is a single decimal byte. The payload depends on the message.
-
keep-alive (len=0000): 0-byte message, specified with a length prefix of 0. It has no message ID or payload. Peers may close a connection if they do not receive messages (keep-alive or otherwise) for a period of time, so a keep-alive must be sent to keep the connection alive if no command has been sent for a while. This is usually 2 minutes.
-
choke (<len=0001><id=0>): the choke message has a fixed length and no payload
-
unchoke (<len=0001><id=1>): has a fixed length and no payload
-
interested (<len=0001><id=2>): same as the previous two
-
not interested (<len=00001><id=3>): same
-
have (<len=00005><id=4>): fixed length. The payload is the zero-based index of a piece that has just been downloaded and hash-verified.
-
bitfield (<len=0001+X><id=5>): the bitfield message may only be sent immediately after completing the handshake, before any other message. It is optional and is not needed if a client has no pieces.
-
request (<len=0013><id=6>): fixed-length message used to request a block. The payload contains: index (piece index), begin (byte offset within the piece), and length (requested length).
-
piece (<len=0009+X><id=7>): variable-length message, where X is the block length. The payload contains: index (piece index), begin (offset within the piece), and block (data, a subset of the specified piece).
-
cancel (<len=0013><id=8>): fixed length and used to cancel block requests. The payload is identical to that of the request message. Typically used during the End Game phase.
-
port (<len=0003><id=9>): the port message is sent by recent versions of Mainline that implement a DHT tracker. listen-port (the port on which the peer's DHT node listens); this peer must be inserted into the local routing table if DHT is supported.
Internal strategies used by BitTorrent clients to improve performance and efficiency. The base protocol defines which messages can be sent (interested, request, piece, ...) but not when or how many to send.
-
Problem: imagine that each 16 KB block is downloaded and the client requests the next one only after it finishes. This means waiting for a full round trip (the time between sending a request and receiving the block). On networks with high latency or high bandwidth, this idle time wastes download capacity.
-
Solution: clients maintain a queue of pending requests ("request outstanding"). While downloading one block, several more have already been requested. When one arrives, the next is already on its way. Making 10 requests in parallel is better than making one request, keeping the channel full and using the available bandwidth.
- When you are the first seed (the one with the complete file), the goal is to distribute unique pieces as efficiently as possible.
- The idea is for the seed to pretend it does not have all pieces and only "announce" one piece to peers at a time. This shares different pieces with each peer so that they can later exchange them among themselves. This reduces the total amount of data the seed must upload before another seed is created.
- Recommended only when seeding a new torrent (when you are the first seed).
- Clients may choose to download pieces in random order. A better strategy is to download pieces in increasing order of rarity (rarest first).
- The client can determine this by maintaining each peer's initial
bitfieldand updating it with everyhavemessage. - It can then download the pieces that appear least frequently in those bitfields.
- Any rarest-first strategy should include some randomization among the least common pieces, because if many clients try to download the same rarest piece, the opposite effect will occur.
- When a download is nearly complete, the last blocks tend to arrive slowly.
- To speed this up, the client sends requests for all missing blocks to all its peers.
- To prevent this from becoming inefficient, the client also sends a cancel message to all others whenever a block arrives.
- The protocol uses
chokingto control who you upload data to. You cannot upload to everyone at once without overwhelming TCP, so you upload only to some peers. - The basic rule is: every 10 seconds, choose the 4 peers uploading to you the fastest (unchoke them). Block requests from the others. This implements tit-for-tat: "you give me speed, I give you speed."
- The optimized version chooses one peer at random every 30 seconds (even if it is not sending anything) to test whether it could be better than the current 4. If it is fast, it joins the group and another peer leaves.
- Sometimes a peer stops sending you pieces (it ignores you).
- If more than 1 minute passes without receiving data, the client marks it as "snubbed" and stops uploading to it, except in the case of
optimistic unchoke. - The goal is to avoid wasting time with peers that do not cooperate.
- reserved bit: the third least significant bit of the 8th reserved byte
reserved[7] |= 0x04 - this speeds up the startup of a new peer in the swarm (the network of peers sharing a torrent).
- Normally, if a peer is choked, it cannot request pieces.
- With this extension, certain peers can download specific pieces while choked, accelerating initial synchronization.
- reserved bit
reserved[7] |= 0x01(last bit of the eighth byte). - allows peers to be discovered without a centralized tracker. Each peer becomes a node in a DHT network, which stores information about which peers have which torrents.
- the system continues to work if the tracker goes down
- peers find one another using a distributed hash table (based on Kademlia)
- BEP-32 adds IPv6 support
- has no specific reserved bit
- allows BitTorrent connections to be encrypted or disguised so internet providers do not detect or limit torrent traffic.
- obfuscates the handshake and protocol messages
- helps evade traffic shaping or throttling
- improves privacy
- does not use a reserved bit
- allows an HTTP server to act as a seed (data source), in addition to regular peers
- in short, you can download parts of the torrent from a web server, not only from other users
- reserved bit
reserved[5] = 0x10, the 4th most significant bit of the sixth byte - defines a generic way to announce and negotiate extensions between clients
- each additional extension (for example DHT, metadata exchange, peer exchange) is announced and negotiated through this protocol
- reserved bits 47 and 48
- allows peers to decide which extension to use when both support several.
- avoids conflicts when two clients implement different extension systems.
- reserved bit: 21
- allows peers to take other peers' geographic location into account. They can then prefer downloading from nearby peers, reducing latency and network load.
- reserved bit in the first byte
0x01 - adds peer information and connection-statistics exchange.
- was used in older versions of SimpleBot
- reserved bit in the first two bytes
ex - used to exchange additional information (authentication, statistics, chat messages)
- not officially documented; it is known through reverse engineering
- Peer Exchange (PEX) : https://www.bittorrent.org/beps/bep_0011.html
- DHT Protocol : https://www.bittorrent.org/beps/bep_0005.html
- BitTorrent Wiki: https://wiki.theory.org/BitTorrentSpecification