diff --git a/Cargo.lock b/Cargo.lock index 279e4a67d..10899d4ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -47,6 +47,18 @@ dependencies = [ "either", ] +[[package]] +name = "arrayref" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" + +[[package]] +name = "arraytools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5367919938d9d809b2f2f6a3c7d7174c85ebb08beadf0c1d3dd94f64439f437" + [[package]] name = "arrayvec" version = "0.5.2" @@ -158,6 +170,20 @@ dependencies = [ "wyz", ] +[[package]] +name = "blake3" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a08e53fc5a564bb15bfe6fae56bd71522205f1f91893f9c0116edad6496c183f" +dependencies = [ + "arrayref", + "arrayvec 0.7.2", + "cc", + "cfg-if", + "constant_time_eq", + "digest 0.10.3", +] + [[package]] name = "block-buffer" version = "0.9.0" @@ -176,6 +202,28 @@ dependencies = [ "generic-array", ] +[[package]] +name = "blowfish" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" +dependencies = [ + "byteorder", + "cipher", +] + +[[package]] +name = "bstr" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +dependencies = [ + "lazy_static", + "memchr", + "regex-automata", + "serde 1.0.137", +] + [[package]] name = "buf_redux" version = "0.8.4" @@ -210,6 +258,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cc" version = "1.0.73" @@ -245,6 +299,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "cipher" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1873270f8f7942c191139cb8a40fd228da6c3fd2fc376d7e92d47aa14aeb59e" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clang-sys" version = "1.3.1" @@ -302,6 +366,12 @@ version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbdcdcb6d86f71c5e97409ad45898af11cbc995b4ee8112d59095a28d376c935" +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + [[package]] name = "convert_case" version = "0.4.0" @@ -342,6 +412,87 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "criterion" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b01d6de93b2b6c65e17c634a26653a29d107b3c98c607c765bf38d041531cd8f" +dependencies = [ + "atty", + "cast", + "clap", + "criterion-plot", + "csv", + "itertools", + "lazy_static", + "num-traits 0.2.15", + "oorandom", + "plotters", + "rayon", + "regex", + "serde 1.0.137", + "serde_cbor", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2673cc8207403546f45f5fd319a974b1e6983ad1a3ee7e6041650013be041876" +dependencies = [ + "cast", + "itertools", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "045ebe27666471bb549370b4b0b3e51b07f56325befa4284db65fc89c02511b1" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "once_cell", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51887d4adc7b564537b15adcfb307936f8075dfcd5f00dde9a9f1d29383682bc" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "crypto-common" version = "0.1.3" @@ -352,6 +503,28 @@ dependencies = [ "typenum", ] +[[package]] +name = "csv" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1" +dependencies = [ + "bstr", + "csv-core", + "itoa 0.4.8", + "ryu", + "serde 1.0.137", +] + +[[package]] +name = "csv-core" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90" +dependencies = [ + "memchr", +] + [[package]] name = "darling" version = "0.14.1" @@ -428,6 +601,7 @@ checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" dependencies = [ "block-buffer 0.10.2", "crypto-common", + "subtle", ] [[package]] @@ -734,6 +908,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" + [[package]] name = "hashbrown" version = "0.11.2" @@ -809,7 +989,7 @@ checksum = "ff8670570af52249509a86f5e3e18a08c60b177071826898fde8997cf5f6bfbb" dependencies = [ "bytes", "fnv", - "itoa", + "itoa 1.0.1", ] [[package]] @@ -856,7 +1036,7 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa", + "itoa 1.0.1", "pin-project-lite", "socket2", "tokio", @@ -893,6 +1073,15 @@ dependencies = [ "serde 1.0.137", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "instant" version = "0.1.12" @@ -914,6 +1103,21 @@ dependencies = [ "syn", ] +[[package]] +name = "itertools" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + [[package]] name = "itoa" version = "1.0.1" @@ -1321,6 +1525,12 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" +[[package]] +name = "oorandom" +version = "11.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" + [[package]] name = "opaque-debug" version = "0.3.0" @@ -1468,6 +1678,34 @@ version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" +[[package]] +name = "plotters" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9428003b84df1496fb9d6eeee9c5f8145cb41ca375eb0dad204328888832811f" +dependencies = [ + "num-traits 0.2.15", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "193228616381fecdc1224c62e96946dfbc73ff4384fba576e052ff8c1bea8142" + +[[package]] +name = "plotters-svg" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0918736323d1baff32ee0eade54984f6f201ad7e97d5cfb5d6ab4a358529615" +dependencies = [ + "plotters-backend", +] + [[package]] name = "ppv-lite86" version = "0.2.16" @@ -1571,6 +1809,30 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rayon" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd99e5772ead8baa5215278c9b15bf92087709e9c1b2d1f97cdb5a183c933a7d" +dependencies = [ + "autocfg", + "crossbeam-deque", + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "258bcdb5ac6dad48491bb2992db6b7cf74878b0384908af124823d118c99683f" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + [[package]] name = "redox_syscall" version = "0.2.13" @@ -1591,6 +1853,12 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" + [[package]] name = "regex-syntax" version = "0.6.25" @@ -1701,6 +1969,15 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "saturating" version = "0.1.0" @@ -1838,6 +2115,16 @@ dependencies = [ "serde 1.0.137", ] +[[package]] +name = "serde_cbor" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5" +dependencies = [ + "half", + "serde 1.0.137", +] + [[package]] name = "serde_derive" version = "1.0.137" @@ -1855,7 +2142,7 @@ version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c" dependencies = [ - "itoa", + "itoa 1.0.1", "ryu", "serde 1.0.137", ] @@ -1867,7 +2154,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa", + "itoa 1.0.1", "ryu", "serde 1.0.137", ] @@ -2081,6 +2368,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + [[package]] name = "syn" version = "1.0.92" @@ -2182,7 +2475,7 @@ version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db76ff9fa4b1458b3c7f077f3ff9887394058460d21e634355b273aaf11eea45" dependencies = [ - "itoa", + "itoa 1.0.1", "libc", "num_threads", "serde 1.0.137", @@ -2211,6 +2504,16 @@ dependencies = [ "syn", ] +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde 1.0.137", + "serde_json", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -2333,10 +2636,16 @@ name = "torrust-tracker" version = "2.3.0" dependencies = [ "aquatic_udp_protocol", + "arraytools", "async-trait", "binascii", + "blake3", + "blowfish", + "byteorder", "chrono", + "cipher", "config", + "criterion", "derive_more", "fern", "futures", @@ -2542,6 +2851,17 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] + [[package]] name = "want" version = "0.3.0" diff --git a/Cargo.toml b/Cargo.toml index 9d21ed7d7..a2ef536fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,13 @@ opt-level = 3 lto = "fat" strip = true +[dev-dependencies] +criterion = "0.3" + +[[bench]] +name = "generate_connection_id" +harness = false + [dependencies] tokio = { version = "1.7", features = ["rt-multi-thread", "net", "sync", "macros", "signal"] } @@ -28,6 +35,10 @@ serde_with = "2.0.0" hex = "0.4.3" percent-encoding = "2.1.0" binascii = "0.1" +blake3 = "1.3.1" +cipher = "0.4.3" +blowfish = "0.9.1" +byteorder = "1.4.3" openssl = { version = "0.10.41", features = ["vendored"] } @@ -52,3 +63,4 @@ async-trait = "0.1.52" aquatic_udp_protocol = "0.2.0" uuid = { version = "1.1.2", features = ["v4"] } +arraytools = "0.1.5" diff --git a/benches/generate_connection_id.rs b/benches/generate_connection_id.rs new file mode 100644 index 000000000..1a8dfc4e0 --- /dev/null +++ b/benches/generate_connection_id.rs @@ -0,0 +1,38 @@ +use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; +use aquatic_udp_protocol::ConnectionId; +use criterion::{Criterion, criterion_group, criterion_main}; +use torrust_tracker::{udp::connection_id::get_connection_id, protocol::clock::current_timestamp}; + +fn get_connection_id_old(current_time: u64, port: u16) -> ConnectionId { + let time_i64 = (current_time / 3600) as i64; + + ConnectionId((time_i64 | port as i64) << 36) +} + +pub fn benchmark_generate_id_with_time_and_port(bench: &mut Criterion) { + let remote_address = SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 117); + let current_time = current_timestamp(); + + bench.bench_function("generate_id_with_time_and_port", |b| { + b.iter(|| { + // Inner closure, the actual test + let _ = get_connection_id_old(current_time, remote_address.port()); + }) + }); +} + +pub fn benchmark_generate_id_with_hashed_time_and_ip_and_port_and_salt(bench: &mut Criterion) { + let remote_address = SocketAddr::from(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 117)); + let current_time = current_timestamp(); + let server_secret = [0;32]; + + bench.bench_function("generate_id_with_hashed_time_and_ip_and_port_and_salt", |b| { + b.iter(|| { + // Inner closure, the actual test + let _ = get_connection_id(&server_secret, &remote_address, current_time); + }) + }); +} + +criterion_group!(benches, benchmark_generate_id_with_time_and_port, benchmark_generate_id_with_hashed_time_and_ip_and_port_and_salt); +criterion_main!(benches); diff --git a/src/api/server.rs b/src/api/server.rs index 19ceac92a..674c496a9 100644 --- a/src/api/server.rs +++ b/src/api/server.rs @@ -1,3 +1,47 @@ +//! # API Server +//! +//! HTTP server for the tracker HTTP API. +//! +//! Endpoint example: +//! +//! GET /api/torrent/:info_hash +//! +//! Get torrent details. +//! +//! ```s +//! curl -s http://127.0.0.1:1212/api/torrent/4beb7001cb833968582c67f55cc59dcc6c8d3fe5?token=MyAccessToken | jq +//! ``` +//! +//! ```json +//! { +//! "info_hash": "4beb7001cb833968582c67f55cc59dcc6c8d3fe5", +//! "seeders": 1, +//! "completed": 0, +//! "leechers": 0, +//! "peers": [ +//! { +//! "peer_id": { +//! "id": "2d7142343431302d7358376d33786d2877674179", +//! "client": "qBittorrent" +//! }, +//! "peer_addr": "192.168.1.88:17548", +//! "updated": 385, +//! "uploaded": 0, +//! "downloaded": 0, +//! "left": 0, +//! "event": "None" +//! } +//! ] +//! } +//! ``` +//! +//! | Parameter | Description | +//! |-----------|-------------| +//! | info_hash | The info_hash of the torrent. | +//! +//! The `info_hash.peers.updated` are the number of milliseconds since the last update. +//! + use std::cmp::min; use std::collections::{HashMap, HashSet}; use std::net::SocketAddr; diff --git a/src/protocol/clock.rs b/src/protocol/clock.rs new file mode 100644 index 000000000..501466404 --- /dev/null +++ b/src/protocol/clock.rs @@ -0,0 +1,22 @@ +use std::time::SystemTime; + +pub type UnixTime = u64; + +/// A Clock which uses the UNIX time. +pub trait UnixClock { + fn now(&self) -> UnixTime; +} + +/// A Clock which uses the operating system to determine the time. +pub struct SystemUnixClock; + +impl UnixClock for SystemUnixClock { + fn now(&self) -> UnixTime { + SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs() + } +} + +/// It returns the current timestamp using the system clock. +pub fn current_timestamp() -> UnixTime { + SystemUnixClock.now() +} diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs index 99cfd91e4..412c3c62c 100644 --- a/src/protocol/mod.rs +++ b/src/protocol/mod.rs @@ -1,2 +1,2 @@ pub mod common; -pub mod utils; +pub mod clock; diff --git a/src/protocol/utils.rs b/src/protocol/utils.rs deleted file mode 100644 index 30b87b99b..000000000 --- a/src/protocol/utils.rs +++ /dev/null @@ -1,21 +0,0 @@ -use std::net::SocketAddr; -use std::time::SystemTime; - -use aquatic_udp_protocol::ConnectionId; - -pub fn get_connection_id(remote_address: &SocketAddr) -> ConnectionId { - match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) { - Ok(duration) => ConnectionId(((duration.as_secs() / 3600) | ((remote_address.port() as u64) << 36)) as i64), - Err(_) => ConnectionId(0x7FFFFFFFFFFFFFFF), - } -} - -pub fn current_time() -> u64 { - SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH).unwrap() - .as_secs() -} - -pub fn ser_instant(inst: &std::time::Instant, ser: S) -> Result { - ser.serialize_u64(inst.elapsed().as_millis() as u64) -} diff --git a/src/tracker/key.rs b/src/tracker/key.rs index 2e2ca81f7..da07ebd7d 100644 --- a/src/tracker/key.rs +++ b/src/tracker/key.rs @@ -4,9 +4,8 @@ use rand::{Rng, thread_rng}; use rand::distributions::Alphanumeric; use serde::Serialize; -use crate::protocol::utils::current_time; - use crate::AUTH_KEY_LENGTH; +use crate::protocol::clock::current_timestamp; pub fn generate_auth_key(seconds_valid: u64) -> AuthKey { let key: String = thread_rng() @@ -19,12 +18,12 @@ pub fn generate_auth_key(seconds_valid: u64) -> AuthKey { AuthKey { key, - valid_until: Some(current_time() + seconds_valid), + valid_until: Some(current_timestamp() + seconds_valid), } } pub fn verify_auth_key(auth_key: &AuthKey) -> Result<(), Error> { - let current_time = current_time(); + let current_time = current_timestamp(); if auth_key.valid_until.is_none() { return Err(Error::KeyInvalid); } if auth_key.valid_until.unwrap() < current_time { return Err(Error::KeyExpired); } diff --git a/src/tracker/mod.rs b/src/tracker/mod.rs index 791e2e7d2..636ae33d9 100644 --- a/src/tracker/mod.rs +++ b/src/tracker/mod.rs @@ -1,6 +1,7 @@ pub mod tracker; pub mod statistics; pub mod peer; +pub mod serializer; pub mod torrent; pub mod key; pub mod mode; diff --git a/src/tracker/peer.rs b/src/tracker/peer.rs index ce4e52022..e2701ab65 100644 --- a/src/tracker/peer.rs +++ b/src/tracker/peer.rs @@ -5,9 +5,9 @@ use serde; use serde::{Serialize}; use crate::protocol::common::{NumberOfBytesDef, AnnounceEventDef}; -use crate::protocol::utils::ser_instant; use crate::http::AnnounceRequest; use crate::PeerId; +use crate::tracker::serializer::ser_instant; #[derive(PartialEq, Eq, Debug, Clone, Serialize)] pub struct TorrentPeer { diff --git a/src/tracker/serializer.rs b/src/tracker/serializer.rs new file mode 100644 index 000000000..2e7a84492 --- /dev/null +++ b/src/tracker/serializer.rs @@ -0,0 +1,47 @@ +/// Serializer for `std::time::Instant` type. +/// Before serializing, it converts the instant to time elapse since that instant in milliseconds. +/// +/// You can use it like this: +/// +/// ```text +/// #[serde(serialize_with = "ser_instant")] +/// pub updated: std::time::Instant, +/// ``` +/// +pub fn ser_instant(inst: &std::time::Instant, ser: S) -> Result { + ser.serialize_u64(inst.elapsed().as_millis() as u64) +} + +#[cfg(test)] +mod tests { + use std::{time::Instant}; + use serde::Serialize; + + #[warn(unused_imports)] + use super::ser_instant; + + #[derive(PartialEq, Eq, Debug, Clone, Serialize)] + struct S { + #[serde(serialize_with = "ser_instant")] + pub time: Instant, + } + + #[test] + fn instant_types_can_be_serialized_as_elapsed_time_since_that_instant_in_milliseconds() { + + use std::{thread, time}; + + let t1 = Instant::now(); + + let s = S { time: t1 }; + + // Sleep 10 milliseconds + let ten_millis = time::Duration::from_millis(10); + thread::sleep(ten_millis); + + let json_serialized_value = serde_json::to_string(&s).unwrap(); + + // Json contains time duration since t1 instant in milliseconds + assert_eq!(json_serialized_value, r#"{"time":10}"#); + } +} diff --git a/src/udp/connection/client_id.rs b/src/udp/connection/client_id.rs new file mode 100644 index 000000000..d652235bf --- /dev/null +++ b/src/udp/connection/client_id.rs @@ -0,0 +1,104 @@ +//! ClientId is a unique ID for the UDP tracker client. +//! Currently implemented with a hash of the socket, i.e the IP and port. +use std::hash::Hash; +use std::hash::Hasher; +use std::net::SocketAddr; + +#[derive(PartialEq, Debug, Clone)] +pub struct ClientId { + value: [u8; 4], +} + +pub trait Make { + fn new(socket: &SocketAddr) -> Self; + + fn hash(socket: &SocketAddr) -> [u8;8] { + let mut hasher = T::default(); + socket.hash(&mut hasher); + + hasher.finish().to_le_bytes() + } +} + +impl Make for ClientId { + fn new(socket: &SocketAddr) -> Self { + let hash = >::hash(socket); + + let mut truncated_hash: [u8; 4] = [0u8; 4]; + truncated_hash.copy_from_slice(&hash[..4]); + + ClientId { + value: truncated_hash, + } + } +} + +impl ClientId { + /// It generates the ID with a previously generated value + pub fn from_bytes(bytes: &[u8]) -> Self { + let mut client_id = ClientId { + value: [0u8; 4] + }; + client_id.value.copy_from_slice(bytes); + client_id + } + + pub fn to_bytes(&self) -> [u8; 4] { + let bytes: [u8; 4] = self.value; + bytes + } +} + +#[cfg(test)] +mod tests { + use std::{net::{IpAddr, Ipv4Addr, SocketAddr}, collections::hash_map::DefaultHasher}; + + use super::{ClientId, Make}; + + #[test] + fn it_should_be_a_hash_of_the_socket() { + let socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080); + let id: ClientId = Make::::new(&socket); + + assert_eq!(id.value, [213, 195, 130, 185]); + } + + #[test] + fn id_should_be_converted_to_a_byte_array() { + let socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080); + let id: ClientId = Make::::new(&socket); + + assert_eq!(id.to_bytes(), [213, 195, 130, 185]); + } + + #[test] + fn id_should_be_instantiate_from_a_previously_generated_value() { + let socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080); + let id: ClientId = Make::::new(&socket); + let bytes = id.to_bytes(); + + assert_eq!(ClientId::from_bytes(&bytes), id); + } + + #[test] + fn it_should_be_unique_with_different_socket_ips() { + let socket_1 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080); + let socket_2 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 2)), 8080); + + assert_ne!( + >::new(&socket_1), + >::new(&socket_2) + ); + } + + #[test] + fn it_should_be_unique_with_different_socket_ports() { + let socket_1 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080); + let socket_2 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8081); + + assert_ne!( + >::new(&socket_1), + >::new(&socket_2) + ); + } +} \ No newline at end of file diff --git a/src/udp/connection/connection_id_data.rs b/src/udp/connection/connection_id_data.rs new file mode 100644 index 000000000..86425bec2 --- /dev/null +++ b/src/udp/connection/connection_id_data.rs @@ -0,0 +1,46 @@ +use super::{client_id::ClientId, timestamp_32::Timestamp32}; + +/// The data stored inside the connection id +#[derive(PartialEq, Debug, Clone)] +pub struct ConnectionIdData { + pub client_id: ClientId, + pub expiration_timestamp: Timestamp32 +} + +impl ConnectionIdData { + pub fn client_id(&self) -> &ClientId { + &self.client_id + } + + pub fn expiration_timestamp(&self) -> &Timestamp32 { + &self.expiration_timestamp + } +} + +#[cfg(test)] +mod tests { + use crate::udp::connection::{connection_id_data::ConnectionIdData, client_id::ClientId}; + + + #[test] + fn it_contains_a_client_id() { + + let connection_id = ConnectionIdData { + client_id: ClientId::from_bytes(&[0u8; 4]), + expiration_timestamp: 0u32.into(), + }; + + assert_eq!(connection_id.client_id, ClientId::from_bytes(&[0u8; 4])); + } + + #[test] + fn it_contains_an_expiration_timestamp() { + + let connection_id = ConnectionIdData { + client_id: ClientId::from_bytes(&[0u8; 4]), + expiration_timestamp: 0u32.into(), + }; + + assert_eq!(connection_id.expiration_timestamp, 0u32.into()); + } +} diff --git a/src/udp/connection/connection_id_issuer.rs b/src/udp/connection/connection_id_issuer.rs new file mode 100644 index 000000000..c662abb42 --- /dev/null +++ b/src/udp/connection/connection_id_issuer.rs @@ -0,0 +1,198 @@ +use std::{net::SocketAddr, collections::hash_map::DefaultHasher}; + +use aquatic_udp_protocol::ConnectionId; + +use super::{cypher::{BlowfishCypher, Cypher}, secret::Secret, timestamp_64::Timestamp64, client_id::Make, timestamp_32::Timestamp32, connection_id_data::{ConnectionIdData}, encrypted_connection_id_data::EncryptedConnectionIdData, encoded_connection_id_data::EncodedConnectionIdData}; + +pub trait ConnectionIdIssuer { + fn new_connection_id(&self, remote_address: &SocketAddr, current_timestamp: Timestamp64) -> ConnectionId; + + fn is_connection_id_valid(&self, connection_id: &ConnectionId, remote_address: &SocketAddr, current_timestamp: Timestamp64) -> bool; +} + +/// An implementation of a ConnectionIdIssuer which encrypts the connection id +pub struct EncryptedConnectionIdIssuer { + cypher: BlowfishCypher +} + +impl ConnectionIdIssuer for EncryptedConnectionIdIssuer { + fn new_connection_id(&self, remote_address: &SocketAddr, current_timestamp: Timestamp64) -> ConnectionId { + + let connection_id_data = self.generate_connection_id_data(remote_address, current_timestamp); + + let encoded_connection_id_data: EncodedConnectionIdData = connection_id_data.into(); + + let encrypted_connection_id_data = self.encrypt_connection_id_data(&encoded_connection_id_data); + + self.pack_connection_id(encrypted_connection_id_data) + } + + fn is_connection_id_valid(&self, connection_id: &ConnectionId, remote_address: &SocketAddr, current_timestamp: Timestamp64) -> bool { + + let encrypted_connection_id_data: EncryptedConnectionIdData = self.unpack_connection_id(connection_id); + + let connection_id_data = self.decrypt_connection_id_data(&encrypted_connection_id_data); + + let current_client_id = Make::::new(remote_address); + if connection_id_data.client_id != current_client_id { + return false; + } + + if current_timestamp > connection_id_data.expiration_timestamp.into() { + return false; + } + + true + } +} + +impl EncryptedConnectionIdIssuer { + pub fn new(secret: Secret) -> Self { + let cypher = BlowfishCypher::new(secret); + Self { + cypher + } + } + + fn generate_connection_id_data(&self, remote_address: &SocketAddr, current_timestamp: Timestamp64) -> ConnectionIdData { + let client_id = Make::::new(remote_address); + + let expiration_timestamp: Timestamp32 = (current_timestamp + 120).try_into().unwrap(); + + ConnectionIdData { + client_id, + expiration_timestamp + } + } + + fn pack_connection_id(&self, encrypted_connection_id_data: EncryptedConnectionIdData) -> ConnectionId { + ConnectionId(encrypted_connection_id_data.into()) + } + + fn unpack_connection_id(&self, connection_id: &ConnectionId) -> EncryptedConnectionIdData { + let encrypted_raw_data: EncryptedConnectionIdData = connection_id.0.into(); + encrypted_raw_data + } + + fn decrypt_connection_id_data(&self, encrypted_connection_id_data: &EncryptedConnectionIdData) -> ConnectionIdData { + let decrypted_raw_data = self.cypher.decrypt(&encrypted_connection_id_data.bytes); + + let encoded_connection_id_data = EncodedConnectionIdData::from_bytes(&decrypted_raw_data); + + let connection_id_data: ConnectionIdData = encoded_connection_id_data.into(); + + connection_id_data + } + + fn encrypt_connection_id_data(&self, encoded_connection_id_data: &EncodedConnectionIdData) -> EncryptedConnectionIdData { + let encrypted_raw_data = self.cypher.encrypt(encoded_connection_id_data.as_bytes()); + EncryptedConnectionIdData::from_encrypted_bytes(&encrypted_raw_data) + } +} + +#[cfg(test)] +mod tests { + use crate::udp::connection::{secret::Secret, connection_id_issuer::{EncryptedConnectionIdIssuer, ConnectionIdIssuer}}; + + use std::{net::{SocketAddr, IpAddr, Ipv4Addr}}; + + fn cypher_secret_for_testing() -> Secret { + Secret::from_bytes([0u8;32]) + } + + fn new_issuer() -> EncryptedConnectionIdIssuer { + EncryptedConnectionIdIssuer::new(cypher_secret_for_testing()) + } + + #[test] + fn it_should_be_valid_for_two_minutes_after_the_generation() { + let client_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080); + let now = 946684800u64; // 01-01-2000 00:00:00 + + let issuer = new_issuer(); + + let connection_id = issuer.new_connection_id(&client_addr, now); + + assert!(issuer.is_connection_id_valid(&connection_id, &client_addr, now)); + + let after_two_minutes = now + (2*60) - 1; + + assert!(issuer.is_connection_id_valid(&connection_id, &client_addr, after_two_minutes)); + } + + #[test] + fn it_should_expire_after_two_minutes_from_the_generation() { + let client_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080); + let now = 946684800u64; + + let issuer = new_issuer(); + + let connection_id = issuer.new_connection_id(&client_addr, now); + + let after_more_than_two_minutes = now + (2*60) + 1; + + assert!(!issuer.is_connection_id_valid(&connection_id, &client_addr, after_more_than_two_minutes)); + } + + #[test] + fn it_should_change_for_the_same_client_ip_and_port_after_two_minutes() { + let client_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080); + + let now = 946684800u64; + + let issuer = new_issuer(); + + let connection_id = issuer.new_connection_id( &client_addr, now); + + let after_two_minutes = now + 120; + + let connection_id_after_two_minutes = issuer.new_connection_id(&client_addr, after_two_minutes); + + assert_ne!(connection_id, connection_id_after_two_minutes); + } + + #[test] + fn it_should_be_different_for_each_client_at_the_same_time_if_they_use_a_different_ip() { + let client_1_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 2)), 1); + let client_2_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 1); + + let now = 946684800u64; + + let issuer = new_issuer(); + + let connection_id_for_client_1 = issuer.new_connection_id(&client_1_addr, now); + let connection_id_for_client_2 = issuer.new_connection_id(&client_2_addr, now); + + assert_ne!(connection_id_for_client_1, connection_id_for_client_2); + } + + #[test] + fn it_should_be_different_for_each_client_at_the_same_time_if_they_use_a_different_port() { + let client_1_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 1); + let client_2_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 2); + + let now = 946684800u64; + + let issuer = new_issuer(); + + let connection_id_for_client_1 = issuer.new_connection_id(&client_1_addr, now); + let connection_id_for_client_2 = issuer.new_connection_id(&client_2_addr, now); + + assert_ne!(connection_id_for_client_1, connection_id_for_client_2); + } + + #[test] + fn it_should_fails_verifying_a_connection_id_when_the_client_id_in_the_connection_id_data_does_not_the_current_client_id() { + let issuer = new_issuer(); + + // Generate connection id for a given client + let client_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 1); + let now = 946684800u64; + let connection_id = issuer.new_connection_id(&client_addr, now); + + // Verify the connection id with a different client address + let different_client_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 2)), 2); + + assert!(!issuer.is_connection_id_valid(&connection_id, &different_client_addr, now)); + } +} \ No newline at end of file diff --git a/src/udp/connection/cypher.rs b/src/udp/connection/cypher.rs new file mode 100644 index 000000000..a6e0fb94d --- /dev/null +++ b/src/udp/connection/cypher.rs @@ -0,0 +1,64 @@ +use super::secret::Secret; +use std::convert::TryInto; +use blowfish::{BlowfishLE, cipher::{KeyInit, BlockEncrypt, BlockDecrypt}, Blowfish}; +use byteorder::LittleEndian; +use cipher::generic_array::GenericArray; +use cipher::BlockSizeUser; + +pub trait Cypher { + fn encrypt(&self, decrypted_bytes: &[u8; 8]) -> [u8; 8]; + + fn decrypt(&self, encrypted_bytes: &[u8; 8]) -> [u8; 8]; +} + +pub struct BlowfishCypher { + blowfish: BlowfishLE +} + +impl BlowfishCypher { + pub fn new(secret: Secret) -> Self { + Self { + blowfish: BlowfishLE::new_from_slice(&secret.into_bytes()).unwrap() + } + } +} + +type BlowfishArray = GenericArray as BlockSizeUser>::BlockSize>; + +impl Cypher for BlowfishCypher { + fn encrypt(&self, decrypted_bytes: &[u8; 8]) -> [u8; 8] { + let mut encrypted_bytes: BlowfishArray = BlowfishArray::from(*decrypted_bytes); + + self.blowfish.encrypt_block(&mut encrypted_bytes); + + encrypted_bytes.try_into().unwrap() + } + + fn decrypt(&self, encrypted_bytes: &[u8; 8]) -> [u8; 8] { + let mut decrypted_bytes: BlowfishArray = BlowfishArray::from(*encrypted_bytes); + + self.blowfish.decrypt_block(&mut decrypted_bytes); + + decrypted_bytes.try_into().unwrap() + } +} + +#[cfg(test)] +mod tests { + use crate::udp::connection::{secret::Secret, cypher::{BlowfishCypher, Cypher}}; + + #[test] + fn it_should_encrypt_and_decrypt_a_byte_array() { + let secret = Secret::from_bytes([0u8;32]); + + let cypher = BlowfishCypher::new(secret); + + let text = [0u8, 1u8, 2u8, 3u8, 4u8, 5u8, 6u8, 7u8]; + + let encrypted_text = cypher.encrypt(&text); + + let decrypted_text = cypher.decrypt(&encrypted_text); + + assert_eq!(decrypted_text, [0u8, 1u8, 2u8, 3u8, 4u8, 5u8, 6u8, 7u8]); + } +} \ No newline at end of file diff --git a/src/udp/connection/encoded_connection_id_data.rs b/src/udp/connection/encoded_connection_id_data.rs new file mode 100644 index 000000000..5a6a9f6c7 --- /dev/null +++ b/src/udp/connection/encoded_connection_id_data.rs @@ -0,0 +1,45 @@ +use super::{client_id::ClientId, timestamp_32::Timestamp32, connection_id_data::ConnectionIdData}; + +/// The encoded version of ConnectionIdData to be use in the UPD tracker package field "connection_id" +pub struct EncodedConnectionIdData([u8; 8]); + +impl EncodedConnectionIdData { + pub fn from_bytes(bytes: &[u8; 8]) -> Self { + let mut sized_bytes_arr = [0u8; 8]; + sized_bytes_arr.copy_from_slice(&bytes[..8]); + Self(sized_bytes_arr) + } + + pub fn as_bytes(&self) -> &[u8; 8] { + &self.0 + } + + fn to_client_id(&self) -> ClientId { + ClientId::from_bytes(&self.0[..4]) + } + + fn to_expiration_timestamp(&self) -> Timestamp32 { + let timestamp_bytes = &self.0[4..]; + Timestamp32::from_le_bytes(timestamp_bytes) + } +} + +impl From for ConnectionIdData { + fn from(encoded_connection_id_data: EncodedConnectionIdData) -> Self { + Self { + client_id: encoded_connection_id_data.to_client_id(), + expiration_timestamp: encoded_connection_id_data.to_expiration_timestamp() + } + } +} + +impl From for EncodedConnectionIdData { + fn from(connection_id_data: ConnectionIdData) -> Self { + let byte_vec: Vec = [ + connection_id_data.client_id.to_bytes().as_slice(), + connection_id_data.expiration_timestamp.to_le_bytes().as_slice(), + ].concat(); + let bytes: [u8; 8] = byte_vec.try_into().unwrap(); + EncodedConnectionIdData::from_bytes(&bytes) + } +} diff --git a/src/udp/connection/encrypted_connection_id_data.rs b/src/udp/connection/encrypted_connection_id_data.rs new file mode 100644 index 000000000..4149813e2 --- /dev/null +++ b/src/udp/connection/encrypted_connection_id_data.rs @@ -0,0 +1,52 @@ +#[derive(PartialEq, Debug, Copy, Clone)] +pub struct EncryptedConnectionIdData { + pub bytes: [u8; 8] +} + +impl EncryptedConnectionIdData { + pub fn from_encrypted_bytes(encrypted_bytes: &[u8; 8]) -> Self { + Self { bytes: *encrypted_bytes } + } +} + +impl From for i64 { + fn from(value: EncryptedConnectionIdData) -> Self { + i64::from_le_bytes(value.bytes) + } +} + +impl From for EncryptedConnectionIdData { + fn from(value: i64) -> Self { + Self { bytes: value.to_le_bytes() } + } +} + +#[cfg(test)] +mod tests { + use crate::udp::connection::encrypted_connection_id_data::EncryptedConnectionIdData; + + + #[test] + fn it_should_be_generated_from_the_encrypted_connection_id_data() { + + let encrypted_data = EncryptedConnectionIdData::from_encrypted_bytes(&[0u8; 8]); + + assert_eq!(encrypted_data, EncryptedConnectionIdData { bytes: [0u8; 8]}); + } + + #[test] + fn it_should_be_converted_into_a_i64() { + + let encrypted_data: i64 = EncryptedConnectionIdData::from_encrypted_bytes(&[0u8; 8]).into(); + + assert_eq!(encrypted_data, 0i64); + } + + #[test] + fn it_should_be_converted_from_a_i64() { + + let encrypted_data: EncryptedConnectionIdData = 0i64.into(); + + assert_eq!(encrypted_data, EncryptedConnectionIdData { bytes: [0u8; 8]}); + } +} \ No newline at end of file diff --git a/src/udp/connection/mod.rs b/src/udp/connection/mod.rs new file mode 100644 index 000000000..294db3a81 --- /dev/null +++ b/src/udp/connection/mod.rs @@ -0,0 +1,92 @@ + +//! Connection ID is a value generated by the tracker and sent to the client +//! to avoid the client spoofing it's source IP address. +//! +//! Detailed info in [BEP 15](https://www.bittorrent.org/beps/bep_0015.html) +//! +//! In order for the client to connect to the tracker, it must send a connection ID +//! previously generated by the server. +//! +//! The client has to send a "connect" request: +//! +//! | Offset | Size | Name | Value | +//! |--------|----------------|----------------|---------------------------------| +//! | 0 | 64-bit integer | protocol_id | 0x41727101980 // magic constant | +//! | 8 | 32-bit integer | action | 0 // connect | +//! | 12 | 32-bit integer | transaction_id | | +//! | 16 | | | | +//! +//! And it receives a Connection ID in the response: +//! +//! | Offset | Size | Name | Value | +//! |--------|----------------|----------------|--------------| +//! | 0 | 32-bit integer | action | 0 // connect | +//! | 4 | 32-bit integer | transaction_id | | +//! | 8 | 64-bit integer | connection_id | | +//! | 16 | | | | +//! +//! The client has to send the Connection ID in all subsequent requests. +//! The tracker verifies the connection_id and ignores the request if it doesn't match. +//! +//! From the BEP 15 specification a Connection ID: +//! +//! - Should not be guessable by the client. +//! - Can be used for multiple requests. +//! - Can be used by a client until one minute after it has received it. +//! - Can be accepted by the tracker until two minutes after it has been send. +//! +//! Additionally we define the Connection ID as a value that: +//! +//! - That is unpredictable. The user should not be able to construct their own Connection ID. +//! - That is unique to the the particular connection. Locked to a IP and Port. +//! - That is time bound. It expires after certain time. +//! - That is memoryless. The server doesn't remember what ID's it gave out. +//! - That is stateless. The issuer and the verifier can work interdependently without a dynamic common state. +//! +//! # Why do we need a connection ID? +//! +//! With the Connection ID we check for two things: +//! +//! - The announcing client owns the ip and port it is announcing with. +//! - The announcing client is an online BitTorrent peer. +//! +//! It's a kind of "proof of IP ownership" and "proof of online BitTorrent peer". +//! This makes sure that the client is not a fake client. And it makes harder for attackers +//! to fill the tracker peer list with fake clients. +//! +//! The only way to send an "announce" request is actually being an active and accessible BitTorrent client. +//! +//! It also avoid clients to send requests on behave of other clients. +//! If there is a legitimate client on the network, attackers could impersonate that client, +//! since they know the IP and port of the legitimate client. +//! An attacker could send an "announce" request for a torrent that the legitimate client does not have. +//! That's a kind of DOS attack because it would make harder to find a torrent. +//! The information about what torrents have each client could be easily manipulated. +//! +//! # Example Implementation +//! +//! Some tracker implementations use a time bound connection ID to avoid storing the connection ID +//! in memory or in the DB. +//! +//! ```text +//! static uint64_t _genCiD (uint32_t ip, uint16_t port) +//! { +//! uint64_t x; +//! x = (time(NULL) / 3600) * port; // x will probably overload. +//! x = (ip ^ port); +//! x <<= 16; +//! x |= (~port); +//! return x; +//! } +//! ``` +//! +//! From [here](https://github.com/troydm/udpt/blob/master/src/db/driver_sqlite.cpp#L410-L418). +pub mod secret; +pub mod client_id; +pub mod timestamp_32; +pub mod timestamp_64; +pub mod cypher; +pub mod connection_id_issuer; +pub mod connection_id_data; +pub mod encrypted_connection_id_data; +pub mod encoded_connection_id_data; diff --git a/src/udp/connection/secret.rs b/src/udp/connection/secret.rs new file mode 100644 index 000000000..601549859 --- /dev/null +++ b/src/udp/connection/secret.rs @@ -0,0 +1,60 @@ +//! A secret for encryption. + +#[derive(PartialEq, Debug, Copy, Clone)] +pub struct Secret([u8; 32]); + +impl Secret { + + pub fn new() -> Self { + let key = Self::generate_random_key(); + Self::from_bytes(key) + } + + pub fn generate_random_key() -> [u8; 32] { + let key: [u8; 32] = rand::Rng::gen(&mut rand::rngs::ThreadRng::default()); + key + } + + pub fn from_bytes(bytes: [u8; 32]) -> Self { + Secret(bytes) + } + + pub fn into_bytes(self) -> [u8; 32] { + self.0 + } +} + +impl Default for Secret { + fn default() -> Self { + Self::new() + } +} + +impl From for [u8; 32] { + fn from(secret: Secret) -> Self { + secret.0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_should_be_created_from_a_preexisting_byte_array_key() { + let secret = Secret::from_bytes([0; 32]); + assert_eq!(secret, Secret([0u8; 32])); + } + + #[test] + fn it_should_be_converted_into_a_byte_array_using_the_standard_trait() { + let byte_array_32: [u8; 32] = Secret::from_bytes([0; 32]).into(); + assert_eq!(byte_array_32, [0u8; 32]); + } + + #[test] + fn it_should_be_converted_into_a_byte_array() { + let byte_array_32_1 = Secret::from_bytes([0; 32]); + assert_eq!(byte_array_32_1.into_bytes(), [0u8; 32]); + } +} diff --git a/src/udp/connection/timestamp_32.rs b/src/udp/connection/timestamp_32.rs new file mode 100644 index 000000000..f1e09612c --- /dev/null +++ b/src/udp/connection/timestamp_32.rs @@ -0,0 +1,116 @@ +//! A UNIX 32-bit timestamp. + +use std::num::TryFromIntError; + +use super::timestamp_64::Timestamp64; + +#[derive(PartialEq, Debug, Copy, Clone)] +pub struct Timestamp32(pub u32); + +impl Timestamp32 { + pub fn from_le_bytes(timestamp_bytes: &[u8]) -> Self { + // Little Endian + let timestamp = u32::from_le_bytes([timestamp_bytes[0], timestamp_bytes[1], timestamp_bytes[2], timestamp_bytes[3]]); + Self(timestamp) + } + + pub fn to_le_bytes(self) -> [u8; 4] { + // Little Endian + let mut bytes: [u8; 4] = [0u8; 4]; + bytes.copy_from_slice(&self.0.to_le_bytes()[..4]); + bytes + } +} + +impl From for Timestamp32 { + fn from(value: u32) -> Self { + Self(value) + } +} + +impl TryFrom for Timestamp32 { + type Error = TryFromIntError; + + fn try_from(value: Timestamp64) -> Result { + let timestamp32: u32 = u32::try_from(value)?; + Ok(Self(timestamp32)) + } +} + +impl From for Timestamp64 { + fn from(timestamp32: Timestamp32) -> Self { + u64::from(timestamp32.0) + } +} + +#[cfg(test)] +mod tests { + use crate::udp::connection::{timestamp_32::Timestamp32, timestamp_64::Timestamp64}; + + #[test] + fn it_should_be_instantiated_from_a_four_byte_array_in_little_indian() { + + let min_timestamp = Timestamp32::from_le_bytes(&[0u8, 0u8, 0u8, 0u8]); + + assert_eq!(min_timestamp, Timestamp32(u32::MIN)); + + let max_timestamp = Timestamp32::from_le_bytes(&[255u8, 255u8, 255u8, 255u8]); + + assert_eq!(max_timestamp, Timestamp32(u32::MAX)); + } + + #[test] + fn it_should_be_converted_to_a_four_byte_array_in_little_indian() { + + let min_timestamp = Timestamp32(u32::MIN); + + assert_eq!(min_timestamp.to_le_bytes(), [0u8, 0u8, 0u8, 0u8]); + + let max_timestamp = Timestamp32(u32::MAX); + + assert_eq!(max_timestamp.to_le_bytes(), [255u8, 255u8, 255u8, 255u8]); + } + + #[test] + fn it_should_be_converted_from_a_64_bit_unix_timestamp() { + + let timestamp32: Timestamp32 = 0u64.try_into().unwrap(); + + assert_eq!(timestamp32, Timestamp32(u32::MIN)); + } + + #[test] + fn it_should_fail_trying_to_convert_it_from_a_64_bit_unix_timestamp_which_overflows_u32_range() { + + let out_of_range_value = (u32::MAX as u64) + 1; + + let timestamp32: Result = out_of_range_value.try_into(); + + assert!(timestamp32.is_err()); + } + + #[test] + fn it_should_be_converted_from_a_u32() { + + let timestamp32: Timestamp32 = u32::MIN.into(); + + assert_eq!(timestamp32, Timestamp32(u32::MIN)); + } + + #[test] + fn it_should_be_converted_to_a_timestamp_64() { + + let min_timestamp_32 = Timestamp32(u32::MIN); + + let min_timestamp_64: Timestamp64 = min_timestamp_32.into(); + + assert_eq!(min_timestamp_64, u32::MIN as u64); + + + let max_timestamp_32 = Timestamp32(u32::MAX); + + let max_timestamp_64: Timestamp64 = max_timestamp_32.into(); + + assert_eq!(max_timestamp_64, u32::MAX as u64); + } +} \ No newline at end of file diff --git a/src/udp/connection/timestamp_64.rs b/src/udp/connection/timestamp_64.rs new file mode 100644 index 000000000..16bb8aa5f --- /dev/null +++ b/src/udp/connection/timestamp_64.rs @@ -0,0 +1,8 @@ +/// Connection id contains a timestamp in 4 bytes due to its 64 bits limit. +pub type Timestamp64 = u64; + +pub fn timestamp_from_le_bytes(timestamp_bytes: &[u8]) -> Timestamp64 { + // Little Endian + u64::from_le_bytes([timestamp_bytes[0], timestamp_bytes[1], timestamp_bytes[2], timestamp_bytes[3], 0, 0, 0, 0]) +} + diff --git a/src/udp/errors.rs b/src/udp/errors.rs index fb29e969e..057026f57 100644 --- a/src/udp/errors.rs +++ b/src/udp/errors.rs @@ -28,4 +28,7 @@ pub enum ServerError { #[error("bad request")] BadRequest, + + #[error("invalid connection id")] + InvalidConnectionId, } diff --git a/src/udp/handlers.rs b/src/udp/handlers.rs deleted file mode 100644 index 860a2fe4b..000000000 --- a/src/udp/handlers.rs +++ /dev/null @@ -1,204 +0,0 @@ -use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; -use std::sync::Arc; - -use aquatic_udp_protocol::{AnnounceInterval, AnnounceRequest, AnnounceResponse, ConnectRequest, ConnectResponse, ErrorResponse, NumberOfDownloads, NumberOfPeers, Port, Request, Response, ResponsePeer, ScrapeRequest, ScrapeResponse, TorrentScrapeStatistics, TransactionId}; - -use crate::{InfoHash, MAX_SCRAPE_TORRENTS}; -use crate::peer::TorrentPeer; -use crate::tracker::torrent::{TorrentError}; -use crate::udp::errors::ServerError; -use crate::udp::request::AnnounceRequestWrapper; -use crate::tracker::statistics::TrackerStatisticsEvent; -use crate::tracker::tracker::TorrentTracker; -use crate::protocol::utils::get_connection_id; - -pub async fn authenticate(info_hash: &InfoHash, tracker: Arc) -> Result<(), ServerError> { - match tracker.authenticate_request(info_hash, &None).await { - Ok(_) => Ok(()), - Err(e) => { - let err = match e { - TorrentError::TorrentNotWhitelisted => ServerError::TorrentNotWhitelisted, - TorrentError::PeerNotAuthenticated => ServerError::PeerNotAuthenticated, - TorrentError::PeerKeyNotValid => ServerError::PeerKeyNotValid, - TorrentError::NoPeersFound => ServerError::NoPeersFound, - TorrentError::CouldNotSendResponse => ServerError::InternalServerError, - TorrentError::InvalidInfoHash => ServerError::InvalidInfoHash, - }; - - Err(err) - } - } -} - -pub async fn handle_packet(remote_addr: SocketAddr, payload: Vec, tracker: Arc) -> Response { - match Request::from_bytes(&payload[..payload.len()], MAX_SCRAPE_TORRENTS).map_err(|_| ServerError::InternalServerError) { - Ok(request) => { - let transaction_id = match &request { - Request::Connect(connect_request) => { - connect_request.transaction_id - } - Request::Announce(announce_request) => { - announce_request.transaction_id - } - Request::Scrape(scrape_request) => { - scrape_request.transaction_id - } - }; - - match handle_request(request, remote_addr, tracker).await { - Ok(response) => response, - Err(e) => handle_error(e, transaction_id) - } - } - // bad request - Err(_) => handle_error(ServerError::BadRequest, TransactionId(0)) - } -} - -pub async fn handle_request(request: Request, remote_addr: SocketAddr, tracker: Arc) -> Result { - match request { - Request::Connect(connect_request) => { - handle_connect(remote_addr, &connect_request, tracker).await - } - Request::Announce(announce_request) => { - handle_announce(remote_addr, &announce_request, tracker).await - } - Request::Scrape(scrape_request) => { - handle_scrape(remote_addr, &scrape_request, tracker).await - } - } -} - -pub async fn handle_connect(remote_addr: SocketAddr, request: &ConnectRequest, tracker: Arc) -> Result { - let connection_id = get_connection_id(&remote_addr); - - let response = Response::from(ConnectResponse { - transaction_id: request.transaction_id, - connection_id, - }); - - // send stats event - match remote_addr { - SocketAddr::V4(_) => { tracker.send_stats_event(TrackerStatisticsEvent::Udp4Connect).await; } - SocketAddr::V6(_) => { tracker.send_stats_event(TrackerStatisticsEvent::Udp6Connect).await; } - } - - Ok(response) -} - -pub async fn handle_announce(remote_addr: SocketAddr, announce_request: &AnnounceRequest, tracker: Arc) -> Result { - let wrapped_announce_request = AnnounceRequestWrapper::new(announce_request.clone()); - - authenticate(&wrapped_announce_request.info_hash, tracker.clone()).await?; - - let peer = TorrentPeer::from_udp_announce_request(&wrapped_announce_request.announce_request, remote_addr.ip(), tracker.config.get_ext_ip()); - - //let torrent_stats = tracker.update_torrent_with_peer_and_get_stats(&wrapped_announce_request.info_hash, &peer).await; - - let torrent_stats = tracker.update_torrent_with_peer_and_get_stats(&wrapped_announce_request.info_hash, &peer).await; - - // get all peers excluding the client_addr - let peers = tracker.get_torrent_peers(&wrapped_announce_request.info_hash, &peer.peer_addr).await; - - let announce_response = if remote_addr.is_ipv4() { - Response::from(AnnounceResponse { - transaction_id: wrapped_announce_request.announce_request.transaction_id, - announce_interval: AnnounceInterval(tracker.config.announce_interval as i32), - leechers: NumberOfPeers(torrent_stats.leechers as i32), - seeders: NumberOfPeers(torrent_stats.seeders as i32), - peers: peers.iter() - .filter_map(|peer| if let IpAddr::V4(ip) = peer.peer_addr.ip() { - Some(ResponsePeer:: { - ip_address: ip, - port: Port(peer.peer_addr.port()), - }) - } else { - None - } - ).collect(), - }) - } else { - Response::from(AnnounceResponse { - transaction_id: wrapped_announce_request.announce_request.transaction_id, - announce_interval: AnnounceInterval(tracker.config.announce_interval as i32), - leechers: NumberOfPeers(torrent_stats.leechers as i32), - seeders: NumberOfPeers(torrent_stats.seeders as i32), - peers: peers.iter() - .filter_map(|peer| if let IpAddr::V6(ip) = peer.peer_addr.ip() { - Some(ResponsePeer:: { - ip_address: ip, - port: Port(peer.peer_addr.port()), - }) - } else { - None - } - ).collect(), - }) - }; - - // send stats event - match remote_addr { - SocketAddr::V4(_) => { tracker.send_stats_event(TrackerStatisticsEvent::Udp4Announce).await; } - SocketAddr::V6(_) => { tracker.send_stats_event(TrackerStatisticsEvent::Udp6Announce).await; } - } - - Ok(announce_response) -} - -// todo: refactor this, db lock can be a lot shorter -pub async fn handle_scrape(remote_addr: SocketAddr, request: &ScrapeRequest, tracker: Arc) -> Result { - let db = tracker.get_torrents().await; - - let mut torrent_stats: Vec = Vec::new(); - - for info_hash in request.info_hashes.iter() { - let info_hash = InfoHash(info_hash.0); - - let scrape_entry = match db.get(&info_hash) { - Some(torrent_info) => { - if authenticate(&info_hash, tracker.clone()).await.is_ok() { - let (seeders, completed, leechers) = torrent_info.get_stats(); - - TorrentScrapeStatistics { - seeders: NumberOfPeers(seeders as i32), - completed: NumberOfDownloads(completed as i32), - leechers: NumberOfPeers(leechers as i32), - } - } else { - TorrentScrapeStatistics { - seeders: NumberOfPeers(0), - completed: NumberOfDownloads(0), - leechers: NumberOfPeers(0), - } - } - } - None => { - TorrentScrapeStatistics { - seeders: NumberOfPeers(0), - completed: NumberOfDownloads(0), - leechers: NumberOfPeers(0), - } - } - }; - - torrent_stats.push(scrape_entry); - } - - drop(db); - - // send stats event - match remote_addr { - SocketAddr::V4(_) => { tracker.send_stats_event(TrackerStatisticsEvent::Udp4Scrape).await; } - SocketAddr::V6(_) => { tracker.send_stats_event(TrackerStatisticsEvent::Udp6Scrape).await; } - } - - Ok(Response::from(ScrapeResponse { - transaction_id: request.transaction_id, - torrent_stats, - })) -} - -fn handle_error(e: ServerError, transaction_id: TransactionId) -> Response { - let message = e.to_string(); - Response::from(ErrorResponse { transaction_id, message: message.into() }) -} diff --git a/src/udp/mod.rs b/src/udp/mod.rs index 25780ba93..38d143dc0 100644 --- a/src/udp/mod.rs +++ b/src/udp/mod.rs @@ -1,12 +1,19 @@ +//! BitTorrent UDP Tracker Implementation. +//! +//! Protocol Specification: +//! +//! [BEP 15](https://www.bittorrent.org/beps/bep_0015.html) + pub use self::errors::*; -pub use self::handlers::*; +pub use self::packet_handler::*; pub use self::request::*; pub use self::server::*; +pub mod connection; pub mod errors; pub mod request; pub mod server; -pub mod handlers; +pub mod packet_handler; pub type Bytes = u64; pub type Port = u16; diff --git a/src/udp/packet_handler.rs b/src/udp/packet_handler.rs new file mode 100644 index 000000000..5ad51a02d --- /dev/null +++ b/src/udp/packet_handler.rs @@ -0,0 +1,251 @@ +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; +use std::sync::Arc; +use aquatic_udp_protocol::{AnnounceInterval, AnnounceRequest, AnnounceResponse, ConnectRequest, ConnectResponse, NumberOfDownloads, NumberOfPeers, Port, Request, Response, ResponsePeer, ScrapeRequest, ScrapeResponse, TorrentScrapeStatistics, ConnectionId, ErrorResponse, TransactionId}; +use log::debug; +use crate::torrent::TorrentError; +use crate::udp::connection::connection_id_issuer::{EncryptedConnectionIdIssuer, ConnectionIdIssuer}; +use crate::udp::connection::secret::Secret; +use crate::{InfoHash, MAX_SCRAPE_TORRENTS}; +use crate::peer::TorrentPeer; +use crate::udp::errors::ServerError; +use crate::udp::request::AnnounceRequestWrapper; +use crate::tracker::statistics::TrackerStatisticsEvent; +use crate::tracker::tracker::TorrentTracker; +use crate::protocol::clock::{SystemUnixClock, UnixClock}; + +pub struct PacketHandler { + connection_id_issuer: EncryptedConnectionIdIssuer, + clock: SystemUnixClock +} + +impl PacketHandler { + pub fn new(secret: Secret) -> Self { + Self { + connection_id_issuer: EncryptedConnectionIdIssuer::new(secret), + clock: SystemUnixClock, + } + } + + pub async fn handle_packet(&self, remote_addr: SocketAddr, payload: Vec, tracker: Arc) -> Option { + match Request::from_bytes(&payload[..payload.len()], MAX_SCRAPE_TORRENTS).map_err(|_| ServerError::InternalServerError) { + Ok(request) => { + let transaction_id = match &request { + Request::Connect(connect_request) => { + connect_request.transaction_id + } + Request::Announce(announce_request) => { + announce_request.transaction_id + } + Request::Scrape(scrape_request) => { + scrape_request.transaction_id + } + }; + + match self.handle_request(request, remote_addr, tracker).await { + Ok(response) => Some(response), + Err(ServerError::InvalidConnectionId) => None, + Err(e) => Some(Self::handle_error(e, transaction_id)) + } + } + // don't respond to bad requests + Err(_) => None + } + } + + fn handle_error(e: ServerError, transaction_id: TransactionId) -> Response { + let message = e.to_string(); + Response::from(ErrorResponse { transaction_id, message: message.into() }) + } + + + async fn handle_request(&self, request: Request, remote_addr: SocketAddr, tracker: Arc) -> Result { + match request { + Request::Connect(connect_request) => { + self.handle_connect(remote_addr, &connect_request, tracker).await + } + Request::Announce(announce_request) => { + self.handle_announce(remote_addr, &announce_request, tracker).await + } + Request::Scrape(scrape_request) => { + self.handle_scrape(remote_addr, &scrape_request, tracker).await + } + } + } + + async fn handle_connect(&self, remote_addr: SocketAddr, request: &ConnectRequest, tracker: Arc) -> Result { + let connection_id = self.generate_new_connection_id(&remote_addr); + + let response = Response::from(ConnectResponse { + transaction_id: request.transaction_id, + connection_id, + }); + + // send stats event + match remote_addr { + SocketAddr::V4(_) => { tracker.send_stats_event(TrackerStatisticsEvent::Udp4Connect).await; } + SocketAddr::V6(_) => { tracker.send_stats_event(TrackerStatisticsEvent::Udp6Connect).await; } + } + + Ok(response) + } + + async fn handle_announce(&self, remote_addr: SocketAddr, announce_request: &AnnounceRequest, tracker: Arc) -> Result { + let valid = self.is_connection_id_valid(&announce_request.connection_id, &remote_addr); + if !valid { + return Err(ServerError::InvalidConnectionId); + } + + let wrapped_announce_request = AnnounceRequestWrapper::new(announce_request.clone()); + + self.authenticate(&wrapped_announce_request.info_hash, tracker.clone()).await?; + + let peer = TorrentPeer::from_udp_announce_request(&wrapped_announce_request.announce_request, remote_addr.ip(), tracker.config.get_ext_ip()); + + //let torrent_stats = tracker.update_torrent_with_peer_and_get_stats(&wrapped_announce_request.info_hash, &peer).await; + + let torrent_stats = tracker.update_torrent_with_peer_and_get_stats(&wrapped_announce_request.info_hash, &peer).await; + + // get all peers excluding the client_addr + let peers = tracker.get_torrent_peers(&wrapped_announce_request.info_hash, &peer.peer_addr).await; + + let announce_response = if remote_addr.is_ipv4() { + Response::from(AnnounceResponse { + transaction_id: wrapped_announce_request.announce_request.transaction_id, + announce_interval: AnnounceInterval(tracker.config.announce_interval as i32), + leechers: NumberOfPeers(torrent_stats.leechers as i32), + seeders: NumberOfPeers(torrent_stats.seeders as i32), + peers: peers.iter() + .filter_map(|peer| if let IpAddr::V4(ip) = peer.peer_addr.ip() { + Some(ResponsePeer:: { + ip_address: ip, + port: Port(peer.peer_addr.port()), + }) + } else { + None + } + ).collect(), + }) + } else { + Response::from(AnnounceResponse { + transaction_id: wrapped_announce_request.announce_request.transaction_id, + announce_interval: AnnounceInterval(tracker.config.announce_interval as i32), + leechers: NumberOfPeers(torrent_stats.leechers as i32), + seeders: NumberOfPeers(torrent_stats.seeders as i32), + peers: peers.iter() + .filter_map(|peer| if let IpAddr::V6(ip) = peer.peer_addr.ip() { + Some(ResponsePeer:: { + ip_address: ip, + port: Port(peer.peer_addr.port()), + }) + } else { + None + } + ).collect(), + }) + }; + + // send stats event + match remote_addr { + SocketAddr::V4(_) => { tracker.send_stats_event(TrackerStatisticsEvent::Udp4Announce).await; } + SocketAddr::V6(_) => { tracker.send_stats_event(TrackerStatisticsEvent::Udp6Announce).await; } + } + + Ok(announce_response) + } + + // todo: refactor this, db lock can be a lot shorter + async fn handle_scrape(&self, remote_addr: SocketAddr, request: &ScrapeRequest, tracker: Arc) -> Result { + let valid = self.is_connection_id_valid(&request.connection_id, &remote_addr); + if !valid { + return Err(ServerError::InvalidConnectionId); + } + + let db = tracker.get_torrents().await; + + let mut torrent_stats: Vec = Vec::new(); + + for info_hash in request.info_hashes.iter() { + let info_hash = InfoHash(info_hash.0); + + let scrape_entry = match db.get(&info_hash) { + Some(torrent_info) => { + if self.authenticate(&info_hash, tracker.clone()).await.is_ok() { + let (seeders, completed, leechers) = torrent_info.get_stats(); + + TorrentScrapeStatistics { + seeders: NumberOfPeers(seeders as i32), + completed: NumberOfDownloads(completed as i32), + leechers: NumberOfPeers(leechers as i32), + } + } else { + TorrentScrapeStatistics { + seeders: NumberOfPeers(0), + completed: NumberOfDownloads(0), + leechers: NumberOfPeers(0), + } + } + } + None => { + TorrentScrapeStatistics { + seeders: NumberOfPeers(0), + completed: NumberOfDownloads(0), + leechers: NumberOfPeers(0), + } + } + }; + + torrent_stats.push(scrape_entry); + } + + drop(db); + + // send stats event + match remote_addr { + SocketAddr::V4(_) => { tracker.send_stats_event(TrackerStatisticsEvent::Udp4Scrape).await; } + SocketAddr::V6(_) => { tracker.send_stats_event(TrackerStatisticsEvent::Udp6Scrape).await; } + } + + Ok(Response::from(ScrapeResponse { + transaction_id: request.transaction_id, + torrent_stats, + })) + } + + async fn authenticate(&self, info_hash: &InfoHash, tracker: Arc) -> Result<(), ServerError> { + match tracker.authenticate_request(info_hash, &None).await { + Ok(_) => Ok(()), + Err(e) => { + let err = match e { + TorrentError::TorrentNotWhitelisted => ServerError::TorrentNotWhitelisted, + TorrentError::PeerNotAuthenticated => ServerError::PeerNotAuthenticated, + TorrentError::PeerKeyNotValid => ServerError::PeerKeyNotValid, + TorrentError::NoPeersFound => ServerError::NoPeersFound, + TorrentError::CouldNotSendResponse => ServerError::InternalServerError, + TorrentError::InvalidInfoHash => ServerError::InvalidInfoHash, + }; + + Err(err) + } + } + } + + fn generate_new_connection_id(&self, remote_addr: &SocketAddr) -> ConnectionId { + let current_timestamp = self.clock.now(); + + let connection_id = self.connection_id_issuer.new_connection_id(remote_addr, current_timestamp); + + debug!("new connection id: {:?}, current timestamp: {:?}", connection_id, current_timestamp); + + connection_id + } + + fn is_connection_id_valid(&self, connection_id: &ConnectionId, remote_addr: &SocketAddr) -> bool { + let current_timestamp = self.clock.now(); + + let valid = self.connection_id_issuer.is_connection_id_valid(connection_id, remote_addr, current_timestamp); + + debug!("verify connection id: {:?}, current timestamp: {:?}, valid: {:?}", connection_id, current_timestamp, valid); + + valid + } +} diff --git a/src/udp/server.rs b/src/udp/server.rs index bcacc2642..7c1044731 100644 --- a/src/udp/server.rs +++ b/src/udp/server.rs @@ -7,7 +7,9 @@ use log::{debug, info}; use tokio::net::UdpSocket; use crate::tracker::tracker::TorrentTracker; -use crate::udp::{handle_packet, MAX_PACKET_SIZE}; +use crate::udp::MAX_PACKET_SIZE; +use crate::udp::connection::secret::Secret; +use crate::udp::packet_handler::PacketHandler; pub struct UdpServer { socket: Arc, @@ -25,11 +27,17 @@ impl UdpServer { } pub async fn start(&self) { + let encryption_key = Secret::new(); + + let request_handler = Arc::new(PacketHandler::new(encryption_key)); + loop { let mut data = [0; MAX_PACKET_SIZE]; let socket = self.socket.clone(); let tracker = self.tracker.clone(); + let packet_handler = request_handler.clone(); + // needed for graceful shutdown tokio::select! { _ = tokio::signal::ctrl_c() => { info!("Stopping UDP server: {}..", socket.local_addr().unwrap()); @@ -41,8 +49,9 @@ impl UdpServer { debug!("Received {} bytes from {}", payload.len(), remote_addr); debug!("{:?}", payload); - let response = handle_packet(remote_addr, payload, tracker).await; - UdpServer::send_response(socket, remote_addr, response).await; + if let Some(response) = packet_handler.handle_packet(remote_addr, payload, tracker).await { + UdpServer::send_response(socket, remote_addr, response).await; + } } } }