Browse Source

master: Admin ban and unban commands

ipv6
Denis Drakhnia 1 year ago
parent
commit
a9980ea314
  1. 1
      Cargo.lock
  2. 2
      admin/src/main.rs
  3. 1
      master/Cargo.toml
  4. 13
      master/config/main.toml
  5. 36
      master/src/config.rs
  6. 188
      master/src/master_server.rs
  7. 14
      protocol/src/admin.rs

1
Cargo.lock generated

@ -461,6 +461,7 @@ name = "xash3d-master"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"bitflags 2.4.0", "bitflags 2.4.0",
"blake2b_simd",
"chrono", "chrono",
"fastrand", "fastrand",
"getopts", "getopts",

2
admin/src/main.rs

@ -6,8 +6,8 @@ mod cli;
use std::io::{self, Write}; use std::io::{self, Write};
use std::net::UdpSocket; use std::net::UdpSocket;
use termion::input::TermRead;
use blake2b_simd::Params; use blake2b_simd::Params;
use termion::input::TermRead;
use thiserror::Error; use thiserror::Error;
use xash3d_protocol::{admin, master}; use xash3d_protocol::{admin, master};

1
master/Cargo.toml

@ -18,6 +18,7 @@ bitflags = "2.4"
fastrand = "2.0.1" fastrand = "2.0.1"
serde = { version = "1.0.188", features = ["derive"] } serde = { version = "1.0.188", features = ["derive"] }
toml = "0.5.11" toml = "0.5.11"
blake2b_simd = "<0.6"
xash3d-protocol = { path = "../protocol", version = "0.1.0" } xash3d-protocol = { path = "../protocol", version = "0.1.0" }
[dependencies.chrono] [dependencies.chrono]

13
master/config/main.toml

@ -20,3 +20,16 @@ version = "0.20"
update_title = "https://github.com/FWGS/xash3d-fwgs" update_title = "https://github.com/FWGS/xash3d-fwgs"
update_map = "Update please" update_map = "Update please"
update_addr = "127.0.0.1:27010" update_addr = "127.0.0.1:27010"
[hash]
len = 64
key = "Half-Life"
personal = "Freeman"
#[[admin]]
#name = "gordon"
#password = "crowbar"
#[[admin]]
#name = "gman"
#password = "time2choose"

36
master/src/config.rs

@ -9,6 +9,7 @@ use std::path::Path;
use log::LevelFilter; use log::LevelFilter;
use serde::{de::Error as _, Deserialize, Deserializer}; use serde::{de::Error as _, Deserialize, Deserializer};
use thiserror::Error; use thiserror::Error;
use xash3d_protocol::admin;
use xash3d_protocol::filter::Version; use xash3d_protocol::filter::Version;
pub const DEFAULT_CONFIG_PATH: &str = "config/main.toml"; pub const DEFAULT_CONFIG_PATH: &str = "config/main.toml";
@ -34,6 +35,11 @@ pub struct Config {
pub server: ServerConfig, pub server: ServerConfig,
#[serde(default)] #[serde(default)]
pub client: ClientConfig, pub client: ClientConfig,
#[serde(default)]
pub hash: HashConfig,
#[serde(rename = "admin")]
#[serde(default)]
pub admin_list: Box<[AdminConfig]>,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
@ -105,6 +111,24 @@ pub struct ClientConfig {
pub update_addr: Option<SocketAddrV4>, pub update_addr: Option<SocketAddrV4>,
} }
#[derive(Deserialize, Default, Debug)]
#[serde(deny_unknown_fields)]
pub struct HashConfig {
#[serde(default = "default_hash_len")]
pub len: usize,
#[serde(default = "default_hash_key")]
pub key: Box<str>,
#[serde(default = "default_hash_personal")]
pub personal: Box<str>,
}
#[derive(Deserialize, Default, Debug)]
#[serde(deny_unknown_fields)]
pub struct AdminConfig {
pub name: Box<str>,
pub password: Box<str>,
}
fn default_log_level() -> LevelFilter { fn default_log_level() -> LevelFilter {
LevelFilter::Warn LevelFilter::Warn
} }
@ -121,6 +145,18 @@ fn default_timeout() -> u32 {
DEFAULT_TIMEOUT DEFAULT_TIMEOUT
} }
fn default_hash_len() -> usize {
admin::HASH_LEN
}
fn default_hash_key() -> Box<str> {
Box::from(admin::HASH_KEY)
}
fn default_hash_personal() -> Box<str> {
Box::from(admin::HASH_PERSONAL)
}
fn deserialize_log_level<'de, D>(de: D) -> Result<LevelFilter, D::Error> fn deserialize_log_level<'de, D>(de: D) -> Result<LevelFilter, D::Error>
where where
D: Deserializer<'de>, D: Deserializer<'de>,

