mirror of
https://git.mentality.rip/numas13/xash3d-master.git
synced 2025-01-22 04:44:31 +00:00
master: Admin ban and unban commands
This commit is contained in:
parent
2a39c233fd
commit
a9980ea314
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -461,6 +461,7 @@ name = "xash3d-master"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bitflags 2.4.0",
|
||||
"blake2b_simd",
|
||||
"chrono",
|
||||
"fastrand",
|
||||
"getopts",
|
||||
|
@ -6,8 +6,8 @@ mod cli;
|
||||
use std::io::{self, Write};
|
||||
use std::net::UdpSocket;
|
||||
|
||||
use termion::input::TermRead;
|
||||
use blake2b_simd::Params;
|
||||
use termion::input::TermRead;
|
||||
use thiserror::Error;
|
||||
use xash3d_protocol::{admin, master};
|
||||
|
||||
|
@ -18,6 +18,7 @@ bitflags = "2.4"
|
||||
fastrand = "2.0.1"
|
||||
serde = { version = "1.0.188", features = ["derive"] }
|
||||
toml = "0.5.11"
|
||||
blake2b_simd = "<0.6"
|
||||
xash3d-protocol = { path = "../protocol", version = "0.1.0" }
|
||||
|
||||
[dependencies.chrono]
|
||||
|
@ -20,3 +20,16 @@ version = "0.20"
|
||||
update_title = "https://github.com/FWGS/xash3d-fwgs"
|
||||
update_map = "Update please"
|
||||
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"
|
||||
|
@ -9,6 +9,7 @@ use std::path::Path;
|
||||
use log::LevelFilter;
|
||||
use serde::{de::Error as _, Deserialize, Deserializer};
|
||||
use thiserror::Error;
|
||||
use xash3d_protocol::admin;
|
||||
use xash3d_protocol::filter::Version;
|
||||
|
||||
pub const DEFAULT_CONFIG_PATH: &str = "config/main.toml";
|
||||
@ -34,6 +35,11 @@ pub struct Config {
|
||||
pub server: ServerConfig,
|
||||
#[serde(default)]
|
||||
pub client: ClientConfig,
|
||||
#[serde(default)]
|
||||
pub hash: HashConfig,
|
||||
#[serde(rename = "admin")]
|
||||
#[serde(default)]
|
||||
pub admin_list: Box<[AdminConfig]>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
@ -105,6 +111,24 @@ pub struct ClientConfig {
|
||||
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 {
|
||||
LevelFilter::Warn
|
||||
}
|
||||
@ -121,6 +145,18 @@ fn default_timeout() -> u32 {
|
||||
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>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
|
@ -1,18 +1,19 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
// SPDX-FileCopyrightText: 2023 Denis Drakhnia <numas13@gmail.com>
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::io;
|
||||
use std::net::{SocketAddr, SocketAddrV4, ToSocketAddrs, UdpSocket};
|
||||
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4, ToSocketAddrs, UdpSocket};
|
||||
use std::ops::Deref;
|
||||
use std::time::Instant;
|
||||
|
||||
use blake2b_simd::Params;
|
||||
use fastrand::Rng;
|
||||
use log::{error, info, trace, warn};
|
||||
use thiserror::Error;
|
||||
use xash3d_protocol::filter::{Filter, Version};
|
||||
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};
|
||||
|
||||
@ -25,6 +26,9 @@ const SERVER_CLEANUP_MAX: usize = 100;
|
||||
/// How many cleanup calls should be skipped before removing outdated challenges.
|
||||
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)]
|
||||
pub enum Error {
|
||||
#[error("Failed to bind server socket: {0}")]
|
||||
@ -33,10 +37,12 @@ pub enum Error {
|
||||
Protocol(#[from] ProtocolError),
|
||||
#[error(transparent)]
|
||||
Io(#[from] io::Error),
|
||||
#[error("Admin challenge do not exist")]
|
||||
AdminChallengeNotFound,
|
||||
}
|
||||
|
||||
/// HashMap entry to keep tracking creation time.
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
struct Entry<T> {
|
||||
time: u32,
|
||||
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 {
|
||||
sock: UdpSocket,
|
||||
challenges: HashMap<SocketAddrV4, Entry<u32>>,
|
||||
challenges_counter: Counter,
|
||||
servers: HashMap<SocketAddrV4, Entry<ServerInfo>>,
|
||||
servers_counter: Counter,
|
||||
rng: Rng,
|
||||
|
||||
start_time: Instant,
|
||||
cleanup_challenges: usize,
|
||||
cleanup_servers: usize,
|
||||
timeout: config::TimeoutConfig,
|
||||
|
||||
clver: Version,
|
||||
update_title: Box<str>,
|
||||
update_map: Box<str>,
|
||||
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 {
|
||||
@ -100,15 +134,20 @@ impl MasterServer {
|
||||
sock,
|
||||
start_time: Instant::now(),
|
||||
challenges: Default::default(),
|
||||
challenges_counter: Counter::new(CHALLENGE_CLEANUP_MAX),
|
||||
servers: Default::default(),
|
||||
servers_counter: Counter::new(SERVER_CLEANUP_MAX),
|
||||
rng: Rng::new(),
|
||||
cleanup_challenges: 0,
|
||||
cleanup_servers: 0,
|
||||
timeout: cfg.server.timeout,
|
||||
clver: cfg.client.version,
|
||||
update_title: cfg.client.update_title,
|
||||
update_map: cfg.client.update_map,
|
||||
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];
|
||||
loop {
|
||||
let (n, from) = self.sock.recv_from(&mut buf)?;
|
||||
|
||||
let from = match from {
|
||||
SocketAddr::V4(a) => a,
|
||||
_ => {
|
||||
@ -131,6 +171,10 @@ impl MasterServer {
|
||||
}
|
||||
|
||||
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) {
|
||||
match 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 {
|
||||
admin::Packet::AdminChallenge(p) => {
|
||||
trace!("{}: recv {:?}", from, p);
|
||||
let challenge = 0x12345678; // TODO:
|
||||
let challenge = self.admin_challenge_add(from);
|
||||
|
||||
let p = master::AdminChallengeResponse::new(challenge);
|
||||
trace!("{}: send {:?}", from, p);
|
||||
let mut buf = [0; 64];
|
||||
let n = p.encode(&mut buf)?;
|
||||
self.sock.send_to(&buf[..n], from)?;
|
||||
|
||||
self.admin_challenges_cleanup();
|
||||
}
|
||||
admin::Packet::AdminCommand(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) {
|
||||
if self.cleanup_challenges < CHALLENGE_CLEANUP_MAX {
|
||||
self.cleanup_challenges += 1;
|
||||
return;
|
||||
if self.challenges_counter.next() {
|
||||
let now = self.now();
|
||||
self.challenges
|
||||
.retain(|_, v| v.is_valid(now, self.timeout.challenge));
|
||||
}
|
||||
let now = self.now();
|
||||
let old = self.challenges.len();
|
||||
self.challenges
|
||||
.retain(|_, v| v.is_valid(now, self.timeout.challenge));
|
||||
let new = self.challenges.len();
|
||||
if old != new {
|
||||
trace!("Removed {} outdated challenges", old - new);
|
||||
}
|
||||
|
||||
fn admin_challenge_add(&mut self, addr: SocketAddrV4) -> u32 {
|
||||
let x = self.rng.u32(..);
|
||||
let entry = Entry::new(self.now(), x);
|
||||
self.admin_challenges.insert(*addr.ip(), entry);
|
||||
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) {
|
||||
@ -264,19 +355,11 @@ impl MasterServer {
|
||||
}
|
||||
|
||||
fn remove_outdated_servers(&mut self) {
|
||||
if self.cleanup_servers < SERVER_CLEANUP_MAX {
|
||||
self.cleanup_servers += 1;
|
||||
return;
|
||||
if self.servers_counter.next() {
|
||||
let now = self.now();
|
||||
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>
|
||||
@ -295,6 +378,45 @@ impl MasterServer {
|
||||
}
|
||||
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> {
|
||||
|
@ -2,7 +2,7 @@
|
||||
// SPDX-FileCopyrightText: 2023 Denis Drakhnia <numas13@gmail.com>
|
||||
|
||||
use crate::cursor::{Cursor, CursorMut};
|
||||
use crate::types::{Hide, Str};
|
||||
use crate::types::Hide;
|
||||
use crate::Error;
|
||||
|
||||
pub const HASH_LEN: usize = 64;
|
||||
@ -31,7 +31,7 @@ impl AdminChallenge {
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct AdminCommand<'a> {
|
||||
pub hash: Hide<&'a [u8]>,
|
||||
pub command: Str<&'a [u8]>,
|
||||
pub command: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> AdminCommand<'a> {
|
||||
@ -40,7 +40,7 @@ impl<'a> AdminCommand<'a> {
|
||||
pub fn new(hash: &'a [u8], command: &'a str) -> Self {
|
||||
Self {
|
||||
hash: Hide(hash),
|
||||
command: Str(command.as_bytes()),
|
||||
command,
|
||||
}
|
||||
}
|
||||
|
||||
@ -48,7 +48,7 @@ impl<'a> AdminCommand<'a> {
|
||||
let mut cur = Cursor::new(src);
|
||||
cur.expect(Self::HEADER)?;
|
||||
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()?;
|
||||
Ok(Self { hash, command })
|
||||
}
|
||||
@ -62,7 +62,7 @@ impl<'a> AdminCommand<'a> {
|
||||
Ok(CursorMut::new(buf)
|
||||
.put_bytes(Self::HEADER)?
|
||||
.put_bytes(&self.hash)?
|
||||
.put_bytes(&self.command)?
|
||||
.put_str(self.command)?
|
||||
.pos())
|
||||
}
|
||||
}
|
||||
@ -74,12 +74,12 @@ pub enum 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) {
|
||||
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));
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user