Link to this headingBittorrent

How it works

  1. Use either a magnet link or a torrent file to get a tracker.
  2. Query the tracker for upto date peers
  3. Connect to the peers and request pieces of data.
    • Send Bittorrent Handshake
    • Optional: Send what pieces of data you currently have
    • Send “Intrested” message to the peer
    • Wait for an “Unchoke” message. This means that the peer is ready to send you data

Link to this headingMagnet URL

Example

magnet:?xt=urn:btih:PVJBBJYRFEOXDAOW4B2M4XV5K3Z75XLA&dn=debian-12.10.0-amd64-netinst.iso&xl=663748608&tr=http%3A%2F%2Fbttracker.debian.org%3A6969%2Fannounce

A magnet URL contains:

  1. The tracker URL
  2. Extra Tracker URLs
  3. The Torrent Name
  4. The number of pieces the torrent has
  5. The Info hash which is the unique id for the torrent

How to download using a magnet url:

  1. Parse the magnet url to get the information inside it.
  2. Query the tracker for upto date peers
  3. Connect to the peers and request pieces of data.
    • Send Bittorrent Handshake
    • Optional: Send what pieces of data you currently have
    • Send “Intrested” message to the peer
    • Wait for an “Unchoke” message. This means that the peer is ready to send you data
    • Optional: Wait for a “bitfield” message to see what pices they have availe for you to request
  4. Send a “Request” message with the index and offset to download a chunk of data.
  5. Hash the received chunk and validate that it is the same hash as in the torrent file
  6. Go to 4 until all chunks are downloaded

Link to this headingTorrent Files

A torrent file contains:

  1. The tracker URL
  2. The number of pieces or blocks of data
  3. The file and folder names and block size of the files
  4. The [SHA1](/Crypto/Hash Functions/SHA1) Hashes of each block piece of data.

How to download using a torrent file:

  1. Parse the torrent file to get the information inside it.
  2. Query the tracker for upto date peers
  3. Connect to the peers and request pieces of data.
    • Send Bittorrent Handshake
    • Optional: Send what pieces of data you currently have
    • Send “Intrested” message to the peer
    • Wait for an “Unchoke” message. This means that the peer is ready to send you data
    • Optional: Wait for a “bitfield” message to see what pices they have availe for you to request
  4. Send a “Request” message with the index and offset to download a chunk of data.
  5. Hash the received chunk and validate that it is the same hash as in the torrent file
  6. Go to 4 until all chunks are downloaded

Parsing a Torrent File:

import os, sys def bdecode(data): def decode_next(index): if data[index] == ord('i'): # Integer: i<integer>e end = data.index(b'e', index) number = int(data[index+1:end]) return number, end + 1 elif data[index] == ord('l'): # List: l<items>e index += 1 result = [] while data[index] != ord('e'): item, index = decode_next(index) result.append(item) return result, index + 1 elif data[index] == ord('d'): # Dictionary: d<key><value>e index += 1 result = {} while data[index] != ord('e'): key, index = decode_next(index) value, index = decode_next(index) result[key] = value return result, index + 1 elif data[index] in b'0123456789': # String: <length>:<data> colon = data.index(b':', index) length = int(data[index:colon]) start = colon + 1 end = start + length return data[start:end], end else: raise ValueError(f"Invalid bencoded data at position {index}") result, final_index = decode_next(0) if final_index != len(data): raise ValueError("Extra data after decoding") return result if __name__ == "__main__": file_name = "debian-12.10.0-amd64-netinst.iso.torrent" torrent_file_path = os.path.join(os.getcwd(), file_name) if not os.path.isfile(torrent_file_path): print(f"Error: {torrent_file_path} is not a valid file.") sys.exit(1) with open(torrent_file_path, 'rb') as f: torrent_data = f.read() torrent_dict = bdecode(torrent_data) #Parse the pieces to an array of hex strings pieces = torrent_dict[b'info'][b'pieces'] pieces = [pieces[i:i+20].hex() for i in range(0, len(pieces), 20)] torrent_dict[b'info'][b'pieces'] = pieces print() print(torrent_dict) #{b'announce': b'http://bttracker.debian.org:6969/announce', b'comment': b'Debian CD from cdimage.debian.org', b'created by': b'mktorrent 1.1', b'creation date': 1742039925, b'info': {b'length': 663748608, b'name': b'debian-12.10.0-amd64-netinst.iso', b'piece length': 262144, b'pieces': ['4e6a88778c00584c9d1494f3504f885fe6354569', 'c4405c23fab73c6a05e25b5306612d6d35510afd', '9915ee1573f64c676f82a2f40847152243544cfe', '976e0a51e8d6aadeb6657a6c7a8689f69e1d0e15', 'f324bfbf2fe575e6bc0c451d91588ceb081207b1', 'e61a61a1fb2c53b33395671a22cae004371654d2', 'dc2692544e67dc3772ccb0bb32ffb8007c33b8a8', '09835cee94b5674152c4ae389ddf8db7929d093e', 'cd1ed06cffea2c85d2789c879765e03c00791ce2', '3891fafac9a153f9d1935e48ecf67294025dbfa2', '9b10b44b715ed46a76e5fe3f6b9fe4be4930e05f', 'a4c0a93365c8b1c9cdecea42b592b87caf7d5560', '9cdbaffada32e10f79bca03bf532080749bed39b', 'abd9164db72567f3116d39ae75208e8f72f88755', '2c16b79182b43aa06091c7aa1bf3d69b48bec8af', '46edba3c5e5afc43a2cb4665387a615105db7281', #...

