From 09e41ac2f3ee2c96af511a8a46d9bb483b73d52d Mon Sep 17 00:00:00 2001 From: Denis Drakhnia Date: Wed, 18 Oct 2023 08:41:14 +0300 Subject: [PATCH] master: Admin rate limit --- admin/src/main.rs | 9 +++--- master/config/main.toml | 2 ++ master/src/config.rs | 8 ++++++ master/src/master_server.rs | 55 +++++++++++++++++++++++++++++++------ protocol/src/admin.rs | 14 ++++++++-- protocol/src/master.rs | 23 +++++++++++----- 6 files changed, 88 insertions(+), 23 deletions(-) diff --git a/admin/src/main.rs b/admin/src/main.rs index 3740980..41f7ee7 100644 --- a/admin/src/main.rs +++ b/admin/src/main.rs @@ -30,8 +30,8 @@ fn send_command(cli: &cli::Cli) -> Result<(), Error> { sock.send(&buf[..n])?; let n = sock.recv(&mut buf)?; - let challenge = match master::Packet::decode(&buf[..n])? { - master::Packet::AdminChallengeResponse(p) => p.challenge, + let (master_challenge, hash_challenge) = match master::Packet::decode(&buf[..n])? { + master::Packet::AdminChallengeResponse(p) => (p.master_challenge, p.hash_challenge), _ => return Err(Error::UnexpectedPacket), }; @@ -54,10 +54,11 @@ fn send_command(cli: &cli::Cli) -> Result<(), Error> { .personal(cli.hash_personal.as_bytes()) .to_state() .update(password.as_bytes()) - .update(&challenge.to_le_bytes()) + .update(&hash_challenge.to_le_bytes()) .finalize(); - let n = admin::AdminCommand::new(hash.as_bytes(), &cli.command).encode(&mut buf)?; + let n = admin::AdminCommand::new(master_challenge, hash.as_bytes(), &cli.command) + .encode(&mut buf)?; sock.send(&buf[..n])?; Ok(()) diff --git a/master/config/main.toml b/master/config/main.toml index d12cca3..37b3a99 100644 --- a/master/config/main.toml +++ b/master/config/main.toml @@ -13,6 +13,8 @@ port = 27010 challenge = 300 # Time in seconds while server is valid server = 300 +# TIme in seconds before next admin request is allowed after wrong password +admin = 10 [client] # If client version is less then show update message diff --git a/master/src/config.rs b/master/src/config.rs index 47b8e64..5ca6641 100644 --- a/master/src/config.rs +++ b/master/src/config.rs @@ -17,6 +17,7 @@ pub const DEFAULT_CONFIG_PATH: &str = "config/main.toml"; pub const DEFAULT_MASTER_SERVER_IP: IpAddr = IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)); pub const DEFAULT_MASTER_SERVER_PORT: u16 = 27010; pub const DEFAULT_TIMEOUT: u32 = 300; +pub const DEFAULT_ADMIN_TIMEOUT: u32 = 10; #[derive(Debug, Error)] pub enum Error { @@ -86,6 +87,8 @@ pub struct TimeoutConfig { pub challenge: u32, #[serde(default = "default_timeout")] pub server: u32, + #[serde(default = "default_admin_timeout")] + pub admin: u32, } impl Default for TimeoutConfig { @@ -93,6 +96,7 @@ impl Default for TimeoutConfig { Self { challenge: default_timeout(), server: default_timeout(), + admin: default_admin_timeout(), } } } @@ -145,6 +149,10 @@ fn default_timeout() -> u32 { DEFAULT_TIMEOUT } +fn default_admin_timeout() -> u32 { + DEFAULT_ADMIN_TIMEOUT +} + fn default_hash_len() -> usize { admin::HASH_LEN } diff --git a/master/src/master_server.rs b/master/src/master_server.rs index 37f0165..91b4dc3 100644 --- a/master/src/master_server.rs +++ b/master/src/master_server.rs @@ -29,6 +29,9 @@ const CHALLENGE_CLEANUP_MAX: usize = 100; /// How many cleanup calls should be skipped before removing outdated admin challenges. const ADMIN_CHALLENGE_CLEANUP_MAX: usize = 100; +/// How many cleanup calls should be skipped before removing outdated admin limit entries +const ADMIN_LIMIT_CLEANUP_MAX: usize = 100; + #[derive(Error, Debug)] pub enum Error { #[error("Failed to bind server socket: {0}")] @@ -109,9 +112,12 @@ struct MasterServer { update_map: Box, update_addr: SocketAddrV4, - admin_challenges: HashMap>, + admin_challenges: HashMap>, admin_challenges_counter: Counter, admin_list: Box<[config::AdminConfig]>, + // rate limit if hash is invalid + admin_limit: HashMap>, + admin_limit_counter: Counter, hash: config::HashConfig, blocklist: HashSet, @@ -146,6 +152,8 @@ impl MasterServer { admin_challenges: Default::default(), admin_challenges_counter: Counter::new(ADMIN_CHALLENGE_CLEANUP_MAX), admin_list: cfg.admin_list, + admin_limit: Default::default(), + admin_limit_counter: Counter::new(ADMIN_LIMIT_CLEANUP_MAX), hash: cfg.hash, blocklist: Default::default(), }) @@ -255,13 +263,21 @@ impl MasterServer { } if let Ok(p) = admin::Packet::decode(self.hash.len, src) { - // TODO: throttle + let now = self.now(); + + if let Some(e) = self.admin_limit.get(from.ip()) { + if e.is_valid(now, self.timeout.admin) { + trace!("{}: rate limit", from); + return Ok(()); + } + } + match p { admin::Packet::AdminChallenge(p) => { trace!("{}: recv {:?}", from, p); - let challenge = self.admin_challenge_add(from); + let (master_challenge, hash_challenge) = self.admin_challenge_add(from); - let p = master::AdminChallengeResponse::new(challenge); + let p = master::AdminChallengeResponse::new(master_challenge, hash_challenge); trace!("{}: send {:?}", from, p); let mut buf = [0; 64]; let n = p.encode(&mut buf)?; @@ -271,11 +287,21 @@ impl MasterServer { } admin::Packet::AdminCommand(p) => { trace!("{}: recv {:?}", from, p); - let challenge = *self + let entry = *self .admin_challenges .get(from.ip()) .ok_or(Error::AdminChallengeNotFound)?; + if entry.0 != p.master_challenge { + trace!("{}: master challenge is not valid", from); + return Ok(()); + } + + if !entry.is_valid(now, self.timeout.challenge) { + trace!("{}: challenge is outdated", from); + return Ok(()); + } + let state = Params::new() .hash_length(self.hash.len) .key(self.hash.key.as_bytes()) @@ -286,7 +312,7 @@ impl MasterServer { let hash = state .clone() .update(i.password.as_bytes()) - .update(&challenge.to_le_bytes()) + .update(&entry.1.to_le_bytes()) .finalize(); *p.hash == hash.as_bytes() }); @@ -299,6 +325,8 @@ impl MasterServer { } None => { warn!("{}: invalid admin hash, command: {:?}", from, p.command); + self.admin_limit.insert(*from.ip(), Entry::new(now, ())); + self.admin_limit_cleanup(); } } } @@ -327,11 +355,12 @@ impl MasterServer { } } - fn admin_challenge_add(&mut self, addr: SocketAddrV4) -> u32 { + fn admin_challenge_add(&mut self, addr: SocketAddrV4) -> (u32, u32) { let x = self.rng.u32(..); - let entry = Entry::new(self.now(), x); + let y = self.rng.u32(..); + let entry = Entry::new(self.now(), (x, y)); self.admin_challenges.insert(*addr.ip(), entry); - x + (x, y) } fn admin_challenge_remove(&mut self, addr: SocketAddrV4) { @@ -347,6 +376,14 @@ impl MasterServer { } } + fn admin_limit_cleanup(&mut self) { + if self.admin_limit_counter.next() { + let now = self.now(); + self.admin_limit + .retain(|_, v| v.is_valid(now, self.timeout.admin)); + } + } + fn add_server(&mut self, addr: SocketAddrV4, server: ServerInfo) { match self.servers.insert(addr, Entry::new(self.now(), server)) { Some(_) => trace!("{}: Updated GameServer", addr), diff --git a/protocol/src/admin.rs b/protocol/src/admin.rs index 4dd88b0..614ffb2 100644 --- a/protocol/src/admin.rs +++ b/protocol/src/admin.rs @@ -30,6 +30,7 @@ impl AdminChallenge { #[derive(Clone, Debug, PartialEq)] pub struct AdminCommand<'a> { + pub master_challenge: u32, pub hash: Hide<&'a [u8]>, pub command: &'a str, } @@ -37,8 +38,9 @@ pub struct AdminCommand<'a> { impl<'a> AdminCommand<'a> { pub const HEADER: &'static [u8] = b"admin"; - pub fn new(hash: &'a [u8], command: &'a str) -> Self { + pub fn new(master_challenge: u32, hash: &'a [u8], command: &'a str) -> Self { Self { + master_challenge, hash: Hide(hash), command, } @@ -47,10 +49,15 @@ impl<'a> AdminCommand<'a> { pub fn decode_with_hash_len(hash_len: usize, src: &'a [u8]) -> Result { let mut cur = Cursor::new(src); cur.expect(Self::HEADER)?; + let master_challenge = cur.get_u32_le()?; let hash = Hide(cur.get_bytes(hash_len)?); let command = cur.get_str(cur.remaining())?; cur.expect_empty()?; - Ok(Self { hash, command }) + Ok(Self { + master_challenge, + hash, + command, + }) } #[inline] @@ -61,6 +68,7 @@ impl<'a> AdminCommand<'a> { pub fn encode(&self, buf: &mut [u8]) -> Result { Ok(CursorMut::new(buf) .put_bytes(Self::HEADER)? + .put_u32_le(self.master_challenge)? .put_bytes(&self.hash)? .put_str(self.command)? .pos()) @@ -101,7 +109,7 @@ mod tests { #[test] fn admin_command() { - let p = AdminCommand::new(&[1; HASH_LEN], "foo bar baz"); + let p = AdminCommand::new(0x12345678, &[1; HASH_LEN], "foo bar baz"); let mut buf = [0; 512]; let n = p.encode(&mut buf).unwrap(); assert_eq!(AdminCommand::decode(&buf[..n]), Ok(p)); diff --git a/protocol/src/master.rs b/protocol/src/master.rs index 86787ba..9272f7e 100644 --- a/protocol/src/master.rs +++ b/protocol/src/master.rs @@ -107,28 +107,37 @@ where #[derive(Clone, Debug, PartialEq)] pub struct AdminChallengeResponse { - pub challenge: u32, + pub master_challenge: u32, + pub hash_challenge: u32, } impl AdminChallengeResponse { pub const HEADER: &'static [u8] = b"\xff\xff\xff\xffadminchallenge"; - pub fn new(challenge: u32) -> Self { - Self { challenge } + pub fn new(master_challenge: u32, hash_challenge: u32) -> Self { + Self { + master_challenge, + hash_challenge, + } } pub fn decode(src: &[u8]) -> Result { let mut cur = Cursor::new(src); cur.expect(Self::HEADER)?; - let challenge = cur.get_u32_le()?; + let master_challenge = cur.get_u32_le()?; + let hash_challenge = cur.get_u32_le()?; cur.expect_empty()?; - Ok(Self { challenge }) + Ok(Self { + master_challenge, + hash_challenge, + }) } pub fn encode(&self, buf: &mut [u8]) -> Result { Ok(CursorMut::new(buf) .put_bytes(Self::HEADER)? - .put_u32_le(self.challenge)? + .put_u32_le(self.master_challenge)? + .put_u32_le(self.hash_challenge)? .pos()) } } @@ -187,7 +196,7 @@ mod tests { #[test] fn admin_challenge_response() { - let p = AdminChallengeResponse::new(0x12345678); + let p = AdminChallengeResponse::new(0x12345678, 0x87654321); let mut buf = [0; 64]; let n = p.encode(&mut buf).unwrap(); assert_eq!(AdminChallengeResponse::decode(&buf[..n]), Ok(p));