Master server for Half-Life/Xash3D.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

737 lines
23 KiB

// SPDX-License-Identifier: GPL-3.0-only
// SPDX-FileCopyrightText: 2023 Denis Drakhnia <numas13@gmail.com>
use std::{
cmp::Eq,
collections::hash_map,
fmt::Display,
hash::Hash,
io,
net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6, ToSocketAddrs, UdpSocket},
ops::Deref,
str::FromStr,
sync::atomic::{AtomicBool, Ordering},
time::{Duration, Instant},
};
use ahash::{AHashMap as HashMap, AHashSet as HashSet};
use blake2b_simd::Params;
use fastrand::Rng;
use log::{debug, error, info, trace, warn};
use thiserror::Error;
use xash3d_protocol::{
admin,
filter::{Filter, FilterFlags, Version},
game,
master::{self, ServerAddress},
server,
server::Region,
wrappers::Str,
Error as ProtocolError, ServerInfo,
};
use crate::{
config::{self, Config},
stats::Stats,
};
pub trait AddrExt: Sized + Eq + Hash + Display + Copy + ToSocketAddrs + ServerAddress {
type Ip: Eq + Hash + Display + Copy + FromStr;
fn extract(addr: SocketAddr) -> Result<Self, SocketAddr>;
fn ip(&self) -> &Self::Ip;
fn wrap(self) -> SocketAddr;
fn mtu() -> usize;
}
impl AddrExt for SocketAddrV4 {
type Ip = Ipv4Addr;
fn extract(addr: SocketAddr) -> Result<Self, SocketAddr> {
if let SocketAddr::V4(addr) = addr {
Ok(addr)
} else {
Err(addr)
}
}
fn ip(&self) -> &Self::Ip {
SocketAddrV4::ip(self)
}
fn wrap(self) -> SocketAddr {
SocketAddr::V4(self)
}
#[inline(always)]
fn mtu() -> usize {
512
}
}
impl AddrExt for SocketAddrV6 {
type Ip = Ipv6Addr;
fn extract(addr: SocketAddr) -> Result<Self, SocketAddr> {
if let SocketAddr::V6(addr) = addr {
Ok(addr)
} else {
Err(addr)
}
}
fn ip(&self) -> &Self::Ip {
SocketAddrV6::ip(self)
}
fn wrap(self) -> SocketAddr {
SocketAddr::V6(self)
}
#[inline(always)]
fn mtu() -> usize {
MAX_PACKET_SIZE
}
}
/// The maximum size of UDP packets.
const MAX_PACKET_SIZE: usize = 1280;
/// How many cleanup calls should be skipped before removing outdated servers.
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;
/// 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}")]
BindSocket(io::Error),
#[error(transparent)]
Protocol(#[from] ProtocolError),
#[error(transparent)]
Io(#[from] io::Error),
#[error("Admin challenge do not exist")]
AdminChallengeNotFound,
#[error("Undefined packet")]
UndefinedPacket,
#[error("Unexpected packet")]
UnexpectedPacket,
}
/// HashMap entry to keep tracking creation time.
#[derive(Copy, Clone, Debug)]
struct Entry<T> {
time: u32,
value: T,
}
impl<T> Entry<T> {
fn new(time: u32, value: T) -> Self {
Self { time, value }
}
fn is_valid(&self, now: u32, duration: u32) -> bool {
(now - self.time) < duration
}
}
impl Entry<ServerInfo> {
fn matches<Addr: AddrExt>(&self, addr: Addr, region: Region, filter: &Filter) -> bool {
self.region == region && filter.matches(addr.wrap(), &self.value)
}
}
impl<T> Deref for Entry<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.value
}
}
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
}
}
}
pub struct MasterServer<Addr: AddrExt> {
sock: UdpSocket,
challenges: HashMap<Addr, Entry<u32>>,
challenges_counter: Counter,
servers: HashMap<Addr, Entry<ServerInfo>>,
servers_counter: Counter,
max_servers_per_ip: u16,
rng: Rng,
start_time: Instant,
timeout: config::TimeoutConfig,
clver: Version,
update_title: Box<str>,
update_map: Box<str>,
update_addr: SocketAddr,
admin_challenges: HashMap<Addr::Ip, Entry<(u32, u32)>>,
admin_challenges_counter: Counter,
admin_list: Box<[config::AdminConfig]>,
// rate limit if hash is invalid
admin_limit: HashMap<Addr::Ip, Entry<()>>,
admin_limit_counter: Counter,
hash: config::HashConfig,
blocklist: HashSet<Addr::Ip>,
stats: Stats,
// temporary data
filtered_servers: Vec<Addr>,
filtered_servers_nat: Vec<Addr>,
}
fn resolve_socket_addr<A>(addr: A, is_ipv4: bool) -> io::Result<Option<SocketAddr>>
where
A: ToSocketAddrs,
{
for i in addr.to_socket_addrs()? {
if i.is_ipv4() == is_ipv4 {
return Ok(Some(i));
}
}
Ok(None)
}
fn resolve_update_addr(cfg: &Config, local_addr: SocketAddr) -> SocketAddr {
if let Some(s) = cfg.client.update_addr.as_deref() {
let addr = if !s.contains(':') {
format!("{}:{}", s, local_addr.port())
} else {
s.to_owned()
};
match resolve_socket_addr(&addr, local_addr.is_ipv4()) {
Ok(Some(x)) => return x,
Ok(None) => error!("Update address: failed to resolve IP for \"{}\"", addr),
Err(e) => error!("Update address: {}", e),
}
}
local_addr
}
pub enum Master {
V4(MasterServer<SocketAddrV4>),
V6(MasterServer<SocketAddrV6>),
}
impl Master {
pub fn new(cfg: Config) -> Result<Self, Error> {
match SocketAddr::new(cfg.server.ip, cfg.server.port) {
SocketAddr::V4(addr) => MasterServer::new(cfg, addr).map(Self::V4),
SocketAddr::V6(addr) => MasterServer::new(cfg, addr).map(Self::V6),
}
}
pub fn update_config(&mut self, cfg: Config) -> Result<(), Error> {
let cfg = match self {
Self::V4(inner) => inner.update_config(cfg)?,
Self::V6(inner) => inner.update_config(cfg)?,
};
if let Some(cfg) = cfg {
info!("Server IP version changed, full restart");
*self = Self::new(cfg)?;
}
Ok(())
}
pub fn run(&mut self, sig_flag: &AtomicBool) -> Result<(), Error> {
match self {
Self::V4(inner) => inner.run(sig_flag),
Self::V6(inner) => inner.run(sig_flag),
}
}
}
impl<Addr: AddrExt> MasterServer<Addr> {
pub fn new(cfg: Config, addr: Addr) -> Result<Self, Error> {
info!("Listen address: {}", addr);
let sock = UdpSocket::bind(addr).map_err(Error::BindSocket)?;
// make socket interruptable by singals
sock.set_read_timeout(Some(Duration::from_secs(u32::MAX as u64)))?;
let update_addr = resolve_update_addr(&cfg, addr.wrap());
Ok(Self {
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),
max_servers_per_ip: cfg.server.max_servers_per_ip,
rng: Rng::new(),
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,
admin_limit: Default::default(),
admin_limit_counter: Counter::new(ADMIN_LIMIT_CLEANUP_MAX),
hash: cfg.hash,
blocklist: Default::default(),
stats: Stats::new(cfg.stat),
filtered_servers: Default::default(),
filtered_servers_nat: Default::default(),
})
}
fn local_addr(&self) -> io::Result<SocketAddr> {
self.sock.local_addr()
}
pub fn update_config(&mut self, cfg: Config) -> Result<Option<Config>, Error> {
let local_addr = self.local_addr()?;
let addr = SocketAddr::new(cfg.server.ip, cfg.server.port);
if local_addr.is_ipv4() != addr.is_ipv4() {
return Ok(Some(cfg));
} else if local_addr != addr {
info!("Listen address: {}", addr);
self.sock = UdpSocket::bind(addr).map_err(Error::BindSocket)?;
// make socket interruptable by singals
self.sock
.set_read_timeout(Some(Duration::from_secs(u32::MAX as u64)))?;
self.clear();
}
self.update_addr = resolve_update_addr(&cfg, addr);
self.timeout = cfg.server.timeout;
self.clver = cfg.client.version;
self.update_title = cfg.client.update_title;
self.update_map = cfg.client.update_map;
self.admin_list = cfg.admin_list;
self.hash = cfg.hash;
self.stats.update_config(cfg.stat);
Ok(None)
}
pub fn run(&mut self, sig_flag: &AtomicBool) -> Result<(), Error> {
let mut buf = [0; MAX_PACKET_SIZE];
while !sig_flag.load(Ordering::Relaxed) {
let (n, from) = match self.sock.recv_from(&mut buf[..Addr::mtu()]) {
Ok(x) => x,
Err(e) => match e.kind() {
io::ErrorKind::Interrupted => break,
io::ErrorKind::TimedOut | io::ErrorKind::WouldBlock => continue,
_ => Err(e)?,
},
};
let from = match Addr::extract(from) {
Ok(from) => from,
Err(_) => continue,
};
let src = &buf[..n];
if let Err(e) = self.handle_packet(from, src) {
debug!("{}: {}: \"{}\"", from, e, Str(src));
self.stats.on_error();
}
}
Ok(())
}
fn clear(&mut self) {
info!("Clear all servers and challenges");
self.challenges.clear();
self.servers.clear();
self.admin_challenges.clear();
self.stats.clear();
}
fn handle_server_packet(&mut self, from: Addr, p: server::Packet) -> Result<(), Error> {
trace!("{}: recv {:?}", from, p);
match p {
server::Packet::Challenge(p) => {
let master_challenge = self.add_challenge(from);
let mut buf = [0; MAX_PACKET_SIZE];
let p = master::ChallengeResponse::new(master_challenge, p.server_challenge);
trace!("{}: send {:?}", from, p);
let n = p.encode(&mut buf)?;
self.sock.send_to(&buf[..n], from)?;
self.remove_outdated_challenges();
}
server::Packet::ServerAdd(p) => {
let entry = match self.challenges.get(&from) {
Some(e) => e,
None => {
trace!("{}: Challenge does not exists", from);
return Ok(());
}
};
if !entry.is_valid(self.now(), self.timeout.challenge) {
return Ok(());
}
if p.challenge != entry.value {
warn!(
"{}: Expected challenge {} but received {}",
from, entry.value, p.challenge
);
return Ok(());
}
if self.challenges.remove(&from).is_some() {
self.add_server(from, ServerInfo::new(&p));
self.stats.on_server_add();
self.stats.servers_count(self.servers.len());
}
self.remove_outdated_servers();
}
server::Packet::ServerRemove => {
self.stats.on_server_del();
}
_ => {
return Err(Error::UnexpectedPacket);
}
}
Ok(())
}
fn handle_game_packet(&mut self, from: Addr, p: game::Packet) -> Result<(), Error> {
trace!("{}: recv {:?}", from, p);
match p {
game::Packet::QueryServers(p) => {
if p.filter.clver.map_or(false, |v| v < self.clver) {
match self.update_addr {
SocketAddr::V4(addr) => {
self.send_server_list(from, p.filter.key, &[addr])?;
}
SocketAddr::V6(addr) => {
self.send_server_list(from, p.filter.key, &[addr])?;
}
}
} else {
let now = self.now();
self.filtered_servers.clear();
self.filtered_servers_nat.clear();
self.servers
.iter()
.filter(|(addr, info)| {
info.is_valid(now, self.timeout.server)
&& info.matches(**addr, p.region, &p.filter)
})
.for_each(|(addr, info)| {
self.filtered_servers.push(*addr);
if info.flags.contains(FilterFlags::NAT) {
self.filtered_servers_nat.push(*addr);
}
});
self.send_server_list(from, p.filter.key, &self.filtered_servers)?;
// NOTE: If NAT is not set in a filter then by default the client is announced
// to filtered servers behind NAT.
if p.filter.contains_flags(FilterFlags::NAT).unwrap_or(true) {
self.send_client_to_nat_servers(from, &self.filtered_servers_nat)?;
}
self.stats.on_query_servers();
}
}
game::Packet::GetServerInfo(_) => {
let p = server::GetServerInfoResponse {
map: self.update_map.as_ref(),
host: self.update_title.as_ref(),
protocol: 48, // XXX: how to detect what version client will accept?
dm: true,
maxcl: 32,
gamedir: "valve", // XXX: probably must be specific for client...
..Default::default()
};
trace!("{}: send {:?}", from, p);
let mut buf = [0; MAX_PACKET_SIZE];
let n = p.encode(&mut buf[..Addr::mtu()])?;
self.sock.send_to(&buf[..n], from)?;
}
}
Ok(())
}
fn handle_admin_packet(&mut self, from: Addr, p: admin::Packet) -> Result<(), Error> {
trace!("{}: recv {:?}", from, p);
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 => {
let (master_challenge, hash_challenge) = self.admin_challenge_add(from);
let p = master::AdminChallengeResponse::new(master_challenge, hash_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) => {
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())
.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(&entry.1.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);
self.admin_limit.insert(*from.ip(), Entry::new(now, ()));
self.admin_limit_cleanup();
}
}
}
}
Ok(())
}
fn handle_packet(&mut self, from: Addr, src: &[u8]) -> Result<(), Error> {
if self.is_blocked(from.ip()) {
return Ok(());
}
match server::Packet::decode(src) {
Ok(Some(p)) => return self.handle_server_packet(from, p),
Ok(None) => {}
Err(e) => Err(e)?,
}
match game::Packet::decode(src) {
Ok(Some(p)) => return self.handle_game_packet(from, p),
Ok(None) => {}
Err(e) => Err(e)?,
}
match admin::Packet::decode(self.hash.len, src) {
Ok(Some(p)) => return self.handle_admin_packet(from, p),
Ok(None) => {}
Err(e) => Err(e)?,
}
Err(Error::UndefinedPacket)
}
fn now(&self) -> u32 {
self.start_time.elapsed().as_secs() as u32
}
fn add_challenge(&mut self, addr: Addr) -> u32 {
let x = self.rng.u32(..);
let entry = Entry::new(self.now(), x);
self.challenges.insert(addr, entry);
x
}
fn remove_outdated_challenges(&mut self) {
if self.challenges_counter.next() {
let now = self.now();
self.challenges
.retain(|_, v| v.is_valid(now, self.timeout.challenge));
}
}
fn admin_challenge_add(&mut self, addr: Addr) -> (u32, u32) {
let x = self.rng.u32(..);
let y = self.rng.u32(..);
let entry = Entry::new(self.now(), (x, y));
self.admin_challenges.insert(*addr.ip(), entry);
(x, y)
}
fn admin_challenge_remove(&mut self, addr: Addr) {
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));
}
}
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 count_servers(&self, ip: &Addr::Ip) -> u16 {
self.servers.keys().filter(|i| i.ip() == ip).count() as u16
}
fn add_server(&mut self, addr: Addr, server: ServerInfo) {
let now = self.now();
match self.servers.entry(addr) {
hash_map::Entry::Occupied(mut e) => {
trace!("{}: Updated GameServer", addr);
e.insert(Entry::new(now, server));
}
hash_map::Entry::Vacant(_) => {
if self.count_servers(addr.ip()) >= self.max_servers_per_ip {
trace!("{}: max servers per ip", addr);
return;
}
trace!("{}: New GameServer", addr);
self.servers.insert(addr, Entry::new(now, server));
}
}
}
fn remove_outdated_servers(&mut self) {
if self.servers_counter.next() {
let now = self.now();
self.servers
.retain(|_, v| v.is_valid(now, self.timeout.server));
self.stats.servers_count(self.servers.len());
}
}
fn send_server_list<A, S>(&self, to: A, key: Option<u32>, servers: &[S]) -> Result<(), Error>
where
A: ToSocketAddrs,
S: ServerAddress,
{
let mut buf = [0; MAX_PACKET_SIZE];
let mut offset = 0;
let mut list = master::QueryServersResponse::new(key);
while offset < servers.len() {
let (n, c) = list.encode(&mut buf[..Addr::mtu()], &servers[offset..])?;
offset += c;
self.sock.send_to(&buf[..n], &to)?;
}
Ok(())
}
fn send_client_to_nat_servers(&self, to: Addr, servers: &[Addr]) -> Result<(), Error> {
let mut buf = [0; 64];
let n = master::ClientAnnounce::new(to.wrap()).encode(&mut buf)?;
let buf = &buf[..n];
for i in servers {
self.sock.send_to(buf, i)?;
}
Ok(())
}
#[inline]
fn is_blocked(&self, ip: &Addr::Ip) -> bool {
self.blocklist.contains(ip)
}
fn admin_command(&mut self, cmd: &str) {
let args: Vec<_> = cmd.split(' ').collect();
fn helper<Addr, F>(args: &[&str], mut op: F)
where
Addr: AddrExt,
F: FnMut(&str, Addr::Ip),
{
let iter = args.iter().map(|i| (i, i.parse::<Addr::Ip>()));
for (i, ip) in iter {
match ip {
Ok(ip) => op(i, ip),
Err(_) => warn!("invalid ip: {}", i),
}
}
}
match args[0] {
"ban" => {
helper::<Addr, _>(&args[1..], |_, ip| {
if self.blocklist.insert(ip) {
info!("ban ip: {}", ip);
}
});
}
"unban" => {
helper::<Addr, _>(&args[1..], |_, ip| {
if self.blocklist.remove(&ip) {
info!("unban ip: {}", ip);
}
});
}
_ => {
warn!("invalid command: {}", args[0]);
}
}
}
}