Skip to content

Commit df766ec

Browse files
numas13a1batross
authored andcommitted
master: add client rate limit config
1 parent 04c34a7 commit df766ec

File tree

7 files changed

+252
-108
lines changed

7 files changed

+252
-108
lines changed

master/config/default.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@
1414
# Minimal accepted server version
1515
#min-version = "0.19.2"
1616

17+
# Set maximum allowed client requests from an IP address per second.
18+
#
19+
# Set 0 to disable.
20+
#client-rate-limit = 0
21+
1722
[server.timeout]
1823
# Time in seconds while challenge is valid
1924
#challenge = 10

master/src/config.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ pub struct ServerConfig {
9494
#[serde(deserialize_with = "deserialize_version")]
9595
pub min_version: Version,
9696
pub timeout: TimeoutConfig,
97+
pub client_rate_limit: u32,
9798
}
9899

99100
impl Default for ServerConfig {
@@ -104,6 +105,7 @@ impl Default for ServerConfig {
104105
max_servers_per_ip: DEFAULT_MAX_SERVERS_PER_IP,
105106
min_version: DEFAULT_SERVER_MIN_VERSION,
106107
timeout: Default::default(),
108+
client_rate_limit: 0,
107109
}
108110
}
109111
}

master/src/hash_map.rs

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
use std::{borrow::Borrow, collections::hash_map::Entry, hash::Hash, ops::Deref};
1+
use std::{
2+
borrow::Borrow,
3+
collections::hash_map::Entry,
4+
hash::Hash,
5+
ops::{Deref, DerefMut},
6+
};
27

38
use ahash::AHashMap;
49

@@ -24,6 +29,12 @@ impl<T> Timed<T> {
2429
}
2530
}
2631

32+
impl<T: Default> Default for Timed<T> {
33+
fn default() -> Self {
34+
Self::new(T::default())
35+
}
36+
}
37+
2738
impl<T> Deref for Timed<T> {
2839
type Target = T;
2940

@@ -32,6 +43,12 @@ impl<T> Deref for Timed<T> {
3243
}
3344
}
3445

