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()), } }