NOTE: The Info hash is used to uniquely identify the torrent. This is just the [sha1](/Crypto/Hash Functions/SHA1) hash of the torrent_dict['info'] data.

Link to this headingTrackers

Trackers only have a list of the ipv4 and ipv6 peers that you can connect to.

Get Tracker Information:

import http.client import urllib.parse import base64 def bdecode(data): def decode_next(index): if data[index] == ord('i'): # Integer: i<integer>e end = data.index(b'e', index) number = int(data[index+1:end]) return number, end + 1 elif data[index] == ord('l'): # List: l<items>e index += 1 result = [] while data[index] != ord('e'): item, index = decode_next(index) result.append(item) return result, index + 1 elif data[index] == ord('d'): # Dictionary: d<key><value>e index += 1 result = {} while data[index] != ord('e'): key, index = decode_next(index) value, index = decode_next(index) result[key] = value return result, index + 1 elif data[index] in b'0123456789': # String: <length>:<data> colon = data.index(b':', index) length = int(data[index:colon]) start = colon + 1 end = start + length return data[start:end], end else: raise ValueError(f"Invalid bencoded data at position {index}") result, final_index = decode_next(0) if final_index != len(data): raise ValueError("Extra data after decoding") return result def parse_magnet_uri(magnet_uri): params = dict(param.split('=') for param in magnet_uri[8:].split('&')) info_hash = params.get('xt', '').split(':')[-1] name = params.get('dn', '') xl = params.get('xl', '') tr = urllib.parse.unquote(params.get('tr', '')) return {"torrent_info_hash": info_hash, "filename": name, "xl": xl, "tracker": tr} if __name__ == "__main__": magnet_uri = "magnet:?xt=urn:btih:PVJBBJYRFEOXDAOW4B2M4XV5K3Z75XLA&dn=debian-12.10.0-amd64-netinst.iso&xl=663748608&tr=http%3A%2F%2Fbttracker.debian.org%3A6969%2Fannounce" components = parse_magnet_uri(magnet_uri) # Parse the tracker URL parsed_url = urllib.parse.urlparse(components['tracker']) # Build the GET request raw_infohash = base64.b32decode(components['torrent_info_hash']) encoded_infohash = urllib.parse.quote_from_bytes(raw_infohash) query_params = { "info_hash": encoded_infohash, "peer_id": "-CG0001-6wfG2wk6wWLz", "port": 6881, "uploaded": 0, "downloaded": 0, "left": int(components['xl']), "compact": 1 } query_string = urllib.parse.urlencode(query_params, safe="%") # VERY IMPORTANT: safe="%" path = f"{parsed_url.path}?{query_string}" # Make the connection conn = http.client.HTTPConnection(parsed_url.hostname, parsed_url.port) conn.request("GET", path, headers={ "User-Agent": "curl/8.13.0", "Connection": "close", "Accept": "*/*" }) response = conn.getresponse() if response.status == 200: data = response.read() print("Tracker replied with", len(data), "bytes") print(f"Decoded data: {bdecode(data)}") v4peers = [] v6peers = [] #Parse the peers into a ip:port format peers = bdecode(data)[b'peers'] peers6 = bdecode(data)[b'peers6'] for i in range(0, len(peers), 6): ip = ".".join(str(peers[i+j]) for j in range(4)) port = (peers[i+4] << 8) + peers[i+5] print(f"Peer: {ip}:{port}") v4peers.append(f"{ip}:{port}") for i in range(0, len(peers6), 18): ip = ":".join(f"{peers6[i+j]:02x}" for j in range(16)) port = (peers6[i+16] << 8) + peers6[i+17] print(f"Peer: {ip}:{port}") v6peers.append(f"{ip}:{port}") print("IPv4 Peers:", v4peers) print("IPv6 Peers:", v6peers) else: print("Tracker error:", response.status, response.reason) conn.close() #Tracker replied with 472 bytes #Decoded data: {b'interval': 900, b'peers': b'\xc3\x80\xac:\xc8\xd5-V\xd2\xa4\xc6\xbfS\x94\xf5\xba\xc8\xd6\x96\x88\xa0w\xda\x91GOP\x84\x1a\xe1\x02"a\x90\x1a\xe1\xb9\x94\x01N\x9c3W4h\x89\xc8\xd5.\xbf\xe6\xf3p\xda\x9b]\xc0\x89\x1a\xe1r#\xf5\x96\x1dL\x18;\x9c\x85\xd2\x14\xb9\x95[\r\xc7Nl8J\xf7i\xffk\xc0)xA\xf1\x17\x80 f\xc4\xed\x87\x17\xaa\xab\xc8\xd5\x88>!onV\xbc\x06\xb08\xb5W^i~\xf3\xc8\xd5\x18\xcf\xa8\xf3\xf3\x8c\x95\x16YI\x1a\xe1\xa1\x1d\xdf\xf5BU\xc1mx\xab\xfbG\x18\xf7D\x82|{N.0\x0b\xc3P\xa2\xfd\x14\xc9\x18\xcaW\x1bR\x99\xc8\xd5\x05\x98\x82)g%Yi\xca\x86\xca\xc6\xc1\xc0$\n\xa5\x98N8\xf8L\xd9\x0fW\x9c\x8e\xca\xc8\xd5V}\xe0\x8f\x8c\x12QgmT\x07K\x95X\x1b\x93\x145\xc6\x10\x87\xa1\xe8mO\x7f\x860\xc3PRA\xa0\x0f\xd3\xe2', b'peers6': b" \x01\t\x9a$\xedM\x00\x9ek\x00\xff\xfezD;\xc8\xd5*\t^A\x0e\xb0\x03\xf0H?\xaf\xa4~\xe7\x11\xae\xc8\xd5(\x04\x01L;\xb6\x15y\xfb\xb6\x92\xe4\x8do\x94\x90\xe3\xd0 \x01\xb0\x11@\x06x&\x00\x00\x00\x00\x00\x00\x00\n\x1dL*\x01\x02a\x08\xfd\x86\x00+\xe3\xc4Ib\x87\x83\xc7YI \x01LL#)\xfa\x00\n._\xff\xfe\x01\x18>\xf3D \x01LL\x1e\xa0P\x00\x02\xe0L\xff\xfe\x14c\x8f\xb5W \x01LL\x1e\xa0P\x00\x0c'IGh\x90]L\xb5W*\x03\xa9\xc0\xf2\x00\x00\x00\x88S` \x93\xa8\x1ba\xa5\x98 \x03\x00\xea\x0f\x00I\x00.\xf0]\xff\xfe\xdc\xffc\xc8\xd5*\x02/\n\xe1\x04\xd1\x00P1\xb1\xff\xfe&\xba\xb0\x8c\x12"} #Peer: 195.128.172.58:51413 #...

