From 3163644c620af2598b528f77e09be8e47309543c Mon Sep 17 00:00:00 2001 From: soruh Date: Wed, 25 Jan 2023 19:58:27 +0100 Subject: [PATCH] initial commit --- .gitignore | 2 + Cargo.lock | 506 +++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 14 ++ src/main.rs | 446 +++++++++++++++++++++++++++++++++++++++++++ src/packets.rs | 210 ++++++++++++++++++++ 5 files changed, 1178 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/main.rs create mode 100644 src/packets.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..da1802e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +db.json diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..aa8cd64 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,506 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "anyhow" +version = "1.0.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61" +dependencies = [ + "backtrace", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bytemuck" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c041d3eab048880cb0b86b256447da3f18859a163c3b8d8893f4e6368abe6393" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aca418a974d83d40a0c1f0c5cba6ff4bc28d8df099109ca459a2118d40b6322" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bytes" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" + +[[package]] +name = "cc" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d" + +[[package]] +name = "centralex" +version = "0.1.0" +dependencies = [ + "anyhow", + "bytemuck", + "rand", + "serde", + "serde_json", + "tokio", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "getrandom" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec7af912d60cdbd3677c1af9352ebae6fb8394d165568a2234df0fa00f87793" + +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + +[[package]] +name = "itoa" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" + +[[package]] +name = "libc" +version = "0.2.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" + +[[package]] +name = "lock_api" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "miniz_oxide" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys", +] + +[[package]] +name = "num_cpus" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.30.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b8c786513eb403643f2a88c244c2aaa270ef2153f55094587d0c48a3cf22a83" +dependencies = [ + "memchr", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1ef8814b5c993410bb3adfad7a5ed269563e4a2f90c41f5d85be7fb47133bf" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro2" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef7d57beacfaf2d8aee5937dab7b7f28de3cb8b1828479bb5de2a7106f2bae2" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" + +[[package]] +name = "ryu" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "serde" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "socket2" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "syn" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tokio" +version = "1.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a12a59981d9e3c38d216785b0c37399f6e415e8d0712047620f189371b0bb" +dependencies = [ + "autocfg", + "bytes", + "libc", + "memchr", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..454e8b8 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "centralex" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = { version = "1.0.68", features = ["backtrace"] } +bytemuck = { version = "1.13.0", features = ["derive"] } +rand = "0.8.5" +serde = { version = "1.0.152", features = ["derive"] } +serde_json = "1.0.91" +tokio = { version = "1.24.2", features = ["full"] } diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..dc71eab --- /dev/null +++ b/src/main.rs @@ -0,0 +1,446 @@ +#![feature(generic_const_exprs)] +#![allow(unused)] + +use std::{ + collections::{BTreeMap, BTreeSet, HashMap, HashSet}, + fmt::Debug, + fs::File, + future::Future, + io::{BufReader, BufWriter}, + net::{IpAddr, Ipv4Addr, SocketAddr}, + ops::Range, + path::{Path, PathBuf}, + sync::{Arc, Mutex}, + time::{Duration, SystemTime, UNIX_EPOCH}, +}; + +use anyhow::bail; +use packets::{reject_static, Header, Packet, RemConnect}; +use serde::{Deserialize, Serialize}; +use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + net::{TcpListener, TcpSocket, TcpStream}, + select, + task::JoinHandle, + time::Instant, +}; + +use crate::packets::dyn_ip_update; + +const AUTH_TIMEOUT: Duration = Duration::from_secs(30); +const CALL_ACK_TIMEOUT: Duration = Duration::from_secs(30); +const PING_INTERVAL: Duration = Duration::from_secs(15); +const TIMEOUT_DELAY: Duration = Duration::from_secs(35); +const PORT_TIMEOUT: Duration = Duration::from_secs(60); +const PORT_RETRY_TIME: Duration = Duration::from_secs(60); // 10 * + +const BIND_IP: &str = "0.0.0.0"; + +mod packets; + +type Port = u16; +type Number = u32; +type UnixTimestamp = u64; + +#[derive(Default, Debug, Serialize, Deserialize)] +struct Config { + allowed_ports: AllowedPorts, +} + +impl Config { + fn load(db: &Path) -> std::io::Result { + println!("loading config"); + Ok(serde_json::from_reader(BufReader::new(File::open(db)?))?) + } + + fn load_or_default(db: &Path) -> std::io::Result { + match Self::load(db) { + Ok(db) => Ok(db), + Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(Self::default()), + Err(err) => Err(err), + } + } +} + +#[derive(Default, Debug, Serialize, Deserialize)] +struct PortHandler { + #[serde(skip)] + last_update: Option, + + #[serde(skip)] + port_guards: HashMap, + + allowed_ports: AllowedPorts, + + free_ports: HashSet, + errored_ports: BTreeSet<(UnixTimestamp, Port)>, + allocated_ports: HashMap, + port_status: HashMap, +} + +#[derive(Debug, Serialize, Deserialize)] +struct PortStatus {} + +#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +struct AllowedPorts(Vec>); + +impl AllowedPorts { + fn is_allowed(&self, port: Port) -> bool { + self.0.iter().any(|range| range.contains(&port)) + } +} + +impl PortHandler { + fn register_update(&mut self) { + self.last_update = Some(Instant::now()); + } + + fn store(&self, db: &Path) -> anyhow::Result<()> { + println!("storing database"); + serde_json::to_writer(BufWriter::new(File::create(db)?), self)?; + Ok(()) + } + + fn load(db: &Path) -> std::io::Result { + println!("loading database"); + Ok(serde_json::from_reader(BufReader::new(File::open(db)?))?) + } + + fn load_or_default(db: &Path) -> std::io::Result { + match Self::load(db) { + Ok(db) => Ok(db), + Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(Self::default()), + Err(err) => Err(err), + } + } + + fn update_allowed_ports(&mut self, allowed_ports: &AllowedPorts) { + self.register_update(); + + self.allowed_ports = allowed_ports.clone(); + + self.free_ports.clear(); + self.free_ports + .extend(self.allowed_ports.0.iter().cloned().flatten()); + + self.free_ports.shrink_to_fit(); // we are at the maximum number of ports we'll ever reach + + self.errored_ports + .retain(|(_, port)| self.allowed_ports.is_allowed(*port)); + + self.allocated_ports + .retain(|_, port| self.allowed_ports.is_allowed(*port)); + + self.free_ports.retain(|port| { + self.allocated_ports + .iter() + .find(|(_, allocated_port)| *allocated_port == port) + .is_none() + && self + .errored_ports + .iter() + .find(|(_, errored_port)| errored_port == port) + .is_none() + }); + } + + fn start_port_guard<'fut, Fut, Func>(&mut self, port: Port, listener: TcpListener, f: Func) + where + Fut: Future + Send + 'fut, + Func: FnOnce(&'_ mut TcpListener) -> Fut + Send + 'static, + { + assert!(self + .port_guards + .insert(port, PortGuard::start(listener, f)) + .is_none()); + } + + fn start_rejector(&mut self, port: Port, listener: TcpListener, packet: Packet) { + assert!(self + .port_guards + .insert( + port, + PortGuard::start(listener, move |listener: &mut TcpListener| async move { + loop { + if let Ok((mut socket, _)) = listener.accept().await { + let (_, mut writer) = socket.split(); + let _ = packet.send(&mut writer).await; + } + } + }) + ) + .is_none()); + } +} + +struct PortGuard { + listener: Arc>, + handle: JoinHandle<()>, +} + +impl Debug for PortGuard { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PortGuard").finish() + } +} + +impl PortGuard { + fn start<'fut, Fut>( + listener: TcpListener, + f: impl FnOnce(&mut TcpListener) -> Fut + Send + 'static, + ) -> Self + where + Fut: Future + Send + 'fut, + { + let mut listener = Arc::new(tokio::sync::Mutex::new(listener)); + + let handle = { + let listener = listener.clone(); + + tokio::spawn(async move { + let mut lock = listener.lock().await; + f(&mut *lock).await; + }) + }; + + Self { listener, handle } + } + + async fn stop(mut self) -> TcpListener { + self.handle.abort(); + let _ = self.handle.await; + Arc::try_unwrap(self.listener).unwrap().into_inner() + } +} + +impl PortHandler { + fn allocate_port_for_number(&mut self, number: Number) -> Option { + if let Some(port) = self.allocated_ports.get(&number) { + return Some(*port); + } + + let port = if let Some(&port) = self.free_ports.iter().next() { + self.register_update(); + self.free_ports.remove(&port); + port + } else { + self.try_recover_port()? + }; + + assert!(self.allocated_ports.insert(number, port).is_none()); + Some(port) + } + + fn try_recover_port(&mut self) -> Option { + let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); + + let mut recovered_port = None; + + self.errored_ports = std::mem::take(&mut self.errored_ports) + .into_iter() + .filter_map(|(mut timestamp, mut port)| { + if recovered_port.is_none() + && now.saturating_sub(Duration::from_secs(timestamp)) >= PORT_RETRY_TIME + { + println!( + " trying port: {port} at -{:?}", + Duration::from_secs(now.as_secs()) + .saturating_sub(Duration::from_secs(timestamp)) + ); + + match std::net::TcpListener::bind((BIND_IP, port)) { + Ok(_) => { + recovered_port = Some((timestamp, port)); + return None; + } + Err(_) => timestamp = now.as_secs(), + } + } else { + println!( + "skipped port: {port} at -{:?}", + Duration::from_secs(now.as_secs()) + .saturating_sub(Duration::from_secs(timestamp)) + ); + } + + Some((timestamp, port)) + }) + .collect(); + + if let Some((_, port)) = recovered_port { + println!("recovered_port: {port}"); + return Some(port); + } + + None // TODO + } + + fn mark_port_error(&mut self, number: Number, port: Port) { + self.register_update(); + + self.errored_ports.insert(( + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(), + port, + )); + + self.allocated_ports.remove(&number); + self.free_ports.remove(&port); + } + + fn open_port(&mut self, port: Port) -> Option { + todo!() + } + + fn close_port_for(&mut self, number: Number, listener: TcpListener) -> anyhow::Result<()> { + todo!() + } +} + +async fn connection_handler( + port_handler: Arc>, + stream: &mut TcpStream, +) -> anyhow::Result<()> { + let (mut reader, mut writer) = stream.split(); + + let mut packet = Packet::recv(&mut reader).await?; + + let RemConnect { number, pin } = packet.as_rem_connect()?; + + let (port, listener) = loop { + let port = port_handler + .lock() + .unwrap() + .allocate_port_for_number(number); + + println!("allocated port: {:?}", port); + + let Some(port) = port else { + writer.write_all(&reject_static(b"oop")).await?; + return Ok(()); + }; + + let ip = dyn_ip_update(number, pin, port).await?; + + let listener = TcpListener::bind((BIND_IP, port)).await; + + let listener = match listener { + Ok(listener) => break (port, listener), + Err(err) => { + port_handler.lock().unwrap().mark_port_error(number, port); + // tokio::time::sleep(Duration::from_millis(300)).await; + continue; + } + }; + }; + + #[derive(Debug)] + enum Foo { + Caller { stream: TcpStream, addr: SocketAddr }, + Packet { packet: Packet }, + } + + let result = select! { + kind = Packet::peek_packet_kind(&mut reader) => { + packet.recv_into(&mut reader).await?; + Foo::Packet { packet } + }, + caller = listener.accept() => { + let (stream, addr) = caller?; + Foo::Caller { stream, addr } + }, + }; + + dbg!(&result); + + match result { + Foo::Caller { stream, addr } => todo!(), + Foo::Packet { mut packet } => { + match packet.kind() { + packets::PacketKind::End => { + packet.header = Header { kind: 3, length: 0 }; + packet.data.clear(); + } + packets::PacketKind::Reject => {} + + kind => bail!("unexpected packet: {kind:?}"), + } + port_handler + .lock() + .unwrap() + .start_rejector(port, listener, packet); + } + } + + Ok(()) +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let config = Config::load_or_default("config.json".as_ref())?; + + if config.allowed_ports.0.is_empty() { + panic!("no allowed ports"); + } + + let db_path = PathBuf::from("db.json"); + + let mut port_handler = PortHandler::load_or_default(&db_path)?; + port_handler.update_allowed_ports(&config.allowed_ports); + + let port_handler = Arc::new(Mutex::new(port_handler)); + + { + let port_handler = port_handler.clone(); + tokio::spawn(async move { + let mut last_store = None; + loop { + tokio::time::sleep(Duration::from_secs(1)).await; + + let port_handler = port_handler.lock().unwrap(); + + if let Some(last_update) = port_handler.last_update { + let should_store = last_store + .map(|last_store| last_update > last_store) + .unwrap_or(true); + + if should_store { + last_store = Some(last_update); + port_handler.store(&db_path).unwrap(); + } + } + } + }); + } + + let listener = TcpListener::bind(("127.0.0.1", 11812)).await?; + + while let Ok((mut stream, addr)) = listener.accept().await { + println!("connection from {addr}"); + + let port_handler = port_handler.clone(); + + tokio::spawn(async move { + if let Err(err) = connection_handler(port_handler, &mut stream).await { + println!("client at {addr} had an error: {err}"); + + let mut packet = Packet::default(); + + packet.data.extend_from_slice(err.to_string().as_bytes()); + packet.data.truncate(0xfe); + packet.data.push(0); + packet.header = Header { + kind: 0xff, + length: packet.data.len() as u8, + }; + + let (_, mut writer) = stream.split(); + let _ = packet.send(&mut writer).await; + } + }); + } + + Ok(()) +} diff --git a/src/packets.rs b/src/packets.rs new file mode 100644 index 0000000..33dbc40 --- /dev/null +++ b/src/packets.rs @@ -0,0 +1,210 @@ +use std::{ffi::CString, mem::discriminant}; + +use anyhow::bail; +use bytemuck::{Pod, Zeroable}; +use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + net::tcp::{ReadHalf, WriteHalf}, +}; + +pub const fn reject_static(message: &[u8; N]) -> [u8; N + 2] { + let mut pkg = [0u8; N + 2]; + pkg[0] = 4; + pkg[1] = message.len() as u8; + let mut i = 0; + while i < message.len() { + pkg[i + 2] = message[i]; + i += 1; + } + pkg +} + +pub const REJECT_OCC: &[u8; 6] = b"\x04\x04occ\x00"; +pub const REJECT_NC: &[u8; 5] = b"\x04\x03nc\x00"; + +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PacketKind { + Unknown(u8), + DynIpUpdate = 0x01, + DynIpUpdateResponse = 0x02, + End = 0x03, + Reject = 0x04, + RemConnect = 0x81, + RemConfirm = 0x82, + RemCall = 0x83, + RemAck = 0x84, + Error = 0xff, +} + +impl PacketKind { + fn from_u8(raw: u8) -> Self { + use PacketKind::*; + + match raw { + 0x01 => DynIpUpdate, + 0x02 => DynIpUpdateResponse, + 0x03 => End, + 0x04 => Reject, + 0x81 => RemConnect, + 0x82 => RemConfirm, + 0x83 => RemCall, + 0x84 => RemAck, + 0xff => Error, + kind => Unknown(kind), + } + } + + fn kind(&self) -> u8 { + use PacketKind::*; + + match self { + Unknown(value) => *value, + DynIpUpdate => 0x01, + DynIpUpdateResponse => 0x02, + End => 0x03, + Reject => 0x04, + RemConnect => 0x81, + RemConfirm => 0x82, + RemCall => 0x83, + RemAck => 0x84, + Error => 0xff, + } + } +} + +#[derive(Default, Debug, Clone, Copy, Pod, Zeroable)] +#[repr(C)] +pub struct Header { + pub kind: u8, + pub length: u8, +} + +#[derive(Debug, Default, Clone)] +pub struct Packet { + pub header: Header, + pub data: Vec, +} + +#[derive(Default, Debug, Clone, Copy)] +#[repr(C)] +pub struct RemConnect { + pub number: u32, + pub pin: u16, +} + +impl Packet { + pub async fn peek_packet_kind(stream: &mut ReadHalf<'_>) -> std::io::Result { + Self::peek_packet_kind_raw(stream) + .await + .map(PacketKind::from_u8) + } + + pub async fn peek_packet_kind_raw(stream: &mut ReadHalf<'_>) -> std::io::Result { + let mut kind = 0; + let n = stream.peek(std::slice::from_mut(&mut kind)).await?; + + if n == 1 { + Ok(kind) + } else { + Err(std::io::ErrorKind::UnexpectedEof.into()) + } + } + + pub async fn recv(stream: &mut ReadHalf<'_>) -> std::io::Result { + let mut packet = Packet::default(); + packet.recv_into(stream).await?; + Ok(packet) + } + + pub async fn recv_into(&mut self, stream: &mut ReadHalf<'_>) -> std::io::Result<()> { + let header_bytes = bytemuck::bytes_of_mut(&mut self.header); + + stream.read_exact(header_bytes).await?; + + self.data.resize(self.header.length as usize, 0); + + stream.read_exact(&mut self.data).await?; + + Ok(()) + } + + pub async fn send(&self, stream: &mut WriteHalf<'_>) -> std::io::Result<()> { + stream.write_all(bytemuck::bytes_of(&self.header)).await?; + stream.write_all(&self.data).await?; + Ok(()) + } + + pub fn kind(&self) -> PacketKind { + PacketKind::from_u8(self.header.kind) + } + + pub fn as_rem_connect(&self) -> anyhow::Result { + if self.kind() != PacketKind::RemConnect { + bail!("Unexpected Packet: {:?} expected RemConnect", self.kind()); + } + + if self.data.len() < 6 { + bail!( + "Too little data for RemConnect. Need at least 6 Bytes got {}", + self.data.len() + ); + } + + Ok(RemConnect { + number: u32::from_le_bytes(self.data[..4].try_into()?), + pin: u16::from_le_bytes(self.data[4..6].try_into()?), + }) + } +} + +pub async fn dyn_ip_update(number: u32, pin: u16, port: u16) -> anyhow::Result { + let mut packet = Packet::default(); + packet.header = Header { + kind: PacketKind::DynIpUpdate.kind(), + length: 8, + }; + + packet.data.clear(); + packet.data.reserve(packet.header.length as usize); + packet.data.extend_from_slice(&number.to_le_bytes()); + packet.data.extend_from_slice(&pin.to_le_bytes()); + packet.data.extend_from_slice(&port.to_le_bytes()); + + let mut socket = tokio::net::TcpStream::connect(("127.0.0.1", 11811)).await?; + + let (mut reader, mut writer) = socket.split(); + + packet.send(&mut writer).await?; + + packet.recv_into(&mut reader).await?; + + match packet.kind() { + PacketKind::DynIpUpdateResponse => Ok(<[u8; 4]>::try_from(packet.data) + .map_err(|err| { + anyhow::anyhow!( + "too little data for ip address. Need 4 bytes got {}", + err.len() + ) + })? + .into()), + PacketKind::Error => { + let first_zero = packet + .data + .iter() + .enumerate() + .find_map(|(i, x)| (*x == 0).then_some(i)); + + bail!( + "{}", + std::str::from_utf8( + first_zero + .map(|i| &packet.data[..i]) + .unwrap_or(&packet.data), + )? + ) + } + + _ => bail!("server returned unexpected packet"), + } +}