mirror of
https://git.mentality.rip/numas13/xash3d-master.git
synced 2025-01-22 04:44:31 +00:00
master: Admin rate limit
This commit is contained in:
parent
a9980ea314
commit
09e41ac2f3
@ -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(())
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -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));
|
||||
|
@ -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));
|
||||
|
Loading…
x
Reference in New Issue
Block a user