188
master/src/master_server.rs

@ -1,18 +1,19 @@
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
// SPDX-FileCopyrightText: 2023 Denis Drakhnia <numas13@gmail.com> // SPDX-FileCopyrightText: 2023 Denis Drakhnia <numas13@gmail.com>
use std::collections::HashMap; use std::collections::{HashMap, HashSet};
use std::io; use std::io;
use std::net::{SocketAddr, SocketAddrV4, ToSocketAddrs, UdpSocket}; use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4, ToSocketAddrs, UdpSocket};
use std::ops::Deref; use std::ops::Deref;
use std::time::Instant; use std::time::Instant;
use blake2b_simd::Params;
use fastrand::Rng; use fastrand::Rng;
use log::{error, info, trace, warn}; use log::{error, info, trace, warn};
use thiserror::Error; use thiserror::Error;
use xash3d_protocol::filter::{Filter, Version}; use xash3d_protocol::filter::{Filter, Version};
use xash3d_protocol::server::Region; use xash3d_protocol::server::Region;
use xash3d_protocol::{ServerInfo, admin, game, master, server, Error as ProtocolError}; use xash3d_protocol::{admin, game, master, server, Error as ProtocolError, ServerInfo};
use crate::config::{self, Config}; use crate::config::{self, Config};
@ -25,6 +26,9 @@ const SERVER_CLEANUP_MAX: usize = 100;
/// How many cleanup calls should be skipped before removing outdated challenges. /// How many cleanup calls should be skipped before removing outdated challenges.
const CHALLENGE_CLEANUP_MAX: usize = 100; 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;
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum Error { pub enum Error {
#[error("Failed to bind server socket: {0}")] #[error("Failed to bind server socket: {0}")]
@ -33,10 +37,12 @@ pub enum Error {
Protocol(#[from] ProtocolError), Protocol(#[from] ProtocolError),
#[error(transparent)] #[error(transparent)]
Io(#[from] io::Error), Io(#[from] io::Error),
#[error("Admin challenge do not exist")]
AdminChallengeNotFound,
} }
/// HashMap entry to keep tracking creation time. /// HashMap entry to keep tracking creation time.
#[derive(Clone, Debug)] #[derive(Copy, Clone, Debug)]
struct Entry<T> { struct Entry<T> {
time: u32, time: u32,
value: T, value: T,
@ -66,21 +72,49 @@ impl<T> Deref for Entry<T> {
} }
} }
struct Counter {
max: usize,
cur: usize,
}
impl Counter {
fn new(max: usize) -> Self {
Self { max, cur: 0 }
}
fn next(&mut self) -> bool {
if self.cur <= self.max {
self.cur += 1;
false
} else {
self.cur = 0;
true
}
}
}
struct MasterServer { struct MasterServer {
sock: UdpSocket, sock: UdpSocket,
challenges: HashMap<SocketAddrV4, Entry<u32>>, challenges: HashMap<SocketAddrV4, Entry<u32>>,
challenges_counter: Counter,
servers: HashMap<SocketAddrV4, Entry<ServerInfo>>, servers: HashMap<SocketAddrV4, Entry<ServerInfo>>,
servers_counter: Counter,
rng: Rng, rng: Rng,
start_time: Instant, start_time: Instant,
cleanup_challenges: usize,
cleanup_servers: usize,
timeout: config::TimeoutConfig, timeout: config::TimeoutConfig,
clver: Version, clver: Version,
update_title: Box<str>, update_title: Box<str>,
update_map: Box<str>, update_map: Box<str>,
update_addr: SocketAddrV4, update_addr: SocketAddrV4,
admin_challenges: HashMap<Ipv4Addr, Entry<u32>>,
admin_challenges_counter: Counter,
admin_list: Box<[config::AdminConfig]>,
hash: config::HashConfig,
blocklist: HashSet<Ipv4Addr>,
} }
impl MasterServer { impl MasterServer {
@ -100,15 +134,20 @@ impl MasterServer {
sock, sock,
start_time: Instant::now(), start_time: Instant::now(),
challenges: Default::default(), challenges: Default::default(),
challenges_counter: Counter::new(CHALLENGE_CLEANUP_MAX),
servers: Default::default(), servers: Default::default(),
servers_counter: Counter::new(SERVER_CLEANUP_MAX),
rng: Rng::new(), rng: Rng::new(),
cleanup_challenges: 0,
cleanup_servers: 0,
timeout: cfg.server.timeout, timeout: cfg.server.timeout,
clver: cfg.client.version, clver: cfg.client.version,
update_title: cfg.client.update_title, update_title: cfg.client.update_title,
update_map: cfg.client.update_map, update_map: cfg.client.update_map,
update_addr, update_addr,
admin_challenges: Default::default(),
admin_challenges_counter: Counter::new(ADMIN_CHALLENGE_CLEANUP_MAX),
admin_list: cfg.admin_list,
hash: cfg.hash,
blocklist: Default::default(),
}) })
} }
@ -116,6 +155,7 @@ impl MasterServer {
let mut buf = [0; MAX_PACKET_SIZE]; let mut buf = [0; MAX_PACKET_SIZE];
loop { loop {
let (n, from) = self.sock.recv_from(&mut buf)?; let (n, from) = self.sock.recv_from(&mut buf)?;
let from = match from { let from = match from {
SocketAddr::V4(a) => a, SocketAddr::V4(a) => a,
_ => { _ => {
@ -131,6 +171,10 @@ impl MasterServer {
} }
fn handle_packet(&mut self, from: SocketAddrV4, src: &[u8]) -> Result<(), Error> { fn handle_packet(&mut self, from: SocketAddrV4, src: &[u8]) -> Result<(), Error> {
if self.is_blocked(from.ip()) {
return Ok(());
}
if let Ok(p) = server::Packet::decode(src) { if let Ok(p) = server::Packet::decode(src) {
match p { match p {
server::Packet::Challenge(p) => { server::Packet::Challenge(p) => {
@ -210,18 +254,53 @@ impl MasterServer {
} }
} }
if let Ok(p) = admin::Packet::decode(src) { if let Ok(p) = admin::Packet::decode(self.hash.len, src) {
// TODO: throttle
match p { match p {
admin::Packet::AdminChallenge(p) => { admin::Packet::AdminChallenge(p) => {
trace!("{}: recv {:?}", from, p); trace!("{}: recv {:?}", from, p);
let challenge = 0x12345678; // TODO: let challenge = self.admin_challenge_add(from);
let p = master::AdminChallengeResponse::new(challenge); let p = master::AdminChallengeResponse::new(challenge);
trace!("{}: send {:?}", from, p);
let mut buf = [0; 64]; let mut buf = [0; 64];
let n = p.encode(&mut buf)?; let n = p.encode(&mut buf)?;
self.sock.send_to(&buf[..n], from)?; self.sock.send_to(&buf[..n], from)?;
self.admin_challenges_cleanup();
} }
admin::Packet::AdminCommand(p) => { admin::Packet::AdminCommand(p) => {
trace!("{}: recv {:?}", from, p); trace!("{}: recv {:?}", from, p);
let challenge = *self
.admin_challenges
.get(from.ip())
.ok_or(Error::AdminChallengeNotFound)?;
let state = Params::new()
.hash_length(self.hash.len)
.key(self.hash.key.as_bytes())
.personal(self.hash.personal.as_bytes())
.to_state();
let admin = self.admin_list.iter().find(|i| {
let hash = state
.clone()
.update(i.password.as_bytes())
.update(&challenge.to_le_bytes())
.finalize();
*p.hash == hash.as_bytes()
});
match admin {
Some(admin) => {
info!("{}: admin({}), command: {:?}", from, &admin.name, p.command);
self.admin_command(p.command);
self.admin_challenge_remove(from);
}
None => {
warn!("{}: invalid admin hash, command: {:?}", from, p.command);
}
}
} }
} }
} }
@ -241,19 +320,31 @@ impl MasterServer {
} }
fn remove_outdated_challenges(&mut self) { fn remove_outdated_challenges(&mut self) {
if self.cleanup_challenges < CHALLENGE_CLEANUP_MAX { if self.challenges_counter.next() {
self.cleanup_challenges += 1; let now = self.now();
return; self.challenges
.retain(|_, v| v.is_valid(now, self.timeout.challenge));
} }
let now = self.now(); }
let old = self.challenges.len();
self.challenges fn admin_challenge_add(&mut self, addr: SocketAddrV4) -> u32 {
.retain(|_, v| v.is_valid(now, self.timeout.challenge)); let x = self.rng.u32(..);
let new = self.challenges.len(); let entry = Entry::new(self.now(), x);
if old != new { self.admin_challenges.insert(*addr.ip(), entry);
trace!("Removed {} outdated challenges", old - new); x
}
fn admin_challenge_remove(&mut self, addr: SocketAddrV4) {
self.admin_challenges.remove(addr.ip());
}
/// Remove outdated entries
fn admin_challenges_cleanup(&mut self) {
if self.admin_challenges_counter.next() {
let now = self.now();
self.admin_challenges
.retain(|_, v| v.is_valid(now, self.timeout.challenge));
} }
self.cleanup_challenges = 0;
} }
fn add_server(&mut self, addr: SocketAddrV4, server: ServerInfo) { fn add_server(&mut self, addr: SocketAddrV4, server: ServerInfo) {
@ -264,19 +355,11 @@ impl MasterServer {
} }
fn remove_outdated_servers(&mut self) { fn remove_outdated_servers(&mut self) {
if self.cleanup_servers < SERVER_CLEANUP_MAX { if self.servers_counter.next() {
self.cleanup_servers += 1; let now = self.now();
return; self.servers
.retain(|_, v| v.is_valid(now, self.timeout.server));
} }
let now = self.now();
let old = self.servers.len();
self.servers
.retain(|_, v| v.is_valid(now, self.timeout.server));
let new = self.servers.len();
if old != new {
trace!("Removed {} outdated servers", old - new);
}
self.cleanup_servers = 0;
} }
fn send_server_list<A, I>(&self, to: A, iter: I) -> Result<(), Error> fn send_server_list<A, I>(&self, to: A, iter: I) -> Result<(), Error>
@ -295,6 +378,45 @@ impl MasterServer {
} }
Ok(()) Ok(())
} }
#[inline]
fn is_blocked(&self, ip: &Ipv4Addr) -> bool {
self.blocklist.contains(ip)
}
fn admin_command(&mut self, cmd: &str) {
let args: Vec<_> = cmd.split(' ').collect();
fn helper<F: FnMut(&str, Ipv4Addr)>(args: &[&str], mut op: F) {
let iter = args.iter().map(|i| (i, i.parse::<Ipv4Addr>()));
for (i, ip) in iter {
match ip {
Ok(ip) => op(i, ip),
Err(_) => warn!("invalid ip: {}", i),
}
}
}
match args[0] {
"ban" => {
helper(&args[1..], |_, ip| {
if self.blocklist.insert(ip) {
info!("ban ip: {}", ip);
}
});
}
"unban" => {
helper(&args[1..], |_, ip| {
if self.blocklist.remove(&ip) {
info!("unban ip: {}", ip);
}
});
}
_ => {
warn!("invalid command: {}", args[0]);
}
}
}
} }
pub fn run(cfg: Config) -> Result<(), Error> { pub fn run(cfg: Config) -> Result<(), Error> {

14
protocol/src/admin.rs

@ -2,7 +2,7 @@
// SPDX-FileCopyrightText: 2023 Denis Drakhnia <numas13@gmail.com> // SPDX-FileCopyrightText: 2023 Denis Drakhnia <numas13@gmail.com>
use crate::cursor::{Cursor, CursorMut}; use crate::cursor::{Cursor, CursorMut};
use crate::types::{Hide, Str}; use crate::types::Hide;
use crate::Error; use crate::Error;
pub const HASH_LEN: usize = 64; pub const HASH_LEN: usize = 64;
@ -31,7 +31,7 @@ impl AdminChallenge {
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct AdminCommand<'a> { pub struct AdminCommand<'a> {
pub hash: Hide<&'a [u8]>, pub hash: Hide<&'a [u8]>,
pub command: Str<&'a [u8]>, pub command: &'a str,
} }
impl<'a> AdminCommand<'a> { impl<'a> AdminCommand<'a> {
@ -40,7 +40,7 @@ impl<'a> AdminCommand<'a> {
pub fn new(hash: &'a [u8], command: &'a str) -> Self { pub fn new(hash: &'a [u8], command: &'a str) -> Self {
Self { Self {
hash: Hide(hash), hash: Hide(hash),
command: Str(command.as_bytes()), command,
} }
} }
@ -48,7 +48,7 @@ impl<'a> AdminCommand<'a> {
let mut cur = Cursor::new(src); let mut cur = Cursor::new(src);
cur.expect(Self::HEADER)?; cur.expect(Self::HEADER)?;
let hash = Hide(cur.get_bytes(hash_len)?); let hash = Hide(cur.get_bytes(hash_len)?);
let command = Str(cur.get_bytes(cur.remaining())?); let command = cur.get_str(cur.remaining())?;
cur.expect_empty()?; cur.expect_empty()?;
Ok(Self { hash, command }) Ok(Self { hash, command })
} }
@ -62,7 +62,7 @@ impl<'a> AdminCommand<'a> {
Ok(CursorMut::new(buf) Ok(CursorMut::new(buf)
.put_bytes(Self::HEADER)? .put_bytes(Self::HEADER)?
.put_bytes(&self.hash)? .put_bytes(&self.hash)?
.put_bytes(&self.command)? .put_str(self.command)?
.pos()) .pos())
} }
} }
@ -74,12 +74,12 @@ pub enum Packet<'a> {
} }
impl<'a> Packet<'a> { impl<'a> Packet<'a> {
pub fn decode(src: &'a [u8]) -> Result<Self, Error> { pub fn decode(hash_len: usize, src: &'a [u8]) -> Result<Self, Error> {
if let Ok(p) = AdminChallenge::decode(src) { if let Ok(p) = AdminChallenge::decode(src) {
return Ok(Self::AdminChallenge(p)); return Ok(Self::AdminChallenge(p));
} }
if let Ok(p) = AdminCommand::decode(src) { if let Ok(p) = AdminCommand::decode_with_hash_len(hash_len, src) {
return Ok(Self::AdminCommand(p)); return Ok(Self::AdminCommand(p));
} }

Loading…
Cancel
Save