Link to this headingFull single threaded implementation

import http.client import urllib.parse import base64, hashlib, os, socket, time import traceback def bdecode(data): def decode_next(index): if data[index] == ord('i'): # Integer: i<integer>e end = data.index(b'e', index) number = int(data[index+1:end]) return number, end + 1 elif data[index] == ord('l'): # List: l<items>e index += 1 result = [] while data[index] != ord('e'): item, index = decode_next(index) result.append(item) return result, index + 1 elif data[index] == ord('d'): # Dictionary: d<key><value>e index += 1 result = {} while data[index] != ord('e'): key, index = decode_next(index) value, index = decode_next(index) result[key] = value return result, index + 1 elif data[index] in b'0123456789': # String: <length>:<data> colon = data.index(b':', index) length = int(data[index:colon]) start = colon + 1 end = start + length return data[start:end], end else: raise ValueError(f"Invalid bencoded data at position {index}") result, final_index = decode_next(0) if final_index != len(data): raise ValueError("Extra data after decoding") return result class BitTorrent: def __init__(self, torrent_file): self.tracker_url = None self.files = [] self.announce_list = [] self.peer_id = "-CG0001-6wfG2wk6wWLz" self.info_hash = None self.num_pieces = 0 self.current_piece = 0 self.block_size = 2**14 self.v4peers = [] self.v6peers = [] if torrent_file.startswith("magnet:"): self.from_magnet_uri(torrent_file) else: # Assume it's a torrent file if not os.path.isfile(torrent_file): raise ValueError(f"{torrent_file} is not a valid file.") else: self.from_torrent_file(torrent_file) def _parse_pieces(self, pieces): # Parse the pieces to an array of hex strings return [pieces[i:i+20].hex() for i in range(0, len(pieces), 20)] def _parse_magnet_uri(self, magnet_uri): params = dict(param.split('=') for param in magnet_uri[8:].split('&')) info_hash = params.get('xt', '').split(':')[-1] name = params.get('dn', '') xl = params.get('xl', '') tr = urllib.parse.unquote(params.get('tr', '')) return {"torrent_info_hash": base64.b32decode(info_hash), "filename": name, "xl": xl, "tracker": tr} def from_magnet_uri(self, magnet_uri): components = self._parse_magnet_uri(magnet_uri) self.tracker_url = urllib.parse.urlparse(components['tracker']) self.files.append(components['filename']) self.num_pieces = int(components['xl']) self.info_hash = components['torrent_info_hash'] return components def from_torrent_file(self, torrent_file): with open(torrent_file, 'rb') as f: torrent_data = f.read() torrent_dict = bdecode(torrent_data) # Parse the pieces to an array of hex strings torrent_dict[b'info'][b'pieces'] = self._parse_pieces(torrent_dict[b'info'][b'pieces']) self.tracker_url = urllib.parse.urlparse(torrent_dict.get(b'announce', None)) self.length = torrent_dict["info"].get(b'length', None) self.piece_length = torrent_dict["info"].get(b'piece length', None) self.num_pieces = int(torrent_dict["info"].get(b'pieces', None)) self.files = torrent_dict["info"].get(b'name', None) self.info_hash = hashlib.sha1(torrent_dict["info"]).digest() return torrent_dict def create_handshake(self): protocol_id = 19 pstr = b"BitTorrent protocol" extention_flags = b"\x00" * 8 handshake = ( bytes([protocol_id]) + pstr + extention_flags + self.info_hash + self.peer_id.encode("utf-8") ) return handshake def get_tracker_info(self, encoded_infohash): query_params = { "info_hash": self.info_hash, "peer_id": self.peer_id, "port": str(self.tracker_url.port), "uploaded": 0, "downloaded": 0, "left": int(self.num_pieces), "compact": 1 } query_string = urllib.parse.urlencode(query_params, safe="%") # VERY IMPORTANT: safe="%" path = f"{self.tracker_url.path}?{query_string}" # Make the connection conn = http.client.HTTPConnection(self.tracker_url.hostname, self.tracker_url.port) conn.request("GET", path, headers={ "User-Agent": "curl/8.13.0", "Connection": "close", "Accept": "*/*" }) response = conn.getresponse() if response.status == 200: data = response.read() print("Tracker replied with", len(data), "bytes") #print(f"Decoded data: {bdecode(data)}") self.v4peers = [] self.v6peers = [] #Parse the peers into a ip:port format peers = bdecode(data)[b'peers'] peers6 = bdecode(data)[b'peers6'] for i in range(0, len(peers), 6): ip = ".".join(str(peers[i+j]) for j in range(4)) port = (peers[i+4] << 8) + peers[i+5] #print(f"Peer: {ip}:{port}") self.v4peers.append(f"{ip}:{port}") for i in range(0, len(peers6), 18): ip = ":".join(f"{peers6[i+j]:02x}" for j in range(16)) port = (peers6[i+16] << 8) + peers6[i+17] #print(f"Peer: {ip}:{port}") self.v6peers.append(f"{ip}:{port}") else: print("Tracker error:", response.status, response.reason) conn.close() def recive_message(self, sock): # Read 4 bytes (message length) length_data = self._recv_all(sock, 4) if not length_data: raise Exception("Connection closed before receiving message length") message_length = int.from_bytes(length_data, byteorder='big') if message_length == 0: return None, b'' # keep-alive message # Read the full message full_msg = self._recv_all(sock, message_length) if not full_msg: raise Exception("Connection closed before receiving message body") message_id = full_msg[0] message = full_msg[1:] return message_id, message def _recv_all(self, sock, num_bytes): data = b'' while len(data) < num_bytes: chunk = sock.recv(num_bytes - len(data)) if not chunk: return None data += chunk return data def send_message(self, sock, message_type, message=None): if message_type == "choke": sock.sendall((1).to_bytes(4, 'big') + b"\x00") elif message_type == "unchoke": sock.sendall((1).to_bytes(4, 'big') + b"\x01") elif message_type == "interested": sock.sendall((1).to_bytes(4, 'big') + b"\x02") elif message_type == "not_interested": sock.sendall((1).to_bytes(4, 'big') + b"\x03") elif message_type == "have": sock.sendall((len(message) + 1).to_bytes(4, 'big') + b"\x04" + message) elif message_type == "bitfield": if message is None: message = b"\x00" * (self.piece_length // 8) # Assuming no pieces are available sock.sendall((len(message) + 1).to_bytes(4, 'big') + b"\x05" + message) elif message_type == "request": sock.sendall((len(message) + 1).to_bytes(4, 'big') + b"\x06" + message) elif message_type == "piece": sock.sendall((len(message) + 1).to_bytes(4, 'big') + b"\x07" + message) elif message_type == "cancel": sock.sendall((len(message) + 1).to_bytes(4, 'big') + b"\x08" + message) else: raise ValueError("Unknown message type") def connect_to_peer_and_download(self, peer_ip, peer_port): print(f"Connecting to peer: {peer_ip}:{peer_port}") sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(10) try: sock.connect((peer_ip, peer_port)) # 1. Send handshake handshake = self.create_handshake() sock.sendall(handshake) # 2. Receive handshake response = sock.recv(68) if response[0] != 19 or response[1:20] != b"BitTorrent protocol": raise Exception("Invalid handshake") # 3. Send 'bitfield' message (ID = 5) #Not reqired because we dont have any pieces yet #self.send_message(sock, "bitfield") # 3. Send 4byte length + ID for 'interested' (ID = 2) self.send_message(sock, "interested") # 4. Wait for 'unchoke' (ID = 1) for 30 seconds start_time = time.time() while time.time() - start_time < 30: try: message_id, message = self.recive_message(sock) if message_id == 1: print("Peer unchoked us") break elif message_id == 3: print("Peer sent 'not interested'") os.exit() except socket.timeout: continue else: raise TimeoutError("Timed out waiting for 'unchoke'") # 5. Request and receive piece(s) for index in range(self.current_piece, self.num_pieces): offset = 0 # Send a request for the first block of the piece self.send_message(sock, "request", index.to_bytes(4, 'big') + offset.to_bytes(4, 'big') + self.block_size.to_bytes(4, 'big')) # Receive 'piece' message (ID = 7) msg_id, piece_payload = self.recive_message(sock) if msg_id != 7: print(f"Expected message_id 7, got {msg_id}") continue piece_idx = int.from_bytes(piece_payload[:4], 'big') begin = int.from_bytes(piece_payload[4:8], 'big') block = piece_payload[8:] print(f"Downloaded piece {piece_idx}, begin {begin}, {len(block)} bytes") with open(f"piece_{self.current_piece}.bin", "wb") as f: f.write(block) self.current_piece = piece_idx + 1 break # For now, download only one piece except Exception as e: print(f"Failed to download from peer {peer_ip}:{peer_port} - {e}") print(traceback.format_exc()) finally: sock.close() def main(torrent_info): torrent = BitTorrent(torrent_info) torrent.get_tracker_info(urllib.parse.quote_from_bytes(torrent.info_hash)) # Connect to the first peer and download a piece if torrent.v4peers: for peer in torrent.v4peers: peer_ip, peer_port = peer.split(":") torrent.connect_to_peer_and_download(peer_ip, int(peer_port)) elif torrent.v6peers: peer_ip, peer_port = torrent.v6peers[0].split(":") torrent.connect_to_peer_and_download(peer_ip, int(peer_port)) main("magnet:?xt=urn:btih:PVJBBJYRFEOXDAOW4B2M4XV5K3Z75XLA&dn=debian-12.10.0-amd64-netinst.iso&xl=663748608&tr=http%3A%2F%2Fbttracker.debian.org%3A6969%2Fannounce")

