🧑‍🎓

#53 Understanding BitTorrent 4: Peer handshake

2024/09/13に公開

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.

  1. 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 } => {}
  }
}

  1. 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")?;
  1. 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]>")
  }
}
  1. 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
    },
};
  1. 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?;
}
  1. 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.

Resources

Discussion