master: Admin rate limit

This commit is contained in:
Denis Drakhnia 2023-10-18 08:41:14 +03:00
parent a9980ea314
commit 09e41ac2f3
6 changed files with 88 additions and 23 deletions

View File

@ -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(())

View File

@ -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

View File

@ -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
}

View File

@ -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<str>,
update_addr: SocketAddrV4,
admin_challenges: HashMap<Ipv4Addr, Entry<u32>>,
admin_challenges: HashMap<Ipv4Addr, Entry<(u32, u32)>>,
admin_challenges_counter: Counter,
admin_list: Box<[config::AdminConfig]>,
// rate limit if hash is invalid
admin_limit: HashMap<Ipv4Addr, Entry<()>>,
admin_limit_counter: Counter,
hash: config::HashConfig,
blocklist: HashSet<Ipv4Addr>,
@ -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),

View File

@ -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<Self, Error> {
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<usize, Error> {
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));

View File

@ -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<Self, Error> {
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<usize, Error> {
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));