46+
impl<T> DerefMut for Timed<T> {
47+
fn deref_mut(&mut self) -> &mut Self::Target {
48+
&mut self.value
49+
}
50+
}
51+
3552
impl<T> From<T> for Timed<T> {
3653
fn from(value: T) -> Self {
3754
Self::new(value)
@@ -85,29 +102,40 @@ impl<K: Eq + Hash, V> TimedHashMap<K, V> {
85102
self.cleanup();
86103
}
87104

88-
pub fn remove<Q>(&mut self, k: &Q) -> Option<V>
105+
pub fn remove<Q>(&mut self, k: &Q) -> Option<Timed<V>>
89106
where
90107
K: Borrow<Q>,
91108
Q: Hash + Eq + ?Sized,
92109
{
93110
self.map.remove(k).and_then(|i| {
94111
if i.is_valid(RelativeTime::now(), self.timeout) {
95-
Some(i.value)
112+
Some(i)
96113
} else {
97114
None
98115
}
99116
})
100117
}
101118

102119
pub fn entry(&mut self, key: K) -> Entry<'_, K, Timed<V>> {
120+
// not optimal but HashMap::entry API does not allow to replace
121+
// an occupied entry with vacant
122+
if let Some(v) = self.map.get(&key) {
123+
// try to remove the requested outdated entry
124+
if !v.is_valid(RelativeTime::now(), self.timeout) {
125+
self.map.remove(&key);
126+
}
127+
} else {
128+
// or try to remove other outdated entries
129+
self.cleanup();
130+
}
103131
self.map.entry(key)
104132
}
105133

106-
pub fn iter(&self) -> impl Iterator<Item = (&K, &V)> {
134+
pub fn iter(&self) -> impl Iterator<Item = (&K, &Timed<V>)> {
107135
let now = RelativeTime::now();
108136
self.map.iter().filter_map(move |(k, i)| {
109137
if i.is_valid(now, self.timeout) {
110-
Some((k, &i.value))
138+
Some((k, i))
111139
} else {
112140
None
113141
}
@@ -118,14 +146,14 @@ impl<K: Eq + Hash, V> TimedHashMap<K, V> {
118146
self.iter().map(|(k, _)| k)
119147
}
120148

121-
pub fn get<Q>(&self, k: &Q) -> Option<&V>
149+
pub fn get<Q>(&self, k: &Q) -> Option<&Timed<V>>
122150
where
123151
K: Borrow<Q>,
124152
Q: Hash + Eq + ?Sized,
125153
{
126154
self.map.get(k).and_then(|i| {
127155
if i.is_valid(RelativeTime::now(), self.timeout) {
128-
Some(&i.value)
156+
Some(i)
129157
} else {
130158
None
131159
}

master/src/master_server.rs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ pub struct MasterServer<Addr: AddrExt> {
190190

191191
update_addr: SocketAddr,
192192
update_gamedir: TimedHashMap<Addr, StrArr<GAMEDIR_MAX_SIZE>>,
193+
client_rate_limit: TimedHashMap<Addr::Ip, u32>,
193194

194195
blocklist: HashSet<Addr::Ip>,
195196

@@ -218,6 +219,7 @@ impl<Addr: AddrExt> MasterServer<Addr> {
218219
rng: Rng::new(),
219220
update_addr,
220221
update_gamedir: TimedHashMap::new(5),
222+
client_rate_limit: TimedHashMap::new(1),
221223
admin_challenges: TimedHashMap::new(timeout.challenge),
222224
admin_limit: TimedHashMap::new(timeout.admin),
223225
blocklist: Default::default(),
@@ -294,6 +296,7 @@ impl<Addr: AddrExt> MasterServer<Addr> {
294296
self.servers.clear();
295297
self.admin_challenges.clear();
296298
self.stats.clear();
299+
self.client_rate_limit.clear();
297300
}
298301

299302
fn handle_server_packet(&mut self, from: Addr, p: server::Packet) -> Result<(), Error> {
@@ -310,7 +313,7 @@ impl<Addr: AddrExt> MasterServer<Addr> {
310313
}
311314
server::Packet::ServerAdd(p) => {
312315
let challenge = match self.challenges.get(&from) {
313-
Some(e) => e,
316+
Some(e) => e.value,
314317
None => {
315318
trace!("{}: Challenge does not exists", from);
316319
return Ok(());
@@ -324,7 +327,7 @@ impl<Addr: AddrExt> MasterServer<Addr> {
324327
);
325328
return Ok(());
326329
}
327-
if p.challenge != *challenge {
330+
if p.challenge != challenge {
328331
warn!(
329332
"{from}: Expected challenge {challenge} but received {}",
330333
p.challenge
@@ -484,6 +487,15 @@ impl<Addr: AddrExt> MasterServer<Addr> {
484487
}
485488

486489
fn handle_game_packet(&mut self, from: Addr, p: game::Packet) -> Result<(), Error> {
490+
if self.cfg.server.client_rate_limit > 0 {
491+
let counter = self.client_rate_limit.entry(*from.ip()).or_default();
492+
counter.value = counter.value.saturating_add(1);
493+
if counter.value > self.cfg.server.client_rate_limit {
494+
trace!("{from}: client rate limit {}", counter.value);
495+
return Ok(());
496+
}
497+
}
498+
487499
trace!("{from}: recv {p:?}");
488500
match p {
489501
game::Packet::QueryServers(p) => {
@@ -608,15 +620,15 @@ impl<Addr: AddrExt> MasterServer<Addr> {
608620
fn add_server(&mut self, addr: Addr, server: ServerInfo) {
609621
match self.servers.entry(addr) {
610622
hash_map::Entry::Occupied(mut e) => {
611-
trace!("{}: Updated GameServer", addr);
623+
trace!("{}: game server update", addr);
612624
e.insert(Timed::new(server));
613625
}
614626
hash_map::Entry::Vacant(_) => {
615627
if self.count_servers(addr.ip()) >= self.cfg.server.max_servers_per_ip {
616628
trace!("{}: max servers per ip", addr);
617629
return;
618630
}
619-
trace!("{}: New GameServer", addr);
631+
trace!("{}: game server add", addr);
620632
self.servers.insert(addr, server);
621633
}
622634
}

master/src/time.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,28 @@ use std::{
99
pub struct RelativeTime(u32);
1010

1111
impl RelativeTime {
12+
// 125ms units provide ~17 years of uptime without overflow
13+
const MILLIS_PER_UNIT: u64 = 125;
14+
const UNITS_PER_SEC: u64 = 1000 / Self::MILLIS_PER_UNIT; // 8
15+
16+
fn from_millis(ms: u64) -> Self {
17+
Self((ms / Self::UNITS_PER_SEC) as u32)
18+
}
19+
20+
fn as_millis(&self) -> u64 {
21+
(self.0 as u64) * Self::UNITS_PER_SEC
22+
}
23+
1224
/// Returns current relative time.
1325
pub fn now() -> Self {
1426
static TIMER: OnceLock<Instant> = OnceLock::new();
15-
Self(TIMER.get_or_init(Instant::now).elapsed().as_secs() as u32)
27+
Self::from_millis(TIMER.get_or_init(Instant::now).elapsed().as_millis() as u64)
1628
}
1729

1830
/// Returns the amount of time elapsed from another time to this one.
1931
///
2032
/// Returns zero if `earlier` is later than this time.
2133
pub fn duration_since(&self, earlier: Self) -> Duration {
22-
Duration::from_secs(self.0.saturating_sub(earlier.0) as u64)
34+
Duration::from_millis(self.as_millis().saturating_sub(earlier.as_millis()))
2335
}
2436
}

0 commit comments

Comments
 (0)