From 8c01dcf79b09a81f5b5d2bcff0d26d06e711dc85 Mon Sep 17 00:00:00 2001 From: heinrich5991 Date: Tue, 14 Jan 2025 18:31:39 +0100 Subject: [PATCH] mastersrv: Add config file and hot-reloading of it This allows to add bans and port forward exceptions without restarting the mastersrv itself. It also allows reloading the locations database (with the important caveat that it is mmap-ed, and as such must be removed before being overridden in the file system). --- src/mastersrv/Cargo.lock | 110 +++++++++++++++++++++++++++++--- src/mastersrv/Cargo.toml | 10 ++- src/mastersrv/src/locations.rs | 17 ++--- src/mastersrv/src/main.rs | 111 ++++++++++++++++++++++++--------- 4 files changed, 203 insertions(+), 45 deletions(-) diff --git a/src/mastersrv/Cargo.lock b/src/mastersrv/Cargo.lock index ee42e554b35..96712f4b5cc 100644 --- a/src/mastersrv/Cargo.lock +++ b/src/mastersrv/Cargo.lock @@ -26,6 +26,12 @@ dependencies = [ "libc", ] +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + [[package]] name = "arrayvec" version = "0.5.2" @@ -178,6 +184,12 @@ dependencies = [ "termcolor", ] +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "fnv" version = "1.0.7" @@ -268,7 +280,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap", + "indexmap 1.8.1", "slab", "tokio", "tokio-util 0.7.1", @@ -281,6 +293,12 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + [[package]] name = "headers" version = "0.3.7" @@ -426,7 +444,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.11.2", +] + +[[package]] +name = "indexmap" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +dependencies = [ + "equivalent", + "hashbrown 0.15.2", ] [[package]] @@ -490,6 +518,7 @@ dependencies = [ name = "mastersrv" version = "0.0.1" dependencies = [ + "arc-swap", "arrayvec", "base64", "bytes", @@ -497,6 +526,7 @@ dependencies = [ "env_logger", "headers", "hex", + "ipnet", "libloc", "log", "mime", @@ -506,6 +536,7 @@ dependencies = [ "sha2", "tokio", "tokio-stream", + "toml", "url", "warp", ] @@ -727,22 +758,22 @@ checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" [[package]] name = "serde" -version = "1.0.137" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.137" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2", "quote", - "syn 1.0.92", + "syn 2.0.68", ] [[package]] @@ -751,12 +782,21 @@ version = "1.0.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f972498cf015f7c0746cac89ebe1d6ef10c293b94175a243a2d9442c163d9944" dependencies = [ - "indexmap", + "indexmap 1.8.1", "itoa", "ryu", "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -791,6 +831,15 @@ dependencies = [ "digest", ] +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + [[package]] name = "slab" version = "0.4.6" @@ -907,7 +956,9 @@ dependencies = [ "memchr", "mio", "num_cpus", + "once_cell", "pin-project-lite", + "signal-hook-registry", "tokio-macros", "winapi", ] @@ -962,6 +1013,40 @@ dependencies = [ "tracing", ] +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" +dependencies = [ + "indexmap 2.6.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tower-service" version = "0.3.1" @@ -1276,6 +1361,15 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" +[[package]] +name = "winnow" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +dependencies = [ + "memchr", +] + [[package]] name = "yoke" version = "0.7.4" diff --git a/src/mastersrv/Cargo.toml b/src/mastersrv/Cargo.toml index df249c2c543..bc679c6f0f4 100644 --- a/src/mastersrv/Cargo.toml +++ b/src/mastersrv/Cargo.toml @@ -9,6 +9,7 @@ license = "Zlib" [workspace] [dependencies] +arc-swap = "1.7.1" arrayvec = { version = "0.5.2", features = ["serde"] } base64 = "0.13.0" bytes = "1.1.0" @@ -19,6 +20,7 @@ clap = { version = "2.34.0", default-features = false, features = [ env_logger = "0.8.3" headers = "0.3.7" hex = "0.4.3" +ipnet = "2.9.0" libloc = "0.1.0" log = "0.4.17" mime = "0.3.16" @@ -30,7 +32,13 @@ serde_json = { version = "1.0.64", features = [ "raw_value", ] } sha2 = "0.10.0" -tokio = { version = "1.6.0", features = ["macros", "rt", "rt-multi-thread"] } +toml = "0.8.19" +tokio = { version = "1.6.0", features = [ + "macros", + "rt", + "rt-multi-thread", + "signal", +] } tokio-stream = { version = "0.1.8", features = ["net"] } url = { version = "2.2.2", features = ["serde"] } warp = { version = "0.3.1", default-features = false } diff --git a/src/mastersrv/src/locations.rs b/src/mastersrv/src/locations.rs index 99870e13087..77bcbb8006d 100644 --- a/src/mastersrv/src/locations.rs +++ b/src/mastersrv/src/locations.rs @@ -1,4 +1,5 @@ use arrayvec::ArrayString; +use std::fmt; use std::net::IpAddr; use std::path::Path; @@ -8,22 +9,22 @@ pub type Location = ArrayString<[u8; 12]>; #[derive(Debug)] pub struct LocationsError(String); +#[derive(Default)] pub struct Locations { inner: Option, } -impl Locations { - pub fn empty() -> Locations { - Locations { - inner: None, - } +impl fmt::Display for LocationsError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0.fmt(f) } +} + +impl Locations { pub fn read(filename: &Path) -> Result { let inner = libloc::Locations::open(filename) .map_err(|e| LocationsError(format!("error opening {:?}: {}", filename, e)))?; - Ok(Locations { - inner: Some(inner), - }) + Ok(Locations { inner: Some(inner) }) } pub fn lookup(&self, addr: IpAddr) -> Option { self.inner.as_ref().and_then(|inner| { diff --git a/src/mastersrv/src/main.rs b/src/mastersrv/src/main.rs index d19f48c8488..ae7c86e9e5c 100644 --- a/src/mastersrv/src/main.rs +++ b/src/mastersrv/src/main.rs @@ -1,3 +1,4 @@ +use arc_swap::ArcSwap; use arrayvec::ArrayString; use arrayvec::ArrayVec; use clap::value_t_or_exit; @@ -44,12 +45,15 @@ extern crate log; use crate::addr::Addr; use crate::addr::Protocol; use crate::addr::RegisterAddr; +use crate::config::Config; +use crate::config::ConfigLocation; use crate::locations::Location; use crate::locations::Locations; // Naming convention: Always use the abbreviation `addr` except in user-facing // (e.g. serialized) identifiers. mod addr; +mod config; mod locations; const SERVER_TIMEOUT_SECONDS: u64 = 30; @@ -306,7 +310,7 @@ impl Challenger { struct Shared<'a> { challenger: &'a Mutex, - locations: &'a Locations, + config: &'a Config, servers: &'a Mutex, socket: &'a Arc, timekeeper: Timekeeper, @@ -587,7 +591,7 @@ async fn handle_periodic_writeout( servers.clone() }; if let Some((filename, filename_temp)) = &dump_filename { - let json = json::to_string(&Dump::new(now, &servers)).unwrap(); + let json = json::to_string(&Dump::new(now, &servers)).unwrap() + "\n"; overwrite_atomically(filename, filename_temp, json.as_bytes()) .await .unwrap(); @@ -614,7 +618,7 @@ async fn handle_periodic_writeout( } non_backcompat_addrs.sort_unstable(); non_backcompat_addrs.dedup(); - let json = json::to_string(&non_backcompat_addrs).unwrap(); + let json = json::to_string(&non_backcompat_addrs).unwrap() + "\n"; overwrite_atomically(filename, filename_temp, json.as_bytes()) .await .unwrap(); @@ -637,7 +641,7 @@ async fn handle_periodic_writeout( SerializedServer::new(s, location) })); serialized.servers.sort_by_key(|s| s.addresses); - json::to_string(&serialized).unwrap() + json::to_string(&serialized).unwrap() + "\n" }; overwrite_atomically(&servers_filename, servers_filename_temp, json.as_bytes()) .await @@ -654,6 +658,39 @@ async fn handle_periodic_writeout( } } +async fn handle_config_reread(config_location: ConfigLocation, config: Arc>) { + #[cfg(not(unix))] + { + use std::future; + + // Do nothing. Config rereading isn't implemented on non-Unix OSs. + future::pending().await + } + + #[cfg(unix)] + { + use tokio::signal::unix::signal; + use tokio::signal::unix::SignalKind; + + let mut sighup = signal(SignalKind::hangup()).unwrap(); + loop { + sighup.recv().await.unwrap(); + + // This is theoretically blocking, but it should™ be fine. + match config_location.read() { + Err(e) => { + error!("error re-reading config: {}", e); + continue; + } + Ok(new_config) => { + config.store(Arc::new(new_config)); + info!("successfully reloaded config"); + } + } + } + } +} + async fn send_challenge( connless_request_token_7: Option<[u8; 4]>, socket: Arc, @@ -699,18 +736,25 @@ fn handle_register( }; let addr = register.address.with_ip(remote_addr); - let challenge = shared.challenge_for_addr(&addr); - let correct_challenge = register - .challenge_token - .as_ref() - .map(|ct| challenge.is_valid(ct)) - .unwrap_or(false); - let should_send_challenge = register - .challenge_token - .as_ref() - .map(|ct| ct != challenge.current()) - .unwrap_or(true); + if let Some(reason) = shared.config.is_banned(addr) { + return Err(RegisterError::new(reason.into())); + } + + let is_exempt = shared.config.is_exempt_from_port_forward_check(addr); + let challenge = shared.challenge_for_addr(&addr); + let correct_challenge = is_exempt + || register + .challenge_token + .as_ref() + .map(|ct| challenge.is_valid(ct)) + .unwrap_or(false); + let should_send_challenge = !is_exempt + && register + .challenge_token + .as_ref() + .map(|ct| ct != challenge.current()) + .unwrap_or(true); let result = if correct_challenge { let raw_info = register @@ -728,7 +772,7 @@ fn handle_register( AddrInfo { kind: EntryKind::Mastersrv, ping_time: shared.timekeeper.now(), - location: shared.locations.lookup(addr.ip), + location: shared.config.locations.lookup(addr.ip), secret: register.secret, }, register.info_serial, @@ -824,7 +868,10 @@ fn register_from_headers( challenge_token: parse_opt(headers, "Challenge-Token")?, info_serial: parse(headers, "Info-Serial")?, info: if !info.is_empty() { - match headers.typed_get::().map(mime::Mime::from) { + match headers + .typed_get::() + .map(mime::Mime::from) + { Some(mime) if mime.essence_str() == mime::APPLICATION_JSON => {} _ => return Err(RegisterError::unsupported_media_type()), } @@ -900,6 +947,12 @@ async fn main() { .long("locations") .value_name("LOCATIONS") .help("IP to continent locations database filename (libloc format, can be obtained from https://location.ipfire.org/databases/1/location.db.xz).") + .conflicts_with("config") + ) + .arg(Arg::with_name("config") + .long("config") + .value_name("CONFIG") + .help("TOML config (can be re-read using SIGHUP signal)") ) .arg(Arg::with_name("write-addresses") .long("write-addresses") @@ -953,17 +1006,17 @@ async fn main() { None }; let read_write_dump = matches.value_of("read-write-dump").map(|s| s.to_owned()); + let config_filename = matches.value_of("config"); + let config_location = match (config_filename, matches.value_of("locations")) { + (None, None) => ConfigLocation::None, + (None, Some(l)) => ConfigLocation::LocationsFileParameter(l.into()), + (Some(f), None) => ConfigLocation::File(f.into()), + (Some(_), Some(_)) => unreachable!(), + }; + let config = Arc::new(ArcSwap::from_pointee(config_location.read().unwrap())); let timekeeper = Timekeeper::new(); let challenger = Arc::new(Mutex::new(Challenger::new())); - let locations = Arc::new( - matches - .value_of("locations") - .map(|l| Locations::read(Path::new(&l))) - .transpose() - .unwrap() - .unwrap_or_else(Locations::empty), - ); let mut servers = Servers::new(); match &read_write_dump { Some(path) => match read_dump(Path::new(&path), timekeeper).await { @@ -994,6 +1047,7 @@ async fn main() { matches.value_of("out").unwrap().to_owned(), timekeeper, )); + let task_reread = tokio::spawn(handle_config_reread(config_location, config.clone())); let connecting_addr = move |addr: Option, headers: &warp::http::HeaderMap| @@ -1044,9 +1098,10 @@ async fn main() { .map( move |headers: warp::http::HeaderMap, addr: Option, info: bytes::Bytes| { build_response(|| { + let config = config.load(); let shared = Shared { challenger: &challenger, - locations: &locations, + config: &config, servers: &servers, socket: &socket.0, timekeeper, @@ -1089,8 +1144,8 @@ async fn main() { tokio::spawn(server.run(listen_address)) }; - match tokio::try_join!(task_reseed, task_writeout, task_server) { - Ok(((), (), ())) => unreachable!(), + match tokio::try_join!(task_reseed, task_writeout, task_reread, task_server) { + Ok(((), (), (), ())) => unreachable!(), Err(e) => panic::resume_unwind(e.into_panic()), } }