Skip to content

Instantly share code, notes, and snippets.

@liamzebedee
Created June 27, 2024 12:49
Show Gist options
  • Select an option

  • Save liamzebedee/4145b51f7cba2f9c24e516d8a778634c to your computer and use it in GitHub Desktop.

Select an option

Save liamzebedee/4145b51f7cba2f9c24e516d8a778634c to your computer and use it in GitHub Desktop.

Revisions

  1. liamzebedee created this gist Jun 27, 2024.
    291 changes: 291 additions & 0 deletions bittorrent_udp_trackers.go
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,291 @@
    // https://wiki.theory.org/BitTorrent_Tracker_Protocol
    // https://www.bittorrent.org/beps/bep_0015.html
    package core

    import (
    "bytes"
    "encoding/binary"
    "fmt"
    "math/rand"
    "net"
    "time"
    )

    const (
    protocolID = 0x41727101980
    actionConnect = 0
    actionAnnounce = 1
    actionScrape = 2
    )

    type ConnectRequest struct {
    ProtocolID int64
    Action int32
    TransactionID int32
    }

    type ConnectResponse struct {
    Action int32
    TransactionID int32
    ConnectionID int64
    }

    type AnnounceRequest struct {
    ConnectionID int64
    Action int32
    TransactionID int32
    InfoHash [20]byte
    PeerID [20]byte
    Downloaded int64
    Left int64
    Uploaded int64
    Event int32
    IPAddress int32
    Key int32
    NumWant int32
    Port int16
    }

    type AnnounceResponse struct {
    Action int32
    TransactionID int32
    Interval int32
    Leechers int32
    Seeders int32
    IPs []Peer
    }

    type ScrapeRequest struct {
    ConnectionID int64
    Action int32
    TransactionID int32
    InfoHashes [][20]byte
    }

    type ScrapeResponse struct {
    Action int32
    TransactionID int32
    Torrents []TorrentStats
    }

    type TorrentStats struct {
    Seeders int32
    Completed int32
    Leechers int32
    }

    type Peer struct {
    IP net.IP
    Port uint16
    }

    func RunTrackerDemo(infohash [20]byte, peerId [20]byte) {
    trackerAddr := "tracker.opentrackr.org:1337"
    conn, err := net.Dial("udp", trackerAddr)
    if err != nil {
    fmt.Println("Error connecting to tracker:", err)
    return
    }
    defer conn.Close()

    // Step 1: Connect
    transactionID := rand.Int31()
    connectReq := ConnectRequest{
    ProtocolID: protocolID,
    Action: actionConnect,
    TransactionID: transactionID,
    }
    if err := sendConnectRequest(conn, connectReq); err != nil {
    fmt.Println("Error sending connect request:", err)
    return
    }

    connectResp, err := receiveConnectResponse(conn, transactionID)
    if err != nil {
    fmt.Println("Error receiving connect response:", err)
    return
    }

    // Step 2: Announce
    announceReq := AnnounceRequest{
    ConnectionID: connectResp.ConnectionID,
    Action: actionAnnounce,
    TransactionID: rand.Int31(),
    InfoHash: infohash,
    PeerID: peerId,
    Downloaded: 0,
    Left: 100,
    Uploaded: 0,
    Event: 0,
    IPAddress: 0,
    Key: rand.Int31(),
    NumWant: -1,
    Port: 6881,
    }
    if err := sendAnnounceRequest(conn, announceReq); err != nil {
    fmt.Println("Error sending announce request:", err)
    return
    }

    announceResp, err := receiveAnnounceResponse(conn, announceReq.TransactionID)
    if err != nil {
    fmt.Println("Error receiving announce response:", err)
    return
    }

    fmt.Printf("Received Announce Response: %+v\n", announceResp)
    }

    func sendConnectRequest(conn net.Conn, req ConnectRequest) error {
    buf := new(bytes.Buffer)
    if err := binary.Write(buf, binary.BigEndian, req); err != nil {
    return err
    }
    _, err := conn.Write(buf.Bytes())
    return err
    }

    func receiveConnectResponse(conn net.Conn, transactionID int32) (*ConnectResponse, error) {
    buf := make([]byte, 16)
    conn.SetReadDeadline(time.Now().Add(15 * time.Second))
    if _, err := conn.Read(buf); err != nil {
    return nil, err
    }

    var resp ConnectResponse
    if err := binary.Read(bytes.NewReader(buf), binary.BigEndian, &resp); err != nil {
    return nil, err
    }

    if resp.TransactionID != transactionID {
    return nil, fmt.Errorf("transaction ID mismatch")
    }

    return &resp, nil
    }

    func sendAnnounceRequest(conn net.Conn, req AnnounceRequest) error {
    buf := new(bytes.Buffer)
    if err := binary.Write(buf, binary.BigEndian, req); err != nil {
    return err
    }
    _, err := conn.Write(buf.Bytes())
    return err
    }

    func receiveAnnounceResponse(conn net.Conn, transactionID int32) (*AnnounceResponse, error) {
    buf := make([]byte, 1024) // Adjust buffer size as needed
    conn.SetReadDeadline(time.Now().Add(15 * time.Second))
    n, err := conn.Read(buf)
    if err != nil {
    return nil, err
    }

    if n < 20 {
    return nil, fmt.Errorf("announce response too short")
    }

    var action, txnID, interval, leechers, seeders int32
    r := bytes.NewReader(buf[:20])
    if err := binary.Read(r, binary.BigEndian, &action); err != nil {
    return nil, err
    }
    if err := binary.Read(r, binary.BigEndian, &txnID); err != nil {
    return nil, err
    }
    if err := binary.Read(r, binary.BigEndian, &interval); err != nil {
    return nil, err
    }
    if err := binary.Read(r, binary.BigEndian, &leechers); err != nil {
    return nil, err
    }
    if err := binary.Read(r, binary.BigEndian, &seeders); err != nil {
    return nil, err
    }

    if txnID != transactionID {
    return nil, fmt.Errorf("transaction ID mismatch")
    }

    numPeers := (n - 20) / 6
    peers := make([]Peer, numPeers)
    for i := 0; i < numPeers; i++ {
    ip := net.IPv4(buf[20+6*i], buf[21+6*i], buf[22+6*i], buf[23+6*i])
    port := binary.BigEndian.Uint16(buf[24+6*i : 26+6*i])
    peers[i] = Peer{IP: ip, Port: port}
    }

    return &AnnounceResponse{
    Action: action,
    TransactionID: txnID,
    Interval: interval,
    Leechers: leechers,
    Seeders: seeders,
    IPs: peers,
    }, nil
    }

    func sendScrapeRequest(conn net.Conn, req ScrapeRequest) error {
    buf := new(bytes.Buffer)
    if err := binary.Write(buf, binary.BigEndian, req.ConnectionID); err != nil {
    return err
    }
    if err := binary.Write(buf, binary.BigEndian, req.Action); err != nil {
    return err
    }
    if err := binary.Write(buf, binary.BigEndian, req.TransactionID); err != nil {
    return err
    }
    for _, hash := range req.InfoHashes {
    if err := binary.Write(buf, binary.BigEndian, hash); err != nil {
    return err
    }
    }
    _, err := conn.Write(buf.Bytes())
    return err
    }

    func receiveScrapeResponse(conn net.Conn, transactionID int32) (*ScrapeResponse, error) {
    buf := make([]byte, 1024) // Adjust buffer size as needed
    conn.SetReadDeadline(time.Now().Add(15 * time.Second))
    n, err := conn.Read(buf)
    if err != nil {
    return nil, err
    }

    if n < 8 {
    return nil, fmt.Errorf("scrape response too short")
    }

    var action, txnID int32
    r := bytes.NewReader(buf[:8])
    if err := binary.Read(r, binary.BigEndian, &action); err != nil {
    return nil, err
    }
    if err := binary.Read(r, binary.BigEndian, &txnID); err != nil {
    return nil, err
    }

    if txnID != transactionID {
    return nil, fmt.Errorf("transaction ID mismatch")
    }

    numTorrents := (n - 8) / 12
    torrents := make([]TorrentStats, numTorrents)
    for i := 0; i < numTorrents; i++ {
    offset := 8 + i*12
    torrents[i] = TorrentStats{
    Seeders: int32(binary.BigEndian.Uint32(buf[offset:])),
    Completed: int32(binary.BigEndian.Uint32(buf[offset+4:])),
    Leechers: int32(binary.BigEndian.Uint32(buf[offset+8:])),
    }
    }

    return &ScrapeResponse{
    Action: action,
    TransactionID: txnID,
    Torrents: torrents,
    }, nil
    }