From b32f00499a24b23772af35f19ae064c7a09e2632 Mon Sep 17 00:00:00 2001 From: Adam Macdonald Date: Sun, 3 Aug 2025 20:10:54 +0100 Subject: [PATCH] initial commit --- .gitignore | 2 + Cargo.lock | 558 ++++++++++++++++++++++++++++ Cargo.toml | 12 + README.md | 3 + src/constants.rs | 12 + src/main.rs | 89 +++++ src/tracker/bittorrent/info_hash.rs | 22 ++ src/tracker/bittorrent/mod.rs | 4 + src/tracker/bittorrent/peer.rs | 20 + src/tracker/bittorrent/protocol.rs | 62 ++++ src/tracker/bittorrent/udp.rs | 213 +++++++++++ src/tracker/mod.rs | 5 + src/tracker/tracker.rs | 427 +++++++++++++++++++++ src/tracker/udp_server.rs | 257 +++++++++++++ 14 files changed, 1686 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 src/constants.rs create mode 100644 src/main.rs create mode 100644 src/tracker/bittorrent/info_hash.rs create mode 100644 src/tracker/bittorrent/mod.rs create mode 100644 src/tracker/bittorrent/peer.rs create mode 100644 src/tracker/bittorrent/protocol.rs create mode 100644 src/tracker/bittorrent/udp.rs create mode 100644 src/tracker/mod.rs create mode 100644 src/tracker/tracker.rs create mode 100644 src/tracker/udp_server.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bcbfeee --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.vscode/ +target/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..a0c5737 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,558 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "spin", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "meteoro" +version = "0.1.0" +dependencies = [ + "anyhow", + "flume", + "heapless", + "log", + "rand", + "simplelog", +] + +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "simplelog" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16257adbfaef1ee58b1363bdc0664c9b8e1e30aed86049635fb5f147d065a9c0" +dependencies = [ + "log", + "termcolor", + "time", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "syn" +version = "2.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "zerocopy" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..b45b3d8 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "meteoro" +version = "0.1.0" +edition = "2024" + +[dependencies] +anyhow = "1.0.98" +flume = { version = "0.11.1", features = ["eventual-fairness", "select"] } +heapless = "0.8.0" +log = { version = "0.4.27", features = ["release_max_level_info"] } +rand = "0.9.2" +simplelog = "0.12.2" diff --git a/README.md b/README.md new file mode 100644 index 0000000..7dfb2ea --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# meteoro + +A UDP BitTorrent tracker written in Rust diff --git a/src/constants.rs b/src/constants.rs new file mode 100644 index 0000000..8173ad5 --- /dev/null +++ b/src/constants.rs @@ -0,0 +1,12 @@ +pub const METEORO_HELP_TEXT: &'static str = "Usage:\tmeteoro [OPTIONS] + +OPTIONS: + +-a IPV4_ADDRESS Provide an IPv4 address to bind to + (example: -a 192.168.1.2:6969) + +-A IPV6_ADDRESS Provide an IPv6 address to bind to + (example: -A [::1]:6970) + +-h | --help Show this help text +-v | --version Print the program's build & version information"; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..0db3914 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,89 @@ +use std::{ + env, + net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}, + process::ExitCode, +}; + +use log::LevelFilter; +use simplelog::{ColorChoice, ConfigBuilder, TermLogger, TerminalMode}; + +mod constants; +mod tracker; +use tracker::Tracker; + +fn main() -> ExitCode { + #[cfg(debug_assertions)] + let log_level: LevelFilter = LevelFilter::Trace; + #[cfg(not(debug_assertions))] + let log_level: LevelFilter = LevelFilter::Info; + + let logger_config = ConfigBuilder::new() + .set_target_level(LevelFilter::Off) + .set_location_level(LevelFilter::Off) + .set_thread_level(LevelFilter::Off) + .set_time_level(LevelFilter::Info) + .build(); + + TermLogger::init( + log_level, + logger_config, + TerminalMode::Mixed, + ColorChoice::Auto, + ) + .expect("failed to initialise terminal logging"); + + let args: Vec = env::args().collect(); + + if args.contains(&String::from("--help")) || args.contains(&String::from("-h")) { + println!("{}", constants::METEORO_HELP_TEXT); + return ExitCode::SUCCESS; + } + + if args.contains(&String::from("--version")) || args.contains(&String::from("-v")) { + println!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")); + return ExitCode::SUCCESS; + } + + let ipv4_flag_idxs: Vec = args + .iter() + .enumerate() + .filter_map(|(idx, s)| if s == "-a" { Some(idx) } else { None }) + .collect(); + let ipv6_flag_idxs: Vec = args + .iter() + .enumerate() + .filter_map(|(idx, s)| if s == "-A" { Some(idx) } else { None }) + .collect(); + let mut ip_addrs: Vec = ipv4_flag_idxs + .iter() + .filter_map(|idx| { + Some(SocketAddr::V4( + args.get(*idx + 1)?.parse::().ok()?, + )) + }) + .collect(); + ip_addrs.extend(ipv6_flag_idxs.iter().filter_map(|idx| { + Some(SocketAddr::V6( + args.get(*idx + 1)?.parse::().ok()?, + )) + })); + + if ip_addrs.is_empty() { + ip_addrs.push(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 6969))); + ip_addrs.push(SocketAddr::V6(SocketAddrV6::new( + Ipv6Addr::LOCALHOST, + 6969, + 0, + 0, + ))); + } + + let mut tracker = Tracker::new(); + return match tracker.start_on(&ip_addrs) { + Ok(_) => ExitCode::SUCCESS, + Err(e) => { + log::error!("Fatal error: {}", e); + ExitCode::FAILURE + } + }; +} diff --git a/src/tracker/bittorrent/info_hash.rs b/src/tracker/bittorrent/info_hash.rs new file mode 100644 index 0000000..e0b9918 --- /dev/null +++ b/src/tracker/bittorrent/info_hash.rs @@ -0,0 +1,22 @@ +use std::fmt; +use std::fmt::Write; + +pub const INFO_HASH_SIZE: usize = 20; + +#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)] +pub struct InfoHash { + pub bytes: [u8; INFO_HASH_SIZE], +} + +impl fmt::Display for InfoHash { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + const INFO_HASH_STR_LEN: usize = INFO_HASH_SIZE * 2; + let mut hex_string: heapless::String = heapless::String::new(); + + for b in self.bytes { + let _ = write!(hex_string, "{:02x}", b); + } + + write!(f, "{}", hex_string) + } +} diff --git a/src/tracker/bittorrent/mod.rs b/src/tracker/bittorrent/mod.rs new file mode 100644 index 0000000..6651b4a --- /dev/null +++ b/src/tracker/bittorrent/mod.rs @@ -0,0 +1,4 @@ +pub mod info_hash; +pub mod peer; +pub mod protocol; +pub mod udp; diff --git a/src/tracker/bittorrent/peer.rs b/src/tracker/bittorrent/peer.rs new file mode 100644 index 0000000..a86ca0e --- /dev/null +++ b/src/tracker/bittorrent/peer.rs @@ -0,0 +1,20 @@ +use std::fmt; + +const MAX_PEER_ID_LENGTH: usize = 20; + +#[derive(Debug)] +pub struct PeerId { + pub bytes: [u8; MAX_PEER_ID_LENGTH], +} + +impl PeerId { + pub fn to_string_lossy(&self) -> String { + String::from_utf8_lossy(&self.bytes).to_string() + } +} + +impl fmt::Display for PeerId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.to_string_lossy()) + } +} diff --git a/src/tracker/bittorrent/protocol.rs b/src/tracker/bittorrent/protocol.rs new file mode 100644 index 0000000..9258535 --- /dev/null +++ b/src/tracker/bittorrent/protocol.rs @@ -0,0 +1,62 @@ +/// BitTorrent's UDP magic `connection_id` constant +pub const UDP_MAGIC: i64 = 0x41727101980; + +#[repr(i32)] +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum Action { + Connect = 0, + Announce = 1, + Scrape = 2, + Error = 3, +} + +impl Action { + pub fn from_i32(val: i32) -> Option { + match val { + 0 => Some(Action::Connect), + 1 => Some(Action::Announce), + 2 => Some(Action::Scrape), + 3 => Some(Action::Error), + _ => None, + } + } + + pub fn to_i32(&self) -> i32 { + match self { + Action::Connect => 0, + Action::Announce => 1, + Action::Scrape => 2, + Action::Error => 3, + } + } +} + +#[repr(i32)] +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum Event { + None = 0, + Completed = 1, + Started = 2, + Stopped = 3, +} + +impl Event { + pub fn from_i32(val: i32) -> Option { + match val { + 0 => Some(Event::None), + 1 => Some(Event::Completed), + 2 => Some(Event::Started), + 3 => Some(Event::Stopped), + _ => None, + } + } + + pub fn to_i32(&self) -> i32 { + match self { + Event::None => 0, + Event::Completed => 1, + Event::Started => 2, + Event::Stopped => 3, + } + } +} diff --git a/src/tracker/bittorrent/udp.rs b/src/tracker/bittorrent/udp.rs new file mode 100644 index 0000000..7186ba2 --- /dev/null +++ b/src/tracker/bittorrent/udp.rs @@ -0,0 +1,213 @@ +use std::{ + fmt, + io::{Cursor, Write}, + net::{Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6}, +}; + +use crate::tracker::bittorrent::{ + info_hash::InfoHash, + peer::PeerId, + protocol::{Action, Event}, +}; + +pub const MIN_ANNOUNCE_REQUEST_SIZE: usize = 98; +pub const MIN_SCRAPE_REQUEST_SIZE: usize = 36; +pub const MIN_CONNECTION_RESPONSE_SIZE: usize = 16; +pub const MIN_ANNOUNCE_RESPONSE_SIZE: usize = 20; +pub const MIN_SCRAPE_RESPONSE_SIZE: usize = 8; +pub const SCRAPE_RESPONSE_ENTRY_SIZE: usize = 12; + +pub const IPV4_SIZE: usize = Ipv4Addr::BITS as usize / 8; +pub const IPV6_SIZE: usize = Ipv6Addr::BITS as usize / 8; +pub const PORT_SIZE: usize = size_of::(); +pub const IPV4_ADDR_PAIR_SIZE: usize = IPV4_SIZE + PORT_SIZE; +pub const IPV6_ADDR_PAIR_SIZE: usize = IPV6_SIZE + PORT_SIZE; + +#[derive(Debug)] +pub struct ConnectRequest { + pub protocol_id: i64, + pub transaction_id: i32, +} + +#[derive(Debug)] +pub struct ConnectResponse { + pub transaction_id: i32, + pub connection_id: i64, +} + +impl ConnectResponse { + pub fn write_to_buf(&self, buf: &mut [u8]) -> usize { + let mut c = Cursor::new(buf); + let mut written: usize = 0; + + const ACTION: i32 = Action::Connect as i32; + + written += c.write(&ACTION.to_be_bytes()).unwrap(); + written += c.write(&self.transaction_id.to_be_bytes()).unwrap(); + written += c.write(&self.connection_id.to_be_bytes()).unwrap(); + + return written; + } +} + +#[derive(Debug)] +pub struct AnnounceRequest { + pub connection_id: i64, + pub transaction_id: i32, + /// Info hash of the torrent + pub info_hash: InfoHash, + /// Peer's ID + pub peer_id: PeerId, + /// Number of bytes peer has downloaded this session + pub downloaded: i64, + /// Number of bytes peer has remaining to complete the download + pub left: i64, + /// Number of bytes peer has uploaded this session + pub uploaded: i64, + /// One of 4 announce events. `Event::None` is used to update presence but not change peer's state. + pub event: Event, + /// Peer's desired IPv4 address to be sent the announce response on. `None` implies that the packet's source address should be used. + pub ipv4_address: Option, + /// Client's unique key for tracking a single client over multiple connections (sockets) + pub key: u32, + /// Maximum number of info hashes the client is requesting. `None` implies a tracker-defined default value should be used. + pub num_want: Option, + /// Peer's desired port to be the announce response on. If `ipv4_address` is `Some` then this should be used. + pub port: u16, +} + +pub enum AnnounceResponse { + V4(AnnounceResponseV4), + V6(AnnounceResponseV6), +} + +#[derive(Debug)] +pub struct AnnounceResponseCommon { + pub transaction_id: i32, + /// Number of seconds peer should wait before re-announcing + pub interval: i32, + /// Number of peers still downloading + pub leechers: i32, + /// Number of peers who have completed the download and are seeding + pub seeders: i32, +} + +#[derive(Debug)] +pub struct AnnounceResponseV4 { + pub inner: AnnounceResponseCommon, + pub peers: Vec, +} + +#[derive(Debug)] +pub struct AnnounceResponseV6 { + pub inner: AnnounceResponseCommon, + pub peers: Vec, +} + +impl AnnounceResponseCommon { + pub fn write_to_buf(&self, buf: &mut [u8]) -> usize { + const ACTION: i32 = Action::Announce as i32; + buf[0..4].copy_from_slice(&ACTION.to_be_bytes()); + buf[4..8].copy_from_slice(&self.transaction_id.to_be_bytes()); + buf[8..12].copy_from_slice(&self.interval.to_be_bytes()); + buf[12..16].copy_from_slice(&self.leechers.to_be_bytes()); + buf[16..20].copy_from_slice(&self.seeders.to_be_bytes()); + + return MIN_ANNOUNCE_RESPONSE_SIZE; + } +} + +impl AnnounceResponseV4 { + pub fn write_to_buf(&self, buf: &mut [u8]) -> usize { + let written = self.inner.write_to_buf(buf); + + for (offset, addr) in (written..buf.len()) + .step_by(IPV4_ADDR_PAIR_SIZE) + .zip(&self.peers) + { + buf[offset..offset + IPV4_SIZE].copy_from_slice(&addr.ip().octets()); + buf[offset + IPV4_SIZE..offset + IPV4_ADDR_PAIR_SIZE] + .copy_from_slice(&addr.port().to_be_bytes()); + } + + return written + self.peers.len() * IPV4_ADDR_PAIR_SIZE; + } +} + +impl AnnounceResponseV6 { + pub fn write_to_buf(&self, buf: &mut [u8]) -> usize { + let written = self.inner.write_to_buf(buf); + + for (offset, addr) in (written..buf.len()) + .step_by(IPV6_ADDR_PAIR_SIZE) + .zip(&self.peers) + { + buf[offset..offset + IPV6_SIZE].copy_from_slice(&addr.ip().octets()); + buf[offset + IPV6_SIZE..offset + IPV6_ADDR_PAIR_SIZE] + .copy_from_slice(&addr.port().to_be_bytes()); + } + + return written + self.peers.len() * IPV6_ADDR_PAIR_SIZE; + } +} + +#[derive(Debug)] +pub struct ScrapeRequest { + pub connection_id: i64, + pub transaction_id: i32, + pub info_hashes: Vec, +} + +#[derive(Debug, Default)] +pub struct ScrapeStats { + /// Number of connected peers who have completed the download and are seeding + pub seeders: i32, + /// Number of peers who have ever completed the download + pub completed: i32, + /// Number of connected peers who have not completed the download and are seeding + pub leechers: i32, +} + +#[derive(Debug)] +pub struct ScrapeResponse { + pub transaction_id: i32, + pub stats: Vec, +} + +impl ScrapeResponse { + pub fn write_to_buf(&self, buf: &mut [u8]) -> usize { + let mut c = Cursor::new(buf); + let mut written: usize = 0; + + const ACTION: i32 = Action::Scrape as i32; + written += c.write(&ACTION.to_be_bytes()).unwrap(); + written += c.write(&self.transaction_id.to_be_bytes()).unwrap(); + + for s in &self.stats { + written += c.write(&s.seeders.to_be_bytes()).unwrap(); + written += c.write(&s.completed.to_be_bytes()).unwrap(); + written += c.write(&s.leechers.to_be_bytes()).unwrap(); + } + + return written; + } +} + +#[derive(Debug)] +pub struct ErrorResponse { + pub transaction_id: i32, + pub message: String, +} + +pub enum UdpRequest { + Connect(ConnectRequest), + Announce(AnnounceRequest), + Scrape(ScrapeRequest), +} + +pub enum UdpResponse { + Connect(ConnectResponse), + Announce(AnnounceResponse), + Scrape(ScrapeResponse), + Error(ErrorResponse), +} diff --git a/src/tracker/mod.rs b/src/tracker/mod.rs new file mode 100644 index 0000000..5cc4e4e --- /dev/null +++ b/src/tracker/mod.rs @@ -0,0 +1,5 @@ +mod bittorrent; +mod tracker; +mod udp_server; + +pub use tracker::Tracker; diff --git a/src/tracker/tracker.rs b/src/tracker/tracker.rs new file mode 100644 index 0000000..5eb403f --- /dev/null +++ b/src/tracker/tracker.rs @@ -0,0 +1,427 @@ +use std::collections::HashMap; +use std::net::{SocketAddr, SocketAddrV4, SocketAddrV6}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, RwLock}; +use std::thread; +use std::time::{Duration, Instant}; + +use anyhow::Result; +use flume::Sender; +use rand::Rng; + +use crate::tracker::bittorrent::info_hash::InfoHash; +use crate::tracker::bittorrent::protocol::Event; +use crate::tracker::bittorrent::udp::{ + AnnounceResponse, AnnounceResponseCommon, AnnounceResponseV4, AnnounceResponseV6, + ConnectResponse, ScrapeResponse, ScrapeStats, UdpRequest, UdpResponse, +}; +use crate::tracker::udp_server::UdpServer; + +pub struct RequestMessage { + pub(crate) request: UdpRequest, + pub(crate) src_addr: SocketAddr, + pub(crate) server_id: u32, +} + +pub struct ResponseMessage { + pub(crate) response: UdpResponse, + pub(crate) dst_addr: SocketAddr, +} + +struct PeerMetadata { + socket_addr: SocketAddr, + last_active: Instant, +} + +struct PeerStatus { + connection_id: i64, + last_event: Event, + last_active: Instant, +} + +pub const GARBAGE_COLLECTION_INTERVAL: Duration = Duration::from_secs(20); + +pub const CONNECTION_EXPIRE_TIME: Duration = Duration::from_secs(90); +pub const DEFAULT_ANNOUNCE_INTERVAL: Duration = Duration::from_secs(60); +pub const DEFAULT_ANNOUNCE_WANT: u32 = 80; + +type ConnectionIdMap = HashMap; +type InfoHashMap = HashMap>; + +pub static SHOULD_STOP_SIGNAL: AtomicBool = AtomicBool::new(false); + +pub struct Tracker { + server_threads: Vec>, + response_channels: Vec>, + /// Map of IPs & ports to peer connection IDs + peers: Arc>, + info_hashes: Arc>, +} + +impl Tracker { + pub fn new() -> Self { + Tracker { + server_threads: Vec::new(), + response_channels: Vec::new(), + peers: Arc::new(RwLock::new(HashMap::new())), + info_hashes: Arc::new(RwLock::new(HashMap::new())), + } + } + + pub fn start_on(&mut self, addrs: &[SocketAddr]) -> Result<()> { + if addrs.is_empty() { + log::error!("No addresses provided for tracker to listen on"); + } + + // Only one request channel set is necessary as all the server threads send on the same one and the tracker is the only reader + let (req_send, req_recv) = flume::unbounded::(); + + let mut server_id: u32 = 0; + + for a in addrs { + // Each UDP server gets its own response channels + let (resp_send, resp_recv) = flume::unbounded::(); + let req_chan = req_send.clone(); + let a = a.clone(); + + self.server_threads.push( + thread::Builder::new() + .name(format!( + "{} server thread (ID {})", + env!("CARGO_PKG_NAME"), + server_id + )) + .spawn(move || { + let server = UdpServer::new(a, server_id); + loop { + match server.start(req_chan.clone(), resp_recv.clone()) { + Err(e) => { + log::error!("Failed to start server {}: {}", server_id, e) + } + _ => (), + }; + } + })?, + ); + + self.response_channels.push(resp_send); + + server_id += 1; + } + + let gc_peers = self.peers.clone(); + let gc_info_hashes = self.info_hashes.clone(); + let _gc_thread = thread::spawn(move || { + let peers = gc_peers; + let info_hashes = gc_info_hashes; + + loop { + thread::sleep(GARBAGE_COLLECTION_INTERVAL); + + let start = Instant::now(); + + let expired = Tracker::purge_expired_connections(&mut peers.write().unwrap()); + Tracker::purge_expired_swarm_peers(&mut info_hashes.write().unwrap(), &expired); + + log::trace!("Garbage collection took {} ns", start.elapsed().as_nanos()) + } + }); + + let mut rng = rand::rng(); + + loop { + let request = match req_recv.recv() { + Ok(r) => r, + _ => { + log::error!("Internal tracker error: server request channel closed"); + break; + } + }; + + match request.request { + UdpRequest::Connect(connect) => { + log::trace!( + "(Server {}) Connect request: {:?}", + request.server_id, + connect + ); + + let new_id: i64 = rng.random(); + + let mut peers = self.peers.write().unwrap(); + + peers.insert( + new_id, + PeerMetadata { + socket_addr: request.src_addr, + last_active: Instant::now(), + }, + ); + + match self.response_channels[request.server_id as usize].send(ResponseMessage { + response: UdpResponse::Connect(ConnectResponse { + transaction_id: connect.transaction_id, + connection_id: new_id, + }), + dst_addr: request.src_addr, + }) { + Ok(r) => r, + _ => { + log::error!("Internal tracker error: server response channel closed"); + break; + } + }; + } + + UdpRequest::Announce(announce) => { + log::trace!( + "(Server {}) Announce request: {:?}", + request.server_id, + announce + ); + + if !self.connection_valid(announce.connection_id, request.src_addr) { + continue; + } + + // Ensure we honour the desired number of peers, within our bounaries + + let n_announce_want: u32 = if let Some(n) = announce.num_want { + if n < DEFAULT_ANNOUNCE_WANT { + n + } else { + DEFAULT_ANNOUNCE_WANT + } + } else { + DEFAULT_ANNOUNCE_WANT + }; + + let mut n_announce_entries: u32 = 0; + let mut n_leechers: i32 = 0; + let mut n_seeders: i32 = 0; + let mut v4_peers: Vec = Vec::new(); + let mut v6_peers: Vec = Vec::new(); + + let mut info_hashes = self.info_hashes.write().unwrap(); + + match info_hashes.get_mut(&announce.info_hash) { + Some(swarm) => { + if announce.event == Event::Stopped { + swarm.retain(|status| { + status.connection_id != announce.connection_id + }); + } else { + let peers = self.peers.read().unwrap(); + + for peer_status in swarm { + if peer_status.last_event == Event::Stopped { + // Skip any peers who have stopped their download + continue; + } + + // State update for ourselves + + if peer_status.connection_id == announce.connection_id { + if peer_status.last_event != announce.event + && announce.event != Event::None + { + // Update last event if the event has changed and is not just a `None` for notifying presence + peer_status.last_event = announce.event + } else { + // Peer status has not changed + peer_status.last_active = Instant::now(); + } + + continue; // don't give the peer their own connection ID's address + } + + if n_announce_entries >= n_announce_want { + break; + } + + // Collect and add the relevant peer's address to the appropriate vector + + if let Some(a) = peers.get(&peer_status.connection_id) { + if a.socket_addr == request.src_addr { + continue; // don't give the peer an expired ID with the same address + } + + match &a.socket_addr { + SocketAddr::V4(v4) => { + if request.src_addr.is_ipv4() { + v4_peers.push(*v4); + } + } + SocketAddr::V6(v6) => { + if request.src_addr.is_ipv6() { + v6_peers.push(*v6); + } + } + }; + + // Keep track of the seeders, leechers & announce entries gathered + + if peer_status.last_event == Event::Completed { + n_seeders += 1; + } else { + n_leechers += 1; + } + + n_announce_entries += 1; + } + } + } + } + None => { + // Info hash isn't currently tracked + // No relevant peers in the swarm + + info_hashes.insert( + announce.info_hash, + vec![PeerStatus { + connection_id: announce.connection_id, + last_event: announce.event, + last_active: Instant::now(), + }], + ); + } + }; + + let resp_common = AnnounceResponseCommon { + transaction_id: announce.transaction_id, + interval: DEFAULT_ANNOUNCE_INTERVAL.as_secs() as i32, + leechers: n_leechers, + seeders: n_seeders, + }; + let response = if request.src_addr.is_ipv4() { + // IPv4 + AnnounceResponse::V4(AnnounceResponseV4 { + inner: resp_common, + peers: v4_peers, + }) + } else { + // IPv6 + AnnounceResponse::V6(AnnounceResponseV6 { + inner: resp_common, + peers: v6_peers, + }) + }; + + match self.response_channels[request.server_id as usize].send(ResponseMessage { + response: UdpResponse::Announce(response), + dst_addr: request.src_addr, + }) { + Ok(r) => r, + _ => { + log::error!("Internal tracker error: server response channel closed"); + break; + } + }; + } + + UdpRequest::Scrape(scrape) => { + log::trace!( + "(Server {}) Scrape request: {:?}", + request.server_id, + scrape + ); + + if !self.connection_valid(scrape.connection_id, request.src_addr) { + continue; + } + + let info_hashes = self.info_hashes.read().unwrap(); + + let mut scrape_stats: Vec = Vec::new(); + for info_hash in &scrape.info_hashes { + match info_hashes.get(info_hash) { + Some(peers) => { + scrape_stats.push(ScrapeStats { + seeders: peers + .iter() + .filter(|&peer_status| { + peer_status.last_event == Event::Completed + }) + .count() + as i32, + completed: 0, // TODO: keep track of this + leechers: peers + .iter() + .filter(|&peer_status| { + peer_status.last_event == Event::Started + }) + .count() + as i32, + }); + } + None => scrape_stats.push(ScrapeStats::default()), + }; + } + + match self.response_channels[request.server_id as usize].send(ResponseMessage { + response: UdpResponse::Scrape(ScrapeResponse { + transaction_id: scrape.transaction_id, + stats: scrape_stats, + }), + dst_addr: request.src_addr, + }) { + Ok(r) => r, + _ => { + log::error!("Internal tracker error: server response channel closed"); + break; + } + }; + } + } + } + + self.server_threads.drain(..).for_each(|t| { + let _ = t.join(); + }); + + return Ok(()); + } + + fn connection_valid(&self, connection_id: i64, src_addr: SocketAddr) -> bool { + let peers = self.peers.read().unwrap(); + + return match peers.get(&connection_id) { + Some(metadata) => metadata.socket_addr == src_addr, + None => false, + }; + } + + fn purge_expired_connections(peers: &mut ConnectionIdMap) -> Vec { + let mut purged: Vec = Vec::new(); + + for (id, metadata) in peers.iter() { + if metadata.last_active.elapsed() > CONNECTION_EXPIRE_TIME { + purged.push(*id); + } + } + + for id in &purged { + peers.remove(id); + } + + return purged; + } + + fn purge_expired_swarm_peers(info_hashes: &mut InfoHashMap, expired_connections: &[i64]) { + info_hashes.retain(|_, swarm| { + swarm.is_empty() + || swarm + .iter() + .find(|status| { + expired_connections.contains(&status.connection_id) + || status.last_active.elapsed() > DEFAULT_ANNOUNCE_INTERVAL * 2 + || status.last_event == Event::Stopped + }) + .is_some() + }); + } + + pub fn should_stop() -> bool { + SHOULD_STOP_SIGNAL.load(Ordering::Relaxed) + } +} diff --git a/src/tracker/udp_server.rs b/src/tracker/udp_server.rs new file mode 100644 index 0000000..2523ac9 --- /dev/null +++ b/src/tracker/udp_server.rs @@ -0,0 +1,257 @@ +use std::net::{Ipv4Addr, SocketAddr, UdpSocket}; +use std::thread; + +use anyhow::Result; +use flume::{Receiver, Sender}; +use log; + +use crate::tracker::Tracker; +use crate::tracker::bittorrent::info_hash::InfoHash; +use crate::tracker::bittorrent::peer::PeerId; +use crate::tracker::bittorrent::protocol::Event; +use crate::tracker::bittorrent::udp::{ + AnnounceRequest, AnnounceResponse, MIN_ANNOUNCE_REQUEST_SIZE, MIN_SCRAPE_REQUEST_SIZE, + ScrapeRequest, UdpResponse, +}; +use crate::tracker::{ + bittorrent::{ + protocol::{Action, UDP_MAGIC}, + udp::{ConnectRequest, UdpRequest}, + }, + tracker::{RequestMessage, ResponseMessage}, +}; + +pub struct UdpServer { + addr: SocketAddr, + server_id: u32, +} + +const UDP_RECV_BUF_SIZE: usize = u16::MAX as usize; +const UDP_SEND_BUF_SIZE: usize = u16::MAX as usize; // Could also be 1500 for typical Ethernet MTU (?) + +impl UdpServer { + pub fn new(addr: SocketAddr, id: u32) -> Self { + UdpServer { + addr: addr, + server_id: id, + } + } + + pub fn start( + &self, + req_chan: Sender, + resp_chan: Receiver, + ) -> Result<()> { + let recv_socket = UdpSocket::bind(self.addr)?; + let send_socket = recv_socket.try_clone()?; + let local_addr = recv_socket.local_addr()?; + let id = self.server_id; + + log::info!("Starting UDP server on: {:?}", local_addr); + + let recv_thread = thread::spawn(move || { + let mut buf: [u8; UDP_RECV_BUF_SIZE] = [0; UDP_RECV_BUF_SIZE]; + + loop { + if Tracker::should_stop() { + break; + } + + match recv_socket.recv_from(&mut buf) { + Ok((n_bytes, src)) => { + log::trace!("(Server {}) Received {} bytes from {}", id, n_bytes, src); + + match try_parse_packet(&buf[..n_bytes]) { + Some(req) => { + if req_chan + .send(RequestMessage { + request: req, + src_addr: src, + server_id: id, + }) + .is_err() + { + log::error!( + "Internal tracker error: server {} request channel closed", + id + ); + break; + } + } + None => continue, + }; + } + Err(error) => { + log::error!("Failed to receive on socket: {}", error); + break; + } + } + } + + log::debug!("Server {} receive thread shutting exiting ...", id) + }); + + let send_thread = thread::spawn(move || { + let mut buf: [u8; UDP_SEND_BUF_SIZE] = [0; UDP_SEND_BUF_SIZE]; + + loop { + if Tracker::should_stop() { + break; + } + + let response = match resp_chan.recv() { + Ok(r) => r, + _ => { + log::error!( + "Internal tracker error: server {} response channel closed", + id + ); + break; + } + }; + + log::trace!("(Server {}) Tracker response", id); + + let n_bytes = match response.response { + UdpResponse::Connect(connect) => connect.write_to_buf(&mut buf), + UdpResponse::Announce(announce) => match announce { + AnnounceResponse::V4(v4) => v4.write_to_buf(&mut buf), + AnnounceResponse::V6(v6) => v6.write_to_buf(&mut buf), + }, + UdpResponse::Scrape(scrape) => scrape.write_to_buf(&mut buf), + UdpResponse::Error(_) => 0, + }; + + let n_sent = match send_socket.send_to(&buf[..n_bytes], response.dst_addr) { + Ok(n) => n, + Err(e) => { + log::error!( + "Failed to send {} bytes to {}: {}", + n_bytes, + response.dst_addr, + e + ); + 0 + } + }; + + log::trace!( + "(Server {}) Sent {} bytes to {}: {:x?}", + id, + n_sent, + response.dst_addr, + &buf[..n_bytes] + ); + } + + log::debug!("Server {} send thread shutting exiting ...", id) + }); + + let _ = recv_thread.join(); + let _ = send_thread.join(); + + log::info!("Stopped UDP server on: {:?}", local_addr); + + return Ok(()); + } +} + +fn try_parse_packet(buf: &[u8]) -> Option { + if buf.len() < 16 { + return None; + } + + let action: Action = Action::from_i32(i32::from_be_bytes(buf[8..12].try_into().ok()?))?; + + let request = match action { + Action::Connect => UdpRequest::Connect(try_parse_connect(&buf)?), + Action::Announce => UdpRequest::Announce(try_parse_announce(&buf)?), + Action::Scrape => UdpRequest::Scrape(try_parse_scrape(&buf)?), + _ => return None, + }; + + return Some(request); +} + +fn try_parse_connect(buf: &[u8]) -> Option { + // Buffer length is checked to be at least 16 in `try_parse_packet` + + let protocol_id: i64 = i64::from_be_bytes(buf[0..8].try_into().ok()?); + + if protocol_id != UDP_MAGIC { + return None; + } + + Some(ConnectRequest { + protocol_id: protocol_id, + transaction_id: i32::from_be_bytes(buf[12..16].try_into().ok()?), + }) +} + +fn try_parse_announce(buf: &[u8]) -> Option { + if buf.len() < MIN_ANNOUNCE_REQUEST_SIZE { + return None; + } + + let connection_id: i64 = i64::from_be_bytes(buf[0..8].try_into().ok()?); + let transaction_id: i32 = i32::from_be_bytes(buf[12..16].try_into().ok()?); + let info_hash: InfoHash = InfoHash { + bytes: buf[16..36].try_into().ok()?, + }; + let peer_id: PeerId = PeerId { + bytes: buf[36..56].try_into().unwrap_or_default(), + }; + let downloaded: i64 = i64::from_be_bytes(buf[56..64].try_into().ok()?); + let left: i64 = i64::from_be_bytes(buf[64..72].try_into().ok()?); + let uploaded: i64 = i64::from_be_bytes(buf[72..80].try_into().ok()?); + let event: Event = Event::from_i32(i32::from_be_bytes(buf[80..84].try_into().ok()?))?; + let ip_addr: Option = if buf[84..88].iter().all(|b| *b == 0x00) { + None + } else { + Some(Ipv4Addr::from_bits(u32::from_be_bytes( + buf[84..88].try_into().ok()?, + ))) + }; + let key: u32 = u32::from_be_bytes(buf[88..92].try_into().ok()?); + let num_want: Option = { + let want = i32::from_be_bytes(buf[92..96].try_into().ok()?); + if want < 0 { None } else { Some(want as u32) } + }; + let port: u16 = u16::from_be_bytes(buf[96..98].try_into().ok()?); + + Some(AnnounceRequest { + connection_id: connection_id, + transaction_id: transaction_id, + info_hash: info_hash, + peer_id: peer_id, + downloaded: downloaded, + left: left, + uploaded: uploaded, + event: event, + ipv4_address: ip_addr, + key: key, + num_want: num_want, + port: port, + }) +} + +fn try_parse_scrape(buf: &[u8]) -> Option { + if buf.len() < MIN_SCRAPE_REQUEST_SIZE { + return None; + } + + let connection_id: i64 = i64::from_be_bytes(buf[0..8].try_into().ok()?); + let transaction_id: i32 = i32::from_be_bytes(buf[12..16].try_into().ok()?); + let info_hashes: Vec = buf[16..] + .chunks_exact(20) + .map(|b| InfoHash { + bytes: b.try_into().unwrap(), // unwrap: `chunks_exact` guarantees the size + }) + .collect(); + + Some(ScrapeRequest { + connection_id: connection_id, + transaction_id: transaction_id, + info_hashes: info_hashes, + }) +}