1
A BitTorrent client, from scratch, in Go[github]
1
2
A BitTorrent client built from scratch in Go with no torrent
3
libraries — it hand-rolls the bencode codec, parses .torrent
4
files down to the info-hash, and announces to HTTP trackers to
5
discover a peer swarm.
6
7
Speaks the raw peer wire protocol (68-byte handshake, then
8
length-prefixed messages), downloads pieces in 16 KiB blocks with
9
SHA-1 verification, and saturates the swarm with a
10
goroutine-per-peer work queue. Magnet links are supported too,
11
fetching the torrent metadata itself from peers over ut_metadata
12
(BEP 9/10). Built end-to-end as the CodeCrafters 'Build Your Own
13
BitTorrent' challenge.
14
15
16
18
19
func (p Peer) Handshake(infoHash []byte, extensionSupported bool) (net.Conn, []byte, error) {
20
conn, err := net.DialTimeout("tcp", p.address, DialTimeout)
21
if err != nil {
22
return nil, nil, fmt.Errorf("can't connect to peer %s: %w", p.address, err)
23
}
24
25
handshakeMessage := make([]byte, 68)
26
// length of the protocol string (BitTorrent protocol) which is 19 (1 byte)
27
handshakeMessage[0] = 19
28
// the string BitTorrent protocol (19 bytes)
29
copy(handshakeMessage[1:20], []byte("BitTorrent protocol"))
30
// eight reserved bytes, which are all set to zero (8 bytes)
31
// or 20th bit flagged
32
if extensionSupported {
33
copy(handshakeMessage[20:28], []byte{0, 0, 0, 0, 0, 0b00010000, 0, 0})
34
}
35
// sha1 infohash (20 bytes) (NOT the hexadecimal representation, which is 40 bytes long)
36
copy(handshakeMessage[28:48], infoHash)
37
// peer id (20 bytes) (generate 20 random byte values)
38
copy(handshakeMessage[48:68], PeerId[:])
39
40
func (p Peer) Handshake(infoHash []byte, extensionSupported bool) (net.Conn, []byte, error) {
conn, err := net.DialTimeout("tcp", p.address, DialTimeout)
if err != nil {
return nil, nil, fmt.Errorf("can't connect to peer %s: %w", p.address, err)
}
handshakeMessage := make([]byte, 68)
// length of the protocol string (BitTorrent protocol) which is 19 (1 byte)
handshakeMessage[0] = 19
// the string BitTorrent protocol (19 bytes)
copy(handshakeMessage[1:20], []byte("BitTorrent protocol"))
// eight reserved bytes, which are all set to zero (8 bytes)
// or 20th bit flagged
if extensionSupported {
copy(handshakeMessage[20:28], []byte{0, 0, 0, 0, 0, 0b00010000, 0, 0})
}
// sha1 infohash (20 bytes) (NOT the hexadecimal representation, which is 40 bytes long)
copy(handshakeMessage[28:48], infoHash)
// peer id (20 bytes) (generate 20 random byte values)
copy(handshakeMessage[48:68], PeerId[:])
func Marshall(value interface{}) (string, error) {
switch it := value.(type) {
case string:
return fmt.Sprintf("%d:%s", len(it), it), nil
case int:
return fmt.Sprintf("i%de", value), nil
case byte: // uint8
return fmt.Sprintf("i%de", it), nil
case uint32:
return fmt.Sprintf("i%de", it), nil
case []interface{}:
res := "l"
for _, v := range it {
val, err := Marshall(v)
if err != nil {
return "", err
}
res += val
}
res += "e"
return res, nil
func (p Peer) Request(pieceIndex, blockOffset, blockLength uint32) error {
id := getIdByName("request")
length := uint32(13) // fixed lengta - id(1) + index(4) + offset(4) + length(4)
message := make([]byte, length+4)
binary.BigEndian.PutUint32(message[0:4], length)
message[4] = id
binary.BigEndian.PutUint32(message[5:9], uint32(pieceIndex))
binary.BigEndian.PutUint32(message[9:13], uint32(blockOffset))
binary.BigEndian.PutUint32(message[13:17], uint32(blockLength))
_, err := p.conn.Write(message)
return err
}
func (p Peer) ExtensionHandshake(extensionsIdMap map[string]byte) error {
payload, err := bencode.Marshall(map[string]interface{}{"m": extensionsIdMap})
if err != nil {
return fmt.Errorf("can't marshal extensions: %w", err)
}
length := uint32(2 + len(payload))
message := make([]byte, length+4)
// put length
binary.BigEndian.PutUint32(message[0:4], length)
// put top-level id for extension message
message[4] = 20
// put sub-level id for extension 'handshake' message
message[5] = 0
// put extensions id's map
copy(message[6:], payload)
_, err = p.conn.Write(message)
if err != nil {
return fmt.Errorf("can't write extension handshake: %w", err)
}
return nil
}