Link to this headingDistributed Hash Tables (DHT)

DHT is a way to look for more Peers. Instead of using a centralized server to look for peers like with an announce server. You can ask peers if they know other peers to connect to. Doing so recursively until you make a list of all of the peers that are available to download pieces from.

import socket import random import bencodepy import time import hashlib import urllib.parse import binascii BOOTSTRAP_NODES = [("router.bittorrent.com", 6881), ("dht.transmissionbt.com", 6881)] TRANSACTION_ID_LENGTH = 2 MAX_RECURSION_DEPTH = 3 def random_node_id(): return bytes(random.getrandbits(8) for _ in range(20)) def extract_infohash(magnet_link): parsed = urllib.parse.urlparse(magnet_link) xt = urllib.parse.parse_qs(parsed.query).get("xt", [None])[0] if xt and xt.startswith("urn:btih:"): ih = xt[9:] if len(ih) == 40: # hex return bytes.fromhex(ih) elif len(ih) == 32: # base32 return binascii.a2b_base64(ih.upper()) raise ValueError("Invalid magnet link") def decode_nodes(nodes_data): nodes = [] for i in range(0, len(nodes_data), 26): nid = nodes_data[i:i+20] ip = ".".join(str(b) for b in nodes_data[i+20:i+24]) port = int.from_bytes(nodes_data[i+24:i+26], "big") nodes.append((nid, ip, port)) return nodes def send_krpc_query(sock, message, addr): sock.sendto(bencodepy.encode(message), addr) def random_tid(): return bytes(random.randint(0, 255) for _ in range(TRANSACTION_ID_LENGTH)) def get_peers_message(info_hash, node_id): return { b"t": random_tid(), b"y": b"q", b"q": b"get_peers", b"a": { b"id": node_id, b"info_hash": info_hash } } class DHTCrawler: def __init__(self, info_hash): self.info_hash = info_hash self.node_id = random_node_id() self.routing_table = [] # (ip, port) self.seen_nodes = set() # (ip, port) self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.sock.settimeout(2) def crawl(self, nodes, depth=0): if depth > MAX_RECURSION_DEPTH: return next_nodes = [] for _, ip, port in nodes: if (ip, port) in self.seen_nodes: continue self.seen_nodes.add((ip, port)) try: msg = get_peers_message(self.info_hash, self.node_id) send_krpc_query(self.sock, msg, (ip, port)) data, addr = self.sock.recvfrom(4096) response = bencodepy.decode(data) if b"r" in response: r = response[b"r"] if b"values" in r: print(f"\n💡 Found peers at {ip}:{port}") for v in r[b"values"]: peer_ip = ".".join(str(b) for b in v[:4]) peer_port = int.from_bytes(v[4:], "big") print(f" ➤ Peer: {peer_ip}:{peer_port}") if b"nodes" in r: new_nodes = decode_nodes(r[b"nodes"]) for n in new_nodes: if (n[1], n[2]) not in self.seen_nodes: self.routing_table.append((n[1], n[2])) next_nodes.append(n) except Exception: continue if next_nodes: self.crawl(next_nodes, depth + 1) def bootstrap_and_crawl(self): initial_nodes = [] print(f"🌐 Bootstrapping with public DHT nodes...") for host, port in BOOTSTRAP_NODES: try: msg = get_peers_message(self.info_hash, self.node_id) send_krpc_query(self.sock, msg, (host, port)) data, addr = self.sock.recvfrom(4096) response = bencodepy.decode(data) if b"r" in response and b"nodes" in response[b"r"]: nodes = decode_nodes(response[b"r"][b"nodes"]) print(f"✅ Received {len(nodes)} nodes from {host}") for n in nodes: if (n[1], n[2]) not in self.seen_nodes: self.routing_table.append((n[1], n[2])) initial_nodes.append(n) except Exception as e: print(f"❌ Failed to contact {host}:{port}{e}") print(f"\n🔁 Starting recursive crawl...") self.crawl(initial_nodes) def print_routing_table(self): print("\n📡 Routing Table:") for ip, port in sorted(set(self.routing_table)): print(f" - {ip}:{port}") def main(info_hash): crawler = DHTCrawler(info_hash) crawler.bootstrap_and_crawl() crawler.print_routing_table() if __name__ == "__main__": magnet = "magnet:?xt=urn:btih:PVJBBJYRFEOXDAOW4B2M4XV5K3Z75XLA&dn=debian-12.10.0-amd64-netinst.iso&xl=663748608&tr=http%3A%2F%2Fbttracker.debian.org%3A6969%2Fannounce" info_hash = extract_infohash(magnet) main(info_hash)