Merge branch 'zesterer/ip-banning' into 'master'

Initial implementation of IP banning

See merge request veloren/veloren!3363
This commit is contained in:
crabman
2024-11-02 22:46:51 +00:00
17 changed files with 1637 additions and 277 deletions

View File

@ -41,6 +41,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Reworked Myrmidon dungeon
- Wild Legoom and Goblin mobs
- Bloodservants, Strigoi, and Scarlet Spectacles added to Halloween event
- IP Bans
### Changed

View File

@ -16,6 +16,7 @@ command-body-desc = Change your body to different species
command-buff-desc = Cast a buff on player
command-build-desc = Toggles build mode on and off
command-ban-desc = Ban a player with a given username, for a given duration (if provided). Pass true for overwrite to alter an existing ban.
command-ban-ip-desc = Ban a player with a given username, for a given duration (if provided). Unlike the normal ban this also additionally bans the IP-address associated with this user. Pass true for overwrite to alter an existing ban.
command-battlemode-desc = Set your battle mode to:
+ pvp (player vs player)
+ pve (player vs environment).
@ -96,7 +97,8 @@ command-rtsim_info-desc = Display information about an rtsim NPC
command-rtsim_npc-desc = List rtsim NPCs that fit a given query (e.g: simulated,merchant) in order of distance
command-rtsim_purge-desc = Purge rtsim data on next startup
command-rtsim_tp-desc = Teleport to an rtsim npc
command-unban-desc = Remove the ban for the given username
command-unban-desc = Remove the ban for the given username. If there is an linked IP ban it will be removed as well.
command-unban-ip-desc = Remove just the IP ban for the given username.
command-version-desc = Prints server version
command-waypoint-desc = Set your waypoint to your current position
command-weather_zone-desc = Create a weather zone
@ -227,6 +229,7 @@ command-adminify-role-upgraded = Role for player { $player } upgraded to { $role
command-adminify-removed-role = Role removed from player { $player }: { $role }
command-ban-added = Added { $player } to the banlist with reason: { $reason }
command-ban-already-added = { $player } is already on the banlist
command-ban-ip-added = Added { $username } to the regular banlist and IP banlist with reason: { $reason }
command-faction-join = Please join a faction with /join_faction
command-group-join = Please create a group first
command-group_invite-invited-to-group = Invited { $player } to the group.
@ -240,6 +243,7 @@ command-sudo-no-permission-for-non-players = You don't have permission to sudo n
command-time_scale-current = The current time scale is { $scale }.
command-time_scale-changed = Set time scale to { $scale }.
command-unban-successful = { $player } was successfully unbanned.
command-unban-ip-successful = The IP banned via user "{ $player }" was successfully unbanned (this user will remain banned)
command-unban-already-unbanned = { $player } was already unbanned.
command-version-current = Server is running { $hash }[{ $date }]
command-whitelist-added = Added to whitelist: { $username }
@ -259,6 +263,13 @@ command-death_effect-unknown = Unknown death effect { $effect }.
command-spot-spot_not_found = Didn't find any spots of that kind in this world.
command-spot-world_feature = The `worldgen` feature has to be enabled to run this command.
command-cannot-send-message-hidden = Cannot send messages as a hidden spectator.
command-destroyed-tethers = All tethers destroyed! You are now free
command-destroyed-no-tethers = You're not connected to any tethers
command-dismounted = Dismounted
command-no-dismount = You're not riding or being ridden
command-client-has-no-socketaddr = Cannot get socker addr (connected via mpsc connection) for { $target }
command-parse-duration-error = Could not parse duration: { $error }
command-ip-ban-require-online = { $error }. IP ban needs the target player to be online.
# Unreachable/untestable but added for consistency
@ -268,7 +279,4 @@ command-kit-inventory-unavailable = Could not get inventory
command-inventory-cant-fit-item = Can't fit item to inventory
# Emitted by /disconnect_all when you don't exist (?)
command-you-dont-exist = You do not exist, so you cannot use this command
command-destroyed-tethers = All tethers destroyed! You are now free
command-destroyed-no-tethers = You're not connected to any tethers
command-dismounted = Dismounted
command-no-dismount = You're not riding or being ridden
command-entity-has-no-client = Player has no client client component: { $target }

View File

@ -334,6 +334,7 @@ pub enum ServerChatCommand {
AreaRemove,
Aura,
Ban,
BanIp,
BattleMode,
BattleModeForce,
Body,
@ -414,6 +415,7 @@ pub enum ServerChatCommand {
TimeScale,
Tp,
Unban,
UnbanIp,
Version,
Waypoint,
WeatherZone,
@ -486,6 +488,16 @@ impl ServerChatCommand {
Content::localized("command-ban-desc"),
Some(Moderator),
),
ServerChatCommand::BanIp => cmd(
vec![
PlayerName(Required),
Boolean("overwrite", "true".to_string(), Optional),
Any("ban duration", Optional),
Message(Optional),
],
Content::localized("command-ban-ip-desc"),
Some(Admin),
),
#[rustfmt::skip]
ServerChatCommand::BattleMode => cmd(
vec![Enum(
@ -959,6 +971,11 @@ impl ServerChatCommand {
Content::localized("command-unban-desc"),
Some(Moderator),
),
ServerChatCommand::UnbanIp => cmd(
vec![PlayerName(Required)],
Content::localized("command-unban-ip-desc"),
Some(Moderator),
),
ServerChatCommand::Version => {
cmd(vec![], Content::localized("command-version-desc"), None)
},
@ -1066,6 +1083,7 @@ impl ServerChatCommand {
ServerChatCommand::AreaRemove => "area_remove",
ServerChatCommand::Aura => "aura",
ServerChatCommand::Ban => "ban",
ServerChatCommand::BanIp => "ban_ip",
ServerChatCommand::BattleMode => "battlemode",
ServerChatCommand::BattleModeForce => "battlemode_force",
ServerChatCommand::Body => "body",
@ -1135,6 +1153,7 @@ impl ServerChatCommand {
ServerChatCommand::RtsimPurge => "rtsim_purge",
ServerChatCommand::RtsimChunk => "rtsim_chunk",
ServerChatCommand::Unban => "unban",
ServerChatCommand::UnbanIp => "unban_ip",
ServerChatCommand::Version => "version",
ServerChatCommand::Waypoint => "waypoint",
ServerChatCommand::Wiring => "wiring",

View File

@ -39,6 +39,20 @@ pub enum ConnectAddr {
Mpsc(u64),
}
impl ConnectAddr {
/// Returns the `Some` if the protocol is TCP or QUIC and `None` if the
/// protocol is a local channel (mpsc).
pub fn socket_addr(&self) -> Option<SocketAddr> {
match self {
Self::Tcp(addr) => Some(*addr),
Self::Udp(addr) => Some(*addr),
Self::Mpsc(_) => None,
#[cfg(feature = "quic")]
Self::Quic(addr, _, _) => Some(*addr),
}
}
}
/// Represents a Tcp, Quic, Udp or Mpsc listen address
#[derive(Clone, Debug)]
pub enum ListenAddr {
@ -49,8 +63,8 @@ pub enum ListenAddr {
Mpsc(u64),
}
/// a Participant can throw different events, you are obligated to carefully
/// empty the queue from time to time
/// A Participant can throw different events, you are obligated to carefully
/// empty the queue from time to time.
#[derive(Clone, Debug)]
pub enum ParticipantEvent {
ChannelCreated(ConnectAddr),
@ -412,7 +426,7 @@ impl Network {
Ok(participant)
}
/// returns a [`Participant`] created from a [`ListenAddr`] you
/// Returns a [`Participant`] created from a [`ListenAddr`] you
/// called [`listen`] on before. This function will either return a
/// working [`Participant`] ready to open [`Streams`] on OR has returned
/// a [`NetworkError`] (e.g. Network got closed)

View File

@ -1,8 +1,8 @@
use common_net::msg::{ClientType, ServerGeneral, ServerMsg};
use network::{Message, Participant, Stream, StreamError, StreamParams};
use network::{ConnectAddr, Message, Participant, Stream, StreamError, StreamParams};
use serde::{de::DeserializeOwned, Serialize};
use specs::Component;
use std::sync::atomic::AtomicBool;
use std::{net::SocketAddr, sync::atomic::AtomicBool};
/// Client handles ALL network related information of everything that connects
/// to the server Client DOES NOT handle game states
@ -13,6 +13,8 @@ use std::sync::atomic::AtomicBool;
pub struct Client {
pub client_type: ClientType,
pub participant: Option<Participant>,
pub current_ip_addrs: Vec<SocketAddr>,
connected_from_addr: ConnectAddr,
pub last_ping: f64,
pub login_msg_sent: AtomicBool,
pub locale: Option<String>,
@ -48,6 +50,7 @@ impl Client {
pub(crate) fn new(
client_type: ClientType,
participant: Participant,
connected_from: ConnectAddr,
last_ping: f64,
locale: Option<String>,
general_stream: Stream,
@ -66,6 +69,8 @@ impl Client {
Client {
client_type,
participant: Some(participant),
current_ip_addrs: connected_from.socket_addr().into_iter().collect(),
connected_from_addr: connected_from,
last_ping,
locale,
login_msg_sent: AtomicBool::new(false),
@ -84,6 +89,8 @@ impl Client {
}
}
pub(crate) fn connected_from_addr(&self) -> &ConnectAddr { &self.connected_from_addr }
pub(crate) fn send<M: Into<ServerMsg>>(&self, msg: M) -> Result<(), StreamError> {
// TODO: hack to avoid locking stream mutex while serializing the message,
// remove this when the mutexes on the Streams are removed

View File

@ -8,12 +8,11 @@ use crate::{
location::Locations,
login_provider::LoginProvider,
settings::{
server_description::ServerDescription, Ban, BanAction, BanInfo, EditableSetting,
SettingError, WhitelistInfo, WhitelistRecord,
banlist::NormalizedIpAddr, server_description::ServerDescription, BanInfo, BanOperation,
BanOperationError, EditableSetting, SettingError, WhitelistInfo, WhitelistRecord,
},
sys::terrain::SpawnEntityData,
wiring,
wiring::OutputFormula,
wiring::{self, OutputFormula},
Server, Settings, StateExt,
};
@ -75,7 +74,10 @@ use hashbrown::{HashMap, HashSet};
use humantime::Duration as HumanDuration;
use rand::{thread_rng, Rng};
use specs::{storage::StorageEntry, Builder, Entity as EcsEntity, Join, LendJoin, WorldExt};
use std::{fmt::Write, num::NonZeroU32, ops::DerefMut, str::FromStr, sync::Arc, time::Duration};
use std::{
fmt::Write, net::SocketAddr, num::NonZeroU32, ops::DerefMut, str::FromStr, sync::Arc,
time::Duration,
};
use vek::*;
use wiring::{Circuit, Wire, WireNode, WiringAction, WiringActionEffect, WiringElement};
#[cfg(feature = "worldgen")]
@ -145,6 +147,7 @@ fn do_command(
ServerChatCommand::AreaRemove => handle_area_remove,
ServerChatCommand::Aura => handle_aura,
ServerChatCommand::Ban => handle_ban,
ServerChatCommand::BanIp => handle_ban_ip,
ServerChatCommand::BattleMode => handle_battlemode,
ServerChatCommand::BattleModeForce => handle_battlemode_force,
ServerChatCommand::Body => handle_body,
@ -214,6 +217,7 @@ fn do_command(
ServerChatCommand::RtsimPurge => handle_rtsim_purge,
ServerChatCommand::RtsimChunk => handle_rtsim_chunk,
ServerChatCommand::Unban => handle_unban,
ServerChatCommand::UnbanIp => handle_unban_ip,
ServerChatCommand::Version => handle_version,
ServerChatCommand::Waypoint => handle_waypoint,
ServerChatCommand::Wiring => handle_spawn_wiring,
@ -279,6 +283,24 @@ fn uuid(server: &Server, entity: EcsEntity, descriptor: &str) -> CmdResult<Uuid>
})
}
fn socket_addr(server: &Server, entity: EcsEntity, descriptor: &str) -> CmdResult<SocketAddr> {
server
.state
.ecs()
.read_storage::<Client>()
.get(entity)
.ok_or_else(|| {
Content::localized_with_args("command-entity-has-no-client", [("target", descriptor)])
})?
.connected_from_addr()
.socket_addr()
.ok_or_else(|| {
Content::localized_with_args("command-client-has-no-socketaddr", [(
"target", descriptor,
)])
})
}
fn real_role(server: &Server, uuid: Uuid, descriptor: &str) -> CmdResult<AdminRole> {
server
.editable_settings()
@ -467,7 +489,47 @@ fn edit_setting_feedback<S: EditableSetting>(
);
Ok(())
},
Err(SettingError::Io(err)) => {
Err(setting_error) => edit_setting_error_feedback(server, client, setting_error, || info),
}
}
fn edit_banlist_feedback(
server: &mut Server,
client: EcsEntity,
result: Result<(), BanOperationError>,
// Message to provide if the edit was succesful (even if an IO error occurred, since the
// setting will still be changed in memory)
info: impl FnOnce() -> Content,
// Message to provide if the edit was cancelled due to it having no effect.
failure: impl FnOnce() -> Content,
) -> CmdResult<()> {
match result {
Ok(()) => {
server.notify_client(
client,
ServerGeneral::server_msg(ChatType::CommandInfo, info()),
);
Ok(())
},
// TODO: whether there is a typo and the supplied username has no ban entry or if the
// target was already banned/unbanned, the user of this command will always get the same
// error message here, which seems like it could be misleading.
Err(BanOperationError::NoEffect) => Err(failure()),
Err(BanOperationError::EditFailed(setting_error)) => {
edit_setting_error_feedback(server, client, setting_error, info)
},
}
}
fn edit_setting_error_feedback<S: EditableSetting>(
server: &mut Server,
client: EcsEntity,
setting_error: SettingError<S>,
info: impl FnOnce() -> Content,
) -> CmdResult<()> {
match setting_error {
SettingError::Io(err) => {
let info = info();
warn!(
?err,
"Failed to write settings file to disk, but succeeded in memory (success message: \
@ -486,7 +548,7 @@ fn edit_setting_feedback<S: EditableSetting>(
);
Ok(())
},
Err(SettingError::Integrity(err)) => Err(Content::localized_with_args(
SettingError::Integrity(err) => Err(Content::localized_with_args(
"command-error-while-evaluating-request",
[("error", format!("{err:?}"))],
)),
@ -2026,7 +2088,7 @@ fn handle_make_volume(
) -> CmdResult<()> {
use comp::body::ship::figuredata::VoxelCollider;
//let () = parse_args!(args);
//let () = parse_cmd_args!(args);
let pos = position(server, target, "target")?;
let ship = comp::ship::Body::Volume;
let sz = parse_cmd_args!(args, u32).unwrap_or(15);
@ -4511,6 +4573,36 @@ fn handle_kick(
}
}
fn make_ban_info(server: &mut Server, client: EcsEntity, client_uuid: Uuid) -> CmdResult<BanInfo> {
let client_username = uuid_to_username(server, client, client_uuid)?;
let client_role = real_role(server, client_uuid, "client")?;
let ban_info = BanInfo {
performed_by: client_uuid,
performed_by_username: client_username,
performed_by_role: client_role.into(),
};
Ok(ban_info)
}
fn ban_end_date(
now: chrono::DateTime<Utc>,
parse_duration: Option<HumanDuration>,
) -> CmdResult<Option<chrono::DateTime<Utc>>> {
let end_date = parse_duration
.map(|duration| chrono::Duration::from_std(duration.into()))
.transpose()
.map_err(|err| {
Content::localized_with_args(
"command-parse-duration-error",
[("error", format!("{err:?}"))]
)
})?
// On overflow (someone adding some ridiculous time span), just make the ban infinite.
// (end date of None is an infinite ban)
.and_then(|duration| now.checked_add_signed(duration));
Ok(end_date)
}
fn handle_ban(
server: &mut Server,
client: EcsEntity,
@ -4524,57 +4616,48 @@ fn handle_ban(
let reason = reason_opt.unwrap_or_default();
let overwrite = overwrite.unwrap_or(false);
let client_uuid = uuid(server, client, "client")?;
let ban_info = make_ban_info(server, client, client_uuid)?;
let player_uuid = find_username(server, &username)?;
let client_uuid = uuid(server, client, "client")?;
let client_username = uuid_to_username(server, client, client_uuid)?;
let client_role = real_role(server, client_uuid, "client")?;
let now = Utc::now();
let end_date = parse_duration
.map(|duration| chrono::Duration::from_std(duration.into()))
.transpose()
.map_err(|err| Content::Plain(format!("Error converting to duration: {}", err)))?
// On overflow (someone adding some ridiculous time span), just make the ban infinite.
.and_then(|duration| now.checked_add_signed(duration));
let end_date = ban_end_date(now, parse_duration)?;
let ban_info = BanInfo {
performed_by: client_uuid,
performed_by_username: client_username,
performed_by_role: client_role.into(),
let result = server.editable_settings_mut().banlist.ban_operation(
server.data_dir().as_ref(),
now,
player_uuid,
username.clone(),
BanOperation::Ban {
reason: reason.clone(),
info: ban_info,
end_date,
},
overwrite,
);
let (result, ban_info) = match result {
Ok(info) => (Ok(()), info),
Err(err) => (Err(err), None),
};
let ban = Ban {
reason: reason.clone(),
info: Some(ban_info),
end_date,
};
let ban_info = ban.info();
let edit = server
.editable_settings_mut()
.banlist
.ban_action(
server.data_dir().as_ref(),
now,
player_uuid,
username.clone(),
BanAction::Ban(ban),
overwrite,
)
.map(|result| {
(
Content::localized_with_args("command-ban-added", [
("player", username.to_owned()),
("reason", reason.to_owned()),
]),
result,
)
});
edit_setting_feedback(server, client, edit, || {
Content::localized_with_args("command-ban-already-added", [("player", username)])
})?;
edit_banlist_feedback(
server,
client,
result,
|| {
Content::localized_with_args("command-ban-added", [
("player", username.clone()),
("reason", reason),
])
},
|| {
Content::localized_with_args("command-ban-already-added", [(
"player",
username.clone(),
)])
},
)?;
// If the player is online kick them (this may fail if the player is a hardcoded
// admin; we don't care about that case because hardcoded admins can log on even
// if they're on the ban list).
@ -4584,7 +4667,7 @@ fn handle_ban(
server,
(client, client_uuid),
(target_player, player_uuid),
DisconnectReason::Banned(ban_info),
ban_info.map_or(DisconnectReason::Shutdown, DisconnectReason::Banned),
);
}
Ok(())
@ -4700,6 +4783,102 @@ fn handle_aura(
Ok(())
}
fn handle_ban_ip(
server: &mut Server,
client: EcsEntity,
_target: EcsEntity,
args: Vec<String>,
action: &ServerChatCommand,
) -> CmdResult<()> {
if let (Some(username), overwrite, parse_duration, reason_opt) =
parse_cmd_args!(args, String, bool, HumanDuration, String)
{
let reason = reason_opt.unwrap_or_default();
let overwrite = overwrite.unwrap_or(false);
let client_uuid = uuid(server, client, "client")?;
let ban_info = make_ban_info(server, client, client_uuid)?;
let player_uuid = find_username(server, &username)?;
let player_entity = find_uuid(server.state.ecs(), player_uuid).map_err(|err| {
Content::localized_with_args("command-ip-ban-require-online", [("error", err)])
})?;
let player_ip_addr =
NormalizedIpAddr::from(socket_addr(server, player_entity, &username)?.ip());
let now = Utc::now();
let end_date = ban_end_date(now, parse_duration)?;
let result = server.editable_settings_mut().banlist.ban_operation(
server.data_dir().as_ref(),
now,
player_uuid,
username.clone(),
BanOperation::BanIp {
reason: reason.clone(),
info: ban_info,
end_date,
ip: player_ip_addr,
},
overwrite,
);
let (result, ban_info) = match result {
Ok(info) => (Ok(()), info),
Err(err) => (Err(err), None),
};
edit_banlist_feedback(
server,
client,
result,
|| {
Content::localized_with_args("command-ban-ip-added", [
("player", username.clone()),
("reason", reason),
])
},
|| {
Content::localized_with_args("command-ban-already-added", [(
"player",
username.clone(),
)])
},
)?;
// Kick all online players with this IP address them (this may fail if the
// player is a hardcoded admin; we don't care about that case because
// hardcoded admins can log on even if they're on the ban list).
let ecs = server.state.ecs();
let players_to_kick = (
&ecs.entities(),
&ecs.read_storage::<Client>(),
&ecs.read_storage::<comp::Player>(),
)
.join()
.filter(|(_, client, _)| {
client
.current_ip_addrs
.iter()
.any(|socket_addr| NormalizedIpAddr::from(socket_addr.ip()) == player_ip_addr)
})
.map(|(entity, _, player)| (entity, player.uuid()))
.collect::<Vec<_>>();
for (player_entity, player_uuid) in players_to_kick {
let _ = kick_player(
server,
(client, client_uuid),
(player_entity, player_uuid),
ban_info
.clone()
.map_or(DisconnectReason::Shutdown, DisconnectReason::Banned),
);
}
Ok(())
} else {
Err(action.help_content())
}
}
fn handle_battlemode(
server: &mut Server,
client: EcsEntity,
@ -4856,43 +5035,91 @@ fn handle_unban(
let player_uuid = find_username(server, &username)?;
let client_uuid = uuid(server, client, "client")?;
let client_username = uuid_to_username(server, client, client_uuid)?;
let client_role = real_role(server, client_uuid, "client")?;
let ban_info = make_ban_info(server, client, client_uuid)?;
let now = Utc::now();
let ban_info = BanInfo {
performed_by: client_uuid,
performed_by_username: client_username,
performed_by_role: client_role.into(),
let unban = BanOperation::Unban { info: ban_info };
let result = server.editable_settings_mut().banlist.ban_operation(
server.data_dir().as_ref(),
now,
player_uuid,
username.clone(),
unban,
false,
);
edit_banlist_feedback(
server,
client,
result.map(|_| ()),
// TODO: it would be useful to indicate here whether an IP ban was also removed but we
// don't have that info.
|| {
Content::localized_with_args("command-unban-successful", [(
"player",
username.clone(),
)])
},
|| {
Content::localized_with_args("command-unban-already-unbanned", [(
"player",
username.clone(),
)])
},
)
} else {
Err(action.help_content())
}
}
fn handle_unban_ip(
server: &mut Server,
client: EcsEntity,
_target: EcsEntity,
args: Vec<String>,
action: &ServerChatCommand,
) -> CmdResult<()> {
if let Some(username) = parse_cmd_args!(args, String) {
let player_uuid = find_username(server, &username)?;
let client_uuid = uuid(server, client, "client")?;
let ban_info = make_ban_info(server, client, client_uuid)?;
let now = Utc::now();
let unban = BanOperation::UnbanIp {
info: ban_info,
uuid: player_uuid,
};
let unban = BanAction::Unban(ban_info);
let result = server.editable_settings_mut().banlist.ban_operation(
server.data_dir().as_ref(),
now,
player_uuid,
username.clone(),
unban,
false,
);
let edit = server
.editable_settings_mut()
.banlist
.ban_action(
server.data_dir().as_ref(),
now,
player_uuid,
username.clone(),
unban,
false,
)
.map(|result| {
(
Content::localized_with_args("command-unban-successful", [(
"player",
username.to_owned(),
)]),
result,
)
});
edit_setting_feedback(server, client, edit, || {
Content::localized_with_args("command-unban-already-unbanned", [("player", username)])
})
edit_banlist_feedback(
server,
client,
result.map(|_| ()),
|| {
Content::localized_with_args("command-unban-ip-successful", [(
"player",
username.clone(),
)])
},
|| {
Content::localized_with_args("command-unban-already-unbanned", [(
"player",
username.clone(),
)])
},
)
} else {
Err(action.help_content())
}

View File

@ -108,7 +108,7 @@ impl ConnectionHandler {
}
async fn init_participant(
participant: Participant,
mut participant: Participant,
client_sender: Sender<IncomingClient>,
info_requester_sender: Sender<Sender<ServerInfoPacket>>,
) -> Result<(), Box<dyn std::error::Error>> {
@ -142,9 +142,33 @@ impl ConnectionHandler {
Some(client_type) => client_type?,
};
use network::ParticipantEvent;
let connected_from = match select!(
_ = tokio::time::sleep(TIMEOUT).fuse() => None,
connected_from = participant.fetch_event().fuse() => Some(connected_from),
) {
None => {
error!("Did not receive initial channel created event. This is a bug!");
return Ok(());
},
Some(Err(err)) => {
debug!("Participant error when trying to receive event: {err:?}");
return Ok(());
},
Some(Ok(ParticipantEvent::ChannelDeleted(_))) => {
error!(
"Received channel deleted event instead of the initial channel created event. \
This is a bug!"
);
return Ok(());
},
Some(Ok(ParticipantEvent::ChannelCreated(connected_from))) => connected_from,
};
let client = Client::new(
client_type,
participant,
connected_from,
server_data.time,
None,
general_stream,

View File

@ -175,11 +175,15 @@ pub fn handle_client_disconnect(
) -> Event {
span!(_guard, "handle_client_disconnect");
let mut emit_logoff_event = true;
// Entity deleted below and persist_entity doesn't require a `Client` component,
// so we can just remove the Client component to get ownership of the
// participant.
if let Some(client) = server
.state()
.ecs()
.write_storage::<Client>()
.get_mut(entity)
.remove(entity)
{
// NOTE: There are not and likely will not be a way to safeguard against
// receiving multiple `ServerEvent::ClientDisconnect` messages in a tick
@ -193,7 +197,7 @@ pub fn handle_client_disconnect(
.with_label_values(&[get_reason_str(&reason)])
.inc();
if let Some(participant) = client.participant.take() {
if let Some(participant) = client.participant {
let pid = participant.remote_pid();
server.runtime.spawn(
async {
@ -269,6 +273,8 @@ pub fn handle_client_disconnect(
/// that this function will not be called twice on an entity with the same
/// character id.
pub(super) fn persist_entity(state: &mut State, entity: EcsEntity) -> EcsEntity {
// NOTE: `Client` component may already be removed by the caller to close the
// connection. Don't depend on it here!
if let (
Some(presence),
Some(skill_set),

View File

@ -1,4 +1,7 @@
use crate::settings::{AdminRecord, BanEntry, WhitelistRecord};
use crate::{
settings::{banlist::NormalizedIpAddr, AdminRecord, Ban, Banlist, WhitelistRecord},
Client,
};
use authc::{AuthClient, AuthClientError, AuthToken, Uuid};
use chrono::Utc;
use common::comp::AdminRole;
@ -9,6 +12,24 @@ use std::{str::FromStr, sync::Arc};
use tokio::{runtime::Runtime, sync::oneshot};
use tracing::{error, info};
/// Determines whether a user is banned, given a ban record connected to a user,
/// the `AdminRecord` of that user (if it exists), and the current time.
pub fn ban_applies(
ban: &Ban,
admin: Option<&AdminRecord>,
now: chrono::DateTime<chrono::Utc>,
) -> bool {
// Make sure the ban is active, and that we can't override it.
//
// If we are an admin and our role is at least as high as the role of the
// person who banned us, we can override the ban; we negate this to find
// people who cannot override it.
let exceeds_ban_role = |admin: &AdminRecord| {
AdminRole::from(admin.role) >= AdminRole::from(ban.performed_by_role())
};
!ban.is_expired(now) && !admin.map_or(false, exceeds_ban_role)
}
fn derive_uuid(username: &str) -> Uuid {
let mut state = 144066263297769815596495629667062367629;
@ -93,34 +114,40 @@ impl LoginProvider {
pub(crate) fn login<R>(
pending: &mut PendingLogin,
client: &Client,
admins: &HashMap<Uuid, AdminRecord>,
whitelist: &HashMap<Uuid, WhitelistRecord>,
banlist: &HashMap<Uuid, BanEntry>,
banlist: &Banlist,
player_count_exceeded: impl FnOnce(String, Uuid) -> (bool, R),
) -> Option<Result<R, RegisterError>> {
match pending.pending_r.try_recv() {
Ok(Err(e)) => Some(Err(e)),
Ok(Ok((username, uuid))) => {
let now = Utc::now();
// We ignore mpsc connections since those aren't to an external
// process.
let ip = client
.connected_from_addr()
.socket_addr()
.map(|s| s.ip())
.map(NormalizedIpAddr::from);
// Hardcoded admins can always log in.
let admin = admins.get(&uuid);
if let Some(ban) = banlist
.uuid_bans()
.get(&uuid)
.and_then(|ban_record| ban_record.current.action.ban())
.and_then(|ban_entry| ban_entry.current.action.ban())
.into_iter()
.chain(ip.and_then(|ip| {
banlist
.ip_bans()
.get(&ip)
.and_then(|ban_entry| ban_entry.current.action.ban())
}))
.find(|ban| ban_applies(ban, admin, now))
{
// Make sure the ban is active, and that we can't override it.
//
// If we are an admin and our role is at least as high as the role of the
// person who banned us, we can override the ban; we negate this to find
// people who cannot override it.
let exceeds_ban_role = |admin: &AdminRecord| {
Into::<AdminRole>::into(admin.role)
>= Into::<AdminRole>::into(ban.performed_by_role())
};
if !ban.is_expired(now) && !admin.map_or(false, exceeds_ban_role) {
// Get ban info and send a copy of it
return Some(Err(RegisterError::Banned(ban.info())));
}
// Get ban info and send a copy of it
return Some(Err(RegisterError::Banned(ban.info())));
}
// non-admins can only join if the whitelist is empty (everyone can join)

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,6 @@ use std::{
};
use tracing::{error, info, warn};
#[derive(Debug)]
/// Errors that can occur during edits to a settings file.
pub enum Error<S: EditableSetting> {
/// An error occurred validating the settings file.
@ -17,13 +16,37 @@ pub enum Error<S: EditableSetting> {
Io(std::io::Error),
}
#[derive(Debug)]
impl<S: EditableSetting> fmt::Debug for Error<S> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Error::Integrity(err) => fmt::Formatter::debug_tuple(f, "Integrity")
.field(err)
.finish(),
Error::Io(err) => fmt::Formatter::debug_tuple(f, "Io").field(err).finish(),
}
}
}
/// Same as Error, but carries the validated settings in the Io case.
enum ErrorInternal<S: EditableSetting> {
Integrity(S::Error),
Io(std::io::Error, S),
}
impl<S: EditableSetting> fmt::Debug for ErrorInternal<S> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ErrorInternal::Integrity(err) => fmt::Formatter::debug_tuple(f, "Integrity")
.field(err)
.finish(),
ErrorInternal::Io(err, _setting) => fmt::Formatter::debug_tuple(f, "Io")
.field(err)
.field(&"EditableSetting not required to impl Debug")
.finish(),
}
}
}
pub enum Version {
/// This was an old version of the settings file, so overwrite with the
/// modern config.
@ -160,7 +183,7 @@ pub trait EditableSetting: Clone + Default {
}
}
/// If the result of calling f is None,we return None (this constitutes an
/// If the result of calling f is None, we return None (this constitutes an
/// early return and lets us abandon the in-progress edit). For
/// example, this can be used to avoid adding a new ban entry if someone
/// is already banned and the user didn't explicitly specify that they

View File

@ -8,7 +8,8 @@ pub use editable::{EditableSetting, Error as SettingError};
pub use admin::{AdminRecord, Admins};
pub use banlist::{
Ban, BanAction, BanEntry, BanError, BanErrorKind, BanInfo, BanKind, BanRecord, Banlist,
Ban, BanEntry, BanError, BanErrorKind, BanInfo, BanKind, BanOperation, BanOperationError,
BanRecord, Banlist,
};
pub use server_description::ServerDescriptions;
pub use whitelist::{Whitelist, WhitelistInfo, WhitelistRecord};

View File

@ -1,6 +1,7 @@
pub mod character_screen;
pub mod general;
pub mod in_game;
pub mod network_events;
pub mod ping;
pub mod register;
pub mod terrain;
@ -24,6 +25,7 @@ pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) {
dispatch::<terrain::Sys>(dispatch_builder, &[]);
dispatch::<pets::Sys>(dispatch_builder, &[]);
dispatch::<loot::Sys>(dispatch_builder, &[]);
dispatch::<network_events::Sys>(dispatch_builder, &[]);
}
/// handles all send msg and calls a handle fn

View File

@ -0,0 +1,104 @@
use crate::{client::Client, settings::banlist::NormalizedIpAddr, EditableSettings};
use common::{
comp::Player,
event::{ClientDisconnectEvent, EventBus},
};
use common_ecs::{Job, Origin, Phase, System};
use common_net::msg::{DisconnectReason, ServerGeneral};
use network::ParticipantEvent;
use specs::{Entities, Join, Read, ReadExpect, ReadStorage, WriteStorage};
/// This system consumes events from the `Participant::try_fetch_event`. These
/// currently indicate channels being created and destroyed which potentially
/// corresponds to the client using new addresses.
///
/// New addresses are checked against the existing IP bans. If a match is found
/// that client will be kicked. Otherwise, the IP is added to the set of IPs
/// that client has used. When a new IP ban is created, the set of IP addrs used
/// by each client is scanned and any clients with matches are kicked.
///
/// We could retain addresses of removed channels and use them when banning but
/// that would use a potentially unknown amount of memory (so they are removed).
#[derive(Default)]
pub struct Sys;
impl<'a> System<'a> for Sys {
type SystemData = (
Entities<'a>,
ReadStorage<'a, Player>,
WriteStorage<'a, Client>,
Read<'a, EventBus<ClientDisconnectEvent>>,
ReadExpect<'a, EditableSettings>,
);
const NAME: &'static str = "msg::network_events";
const ORIGIN: Origin = Origin::Server;
const PHASE: Phase = Phase::Create;
fn run(
_job: &mut Job<Self>,
(entities, players, mut clients, client_disconnect_event_bus, editable_settings): Self::SystemData,
) {
let now = chrono::Utc::now();
let mut client_disconnect_emitter = client_disconnect_event_bus.emitter();
for (entity, client) in (&entities, &mut clients).join() {
while let Some(Ok(Some(event))) = client
.participant
.as_mut()
.map(|participant| participant.try_fetch_event())
{
match event {
ParticipantEvent::ChannelCreated(connect_addr) => {
// Ignore mpsc connections
if let Some(addr) = connect_addr.socket_addr() {
client.current_ip_addrs.push(addr);
let banned = editable_settings
.banlist
.ip_bans()
.get(&NormalizedIpAddr::from(addr.ip()))
.and_then(|ban_entry| ban_entry.current.action.ban())
.and_then(|ban| {
// Hardcoded admins can always log in.
let admin = players.get(entity).and_then(|player| {
editable_settings.admins.get(&player.uuid())
});
crate::login_provider::ban_applies(ban, admin, now)
.then(|| ban.info())
});
if let Some(ban_info) = banned {
// Kick client
client_disconnect_emitter.emit(ClientDisconnectEvent(
entity,
common::comp::DisconnectReason::Kicked,
));
let _ = client.send(ServerGeneral::Disconnect(
DisconnectReason::Banned(ban_info),
));
}
}
},
ParticipantEvent::ChannelDeleted(connect_addr) => {
// Ignore mpsc connections
if let Some(addr) = connect_addr.socket_addr() {
if let Some(i) = client
.current_ip_addrs
.iter()
.rev()
.position(|a| *a == addr)
{
client.current_ip_addrs.remove(i);
} else {
tracing::error!(
"Channel deleted but its address isn't present in \
client.current_ip_addrs!"
);
}
}
},
}
}
}
}
}

View File

@ -42,11 +42,6 @@ impl<'a> System<'a> for Sys {
(&entities, &mut clients).par_join().for_each_init(
|| client_disconnect.emitter(),
|client_disconnect_emitter, (entity, client)| {
// ignore network events
while let Some(Ok(Some(_))) =
client.participant.as_mut().map(|p| p.try_fetch_event())
{}
let res = super::try_recv_all(client, 4, Self::handle_ping_msg);
match res {

View File

@ -229,6 +229,7 @@ impl<'a> System<'a> for Sys {
mut new_players_guard,
) = match LoginProvider::login(
pending,
client,
&read_data.editable_settings.admins,
&read_data.editable_settings.whitelist,
&read_data.editable_settings.banlist,

View File

@ -793,7 +793,10 @@ fn verify_cmd_list_sorted() {
#[test]
fn test_complete_command() {
assert_eq!(complete_command("mu", '/'), vec!["/mute".to_string()]);
assert_eq!(complete_command("unba", '/'), vec!["/unban".to_string()]);
assert_eq!(complete_command("unba", '/'), vec![
"/unban".to_string(),
"/unban_ip".to_string()
]);
assert_eq!(complete_command("make_", '/'), vec![
"/make_block".to_string(),
"/make_npc".to_string(),