#53 Understanding BitTorrent 4: Peer handshake
Introduction
In our last exploration, we delved into the intricacies of communicating with a BitTorrent tracker, breaking down the request components essential for successful peer discovery. Building on that foundation, we now advance to establishing direct communication with peers. In this installment, we'll navigate the crucial step of initiating a handshake with a peer, a gateway to peer-to-peer file sharing in the BitTorrent ecosystem. Mastering the handshake process is pivotal, as it not only validates the connection between peers but also sets the stage for subsequent data exchange, laying the groundwork for efficient file sharing. Ever wondered how your BitTorrent client selects peers for file exchange or how it ensures secure and reliable connections? In this post, we demystify the handshake protocol that makes it all possible. If you're new to our series or need a refresher on the fundamentals of tracker communication, I encourage you to revisit our previous discussion for a comprehensive overview.
Handshake
First of all, we have to define a handshake struct.
Handshake
struct has 5 attributes according to the spec.
length
length of the protocol string (BitTorrent protocol) which is 19 (1 byte)
protocol
the string BitTorrent protocol (19 bytes)
reserved
eight reserved bytes, which are all set to zero (8 bytes)
info_hash
sha1 infohash (20 bytes) (NOT the hexadecimal representation, which is 40 bytes long)
peer_id
peer id (20 bytes) (you can use 00112233445566778899 for this blog)
#[derive(Debug, Clone)]
pub struct Handshake {
pub length: u8,
pub protocol: Vec<u8>,
pub reserved: Vec<u8>,
pub info_hash: Vec<u8>,
pub peer_id: Vec<u8>,
}
To initiate the new Handshake, we are going to create new function.
impl Handshake {
pub fn new(info_hash: &[u8; 20]) -> Self {
Self {
length: 19,
protocol: b"BitTorrent protocol".to_vec(),
reserved: vec![0; 8],
info_hash: info_hash.to_vec(),
peer_id: b"00112233445566778899".to_vec(),
}
}
pub fn bytes(&self) -> Vec<u8> {
let mut bytes = Vec::with_capacity(68);
bytes.push(self.length);
bytes.extend(self.protocol.clone());
bytes.extend(self.reserved.clone());
bytes.extend(self.info_hash.clone());
bytes.extend(self.peer_id.clone());
bytes
}
}
Command
To make life easier, we are going to do step by step.
- Define the handshake command
We are going to take 2 arguments torrent
, peer
. torrent
is the path of torrent file and peer
is address of the peer that we are try to connect and handshake with.
#[derive(Subcommand)]
enum Commands {
Handshake {
torrent: PathBuf,
peer: String,
},
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let args = Args::parse();
match args.command {
Commands::Handshake { torrent, peer } => {}
}
}
- Read the torrent file
Inside handshake comamnd, we going to read the torrent file and deseriaze into actual Torrent
struct to get the information.
let dot_torrent = std::fs::read(torrent).context("read torrent file")?;
let t: Torrent = serde_bencode::from_bytes(&dot_torrent).context("parse torrent file")?;
- Calculate info hash
In order to handshake with the peer, we have to send the info hash.
let info_hash = t.info_hash();
Inside info_hash
function, we convert the info
to bytes, then hash them by Sha1
.
impl Torrent {
pub fn info_hash(&self) -> [u8; 20] {
let info_encoded = serde_bencode::to_bytes(&self.info).expect("re-encode info section");
let mut hasher = Sha1::new();
hasher.update(&info_encoded);
hasher
.finalize()
.try_into()
.expect("GenericArray<[u8; 20]>")
}
}
- Connect with the peer
From the argument of peer info, we try to connect with it. By tokio, connect asynchronously.
let mut res = tokio::net::TcpStream::connect(peer)
.await;
Error Handling in the Handshake Process
When connecting to a peer and performing a handshake, various errors can occur, such as connection failures, I/O errors, or mismatches in the handshake response. Properly handling these errors not only makes your application more reliable but also aids in debugging and maintenance. To interact with error handling, very helpful with anyhow crate.
let mut peer_stream = match res {
Ok(stream) => stream,
Err(e) => {
eprintln!("Failed to connect to peer {}: {}", peer, e);
return Err(anyhow::Error::new(e)); // Convert the error to anyhow::Error if using anyhow for error handling
},
};
- Handshake
// Default handshake creation logic
let handshake = Handshake::new(info_hash);
{
let mut handshake_bytes = handshake.bytes();
stream.write_all(&mut handshake_bytes).await?;
stream.read_exact(&mut handshake_bytes).await?;
}
- Get peer id from the peer
assert_eq!(handshake.length, 19);
assert_eq!(handshake.bittorent_protocol, *b"BitTorrent protocol");
println!("Peer ID: {}", hex::encode(handshake.peer_id));
When we hit the command like below, we would get the peer id.
./build.sh handshake sample.torrent 178.62.82.89:51470
Conclusion
We explored discovering peers and tried to handshake with one of the peer, then we successfully got the peer id.
In the next blog, we will download the piece of file from the peer.
Thank you for reading.
Discussion