From 0ab502cfb6358cc8608243dcd02f937b7ede9b9c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Mar 2026 23:13:02 +0000 Subject: [PATCH] Add UDP tracker support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements UDP tracker protocol (BEP 15): - Supports both HTTP and UDP tracker URLs - Automatically detects tracker type from URI scheme - Implements connect and announce requests for UDP - Binary packet encoding/decoding for UDP protocol - Falls back to HTTP for non-UDP trackers No new dependencies required - uses Lwt_unix for UDP sockets. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/lib/peers.ml | 112 +++++++++++++++++++++++++++++++++++++- src/lib/torrent_client.ml | 8 ++- 2 files changed, 116 insertions(+), 4 deletions(-) diff --git a/src/lib/peers.ml b/src/lib/peers.ml index 1ea6b12..b59d594 100644 --- a/src/lib/peers.ml +++ b/src/lib/peers.ml @@ -29,7 +29,107 @@ let create peers_bin = { ip; port }) ;; -let request_peers uri = +(* UDP Tracker Protocol (BEP 15) *) + +let udp_protocol_id = Int64.of_string "0x41727101980" + +let write_int64_be buf offset value = + let bytes = Bytes.create 8 in + Stdint.Int64.to_bytes_big_endian value bytes 0; + Bytes.blit bytes 0 buf offset 8 +;; + +let write_int32_be buf offset value = + let bytes = Bytes.create 4 in + Stdint.Int32.to_bytes_big_endian value bytes 0; + Bytes.blit bytes 0 buf offset 4 +;; + +let write_int16_be buf offset value = + let bytes = Bytes.create 2 in + Stdint.Uint16.to_bytes_big_endian value bytes 0; + Bytes.blit bytes 0 buf offset 2 +;; + +let read_int64_be buf offset = + Stdint.Int64.of_bytes_big_endian buf offset +;; + +let read_int32_be buf offset = + Stdint.Int32.of_bytes_big_endian buf offset +;; + +let request_peers_udp host port info_hash peer_id client_port left = + let open Lwt.Infix in + let sock = Lwt_unix.socket Unix.PF_INET Unix.SOCK_DGRAM 0 in + + (* Resolve host *) + Lwt_unix.gethostbyname host >>= fun host_entry -> + let addr = host_entry.Unix.h_addr_list.(0) in + let sockaddr = Unix.ADDR_INET (addr, port) in + + (* Connect request *) + let transaction_id = Random.int32 Int32.max_int in + let connect_req = Bytes.create 16 in + write_int64_be connect_req 0 udp_protocol_id; + write_int32_be connect_req 8 (Stdint.Int32.zero); + write_int32_be connect_req 12 transaction_id; + + Lwt_unix.sendto sock connect_req 0 16 [] sockaddr >>= fun _ -> + + (* Connect response *) + let connect_resp = Bytes.create 16 in + Lwt_unix.recvfrom sock connect_resp 0 16 [] >>= fun (len, _) -> + + if len < 16 then + Lwt.fail (Failure "UDP tracker: invalid connect response") + else + let resp_action = read_int32_be connect_resp 0 in + let resp_transaction = read_int32_be connect_resp 4 in + if resp_action <> Stdint.Int32.zero || resp_transaction <> transaction_id then + Lwt.fail (Failure "UDP tracker: connect response mismatch") + else + let connection_id = read_int64_be connect_resp 8 in + + (* Announce request *) + let announce_transaction = Random.int32 Int32.max_int in + let announce_req = Bytes.create 98 in + write_int64_be announce_req 0 connection_id; + write_int32_be announce_req 8 (Stdint.Int32.one); (* action=1 for announce *) + write_int32_be announce_req 12 announce_transaction; + Bytes.blit info_hash 0 announce_req 16 20; + Bytes.blit peer_id 0 announce_req 36 20; + write_int64_be announce_req 56 (Stdint.Int64.zero); (* downloaded *) + write_int64_be announce_req 64 left; (* left *) + write_int64_be announce_req 72 (Stdint.Int64.zero); (* uploaded *) + write_int32_be announce_req 80 (Stdint.Int32.zero); (* event=0 (none) *) + write_int32_be announce_req 84 (Stdint.Int32.zero); (* ip=0 (default) *) + write_int32_be announce_req 88 (Random.int32 Int32.max_int); (* key *) + write_int32_be announce_req 92 (Stdint.Int32.of_int (-1)); (* num_want=-1 *) + write_int16_be announce_req 96 (Stdint.Uint16.of_int client_port); + + Lwt_unix.sendto sock announce_req 0 98 [] sockaddr >>= fun _ -> + + (* Announce response *) + let announce_resp = Bytes.create 65536 in + Lwt_unix.recvfrom sock announce_resp 0 65536 [] >>= fun (len, _) -> + + Lwt_unix.close sock >>= fun () -> + + if len < 20 then + Lwt.fail (Failure "UDP tracker: invalid announce response") + else + let resp_action = read_int32_be announce_resp 0 in + let resp_transaction = read_int32_be announce_resp 4 in + if resp_action <> Stdint.Int32.one || resp_transaction <> announce_transaction then + Lwt.fail (Failure "UDP tracker: announce response mismatch") + else + let peers_data = Bytes.sub announce_resp 20 (len - 20) in + let peers = create peers_data in + Lwt.return peers +;; + +let request_peers_http uri = let open Lwt.Infix in Cohttp_lwt_unix.Client.get uri >>= fun (resp, body) -> Cohttp_lwt.Body.to_string body >>= fun body_string -> @@ -42,4 +142,14 @@ let request_peers uri = Lwt.return peers ;; +let request_peers uri info_hash peer_id client_port left = + let scheme = Uri.scheme uri in + match scheme with + | Some "udp" -> + let host = Uri.host uri |> Option.get in + let port = Uri.port uri |> Option.value ~default:80 in + request_peers_udp host port info_hash peer_id client_port left + | _ -> request_peers_http uri +;; + let to_string peer = Printf.sprintf "%s:%d" (V4.to_string peer.ip) peer.port diff --git a/src/lib/torrent_client.ml b/src/lib/torrent_client.ml index e538a84..f89f203 100644 --- a/src/lib/torrent_client.ml +++ b/src/lib/torrent_client.ml @@ -418,11 +418,13 @@ let download_file output_file torrent_file = in (* Get Peers *) let random_peer = Bytes.create 20 in - let uri = build_tracker_url torrent_file random_peer 6881 in - Peers.request_peers uri >>= fun peers -> + let client_port = 6881 in + let uri = build_tracker_url torrent_file random_peer client_port in + let total_len = total_length torrent_file |> Int64.to_int in + let left = Stdint.Int64.of_int total_len in + Peers.request_peers uri torrent_file.info_hash random_peer client_port left >>= fun peers -> Logs.debug (fun m -> m "Got %d peers\n" (List.length peers)); (* Download *) - let total_len = total_length torrent_file |> Int64.to_int in let pieces_count = Array.length torrent_file.piece_hashes in (* Load resume state if exists *) -- 2.43.0