rumor has it, that xMAC is so annoyed by people just not updating their game and be always blammed that its a network issue, that he just implements some dummy packages.

The Server sends some known structs on a stream.
Every Client from now on knows what to expect. If the list has changed, it can be assumed its incompatible anyway.
If some serialisations or checks fail, we detected a wrong version.
WARNING: it might happen to exist a server/client combination where this check does not work, but the versions are still incompatible.
There is no guarantee, i am just trying to get rid of 90% of the discord pings
This commit is contained in:
Marcel Märtens 2021-03-23 15:58:19 +01:00
parent 21b20ea75e
commit 7415ddd18d
10 changed files with 298 additions and 5 deletions

View File

@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Server kicks old client when a user is trying to log in again (often the case when a user's original connection gets dropped)
- Client has better detection for incompatible versions
- Added a raycast check to beams to prevent their effect applying through walls
## [0.9.0] - 2021-03-20

View File

@ -45,7 +45,8 @@ https://veloren.net/account/."#,
"main.login.insecure_auth_scheme": "The auth Scheme HTTP is NOT supported. It's insecure! For development purposes, HTTP is allowed for 'localhost' or debug builds",
"main.login.server_full": "Server is full",
"main.login.untrusted_auth_server": "Auth server not trusted",
"main.login.outdated_client_or_server": "ServerWentMad: Probably versions are incompatible, check for updates.",
"main.login.prob_outdated_client_or_server": "ServerWentMad: Probably versions are incompatible, check for updates.",
"main.login.outdated_client_or_server": "Version check failed: versions are incompatible, check for updates!",
"main.login.timeout": "Timeout: Server did not respond in time. (Overloaded or network issues).",
"main.login.server_shut_down": "Server shut down",
"main.login.already_logged_in": "You are already logged into the server.",

View File

@ -8,6 +8,7 @@ pub enum Error {
NetworkErr(NetworkError),
ParticipantErr(ParticipantError),
StreamErr(StreamError),
ServerValidationFailed(String, String),
ServerWentMad,
ServerTimeout,
ServerShutdown,

View File

@ -5,6 +5,7 @@
pub mod addr;
pub mod cmd;
pub mod error;
pub mod validate_version;
// Reexports
pub use crate::error::Error;
@ -225,6 +226,7 @@ impl Client {
let character_screen_stream = participant.opened().await?;
let in_game_stream = participant.opened().await?;
let terrain_stream = participant.opened().await?;
let validation_stream = participant.opened().await?;
register_stream.send(ClientType::Game)?;
let server_info: ServerInfo = register_stream.recv().await?;
@ -243,6 +245,22 @@ impl Client {
ping_stream.send(PingMsg::Ping)?;
// Validate Server dump
if let Err(()) = validate_version::validate_dump_from_server(validation_stream).await {
error!(
"Client does not pass server dump validation, versions do not match. \
Install/Update the right version"
);
return Err(Error::ServerValidationFailed(
format!("{}[{}]", server_info.git_hash, server_info.git_date),
format!(
"{}[{}]",
common::util::GIT_HASH.to_string(),
common::util::GIT_DATE.to_string()
),
));
};
// Wait for initial sync
let mut ping_interval = tokio::time::interval(core::time::Duration::from_secs(1));
let (

View File

@ -0,0 +1,145 @@
use tracing::{debug, trace};
/// assume deserialization is exactly what we hardcoded.
/// When it fails, we can generate a wrong version error
/// message and people stop annoying xMAC94x
/// see src/server/connecton_handler.rs
pub(crate) async fn validate_dump_from_server(
mut validate_stream: network::Stream,
) -> Result<(), ()> {
use common::{
character::Character,
comp::{
inventory::Inventory,
item::{tool::ToolKind, Reagent},
skills::SkillGroupKind,
},
outcome::Outcome,
terrain::{biome::BiomeKind, TerrainChunkMeta},
trade::{Good, PendingTrade, Trades},
uid::Uid,
};
use common_net::msg::{world_msg::EconomyInfo, ServerGeneral};
use std::collections::HashMap;
use vek::*;
trace!("check Character (1)");
match validate_stream.recv::<ServerGeneral>().await {
Ok(ServerGeneral::CharacterListUpdate(vec)) => {
check_eq(&vec.len(), 1)?;
let item = &vec[0];
check_eq(&item.character, Character {
id: Some(1337),
alias: "foobar".to_owned(),
})?;
check(item.body.is_humanoid())?;
let inv = Inventory::new_empty()
.equipped_items()
.cloned()
.collect::<Vec<_>>();
let inv_remote = item.inventory.equipped_items().cloned().collect::<Vec<_>>();
check_eq(&inv_remote, inv)?;
},
_ => return Err(()),
}
trace!("check Outcomes (2)");
match validate_stream.recv::<ServerGeneral>().await {
Ok(ServerGeneral::Outcomes(vec)) => {
check_eq(&vec.len(), 3)?;
check_eq(&vec[0], Outcome::Explosion {
pos: Vec3::new(1.0, 2.0, 3.0),
power: 4.0,
radius: 5.0,
is_attack: true,
reagent: Some(Reagent::Blue),
})?;
check_eq(&vec[1], Outcome::SkillPointGain {
uid: Uid::from(1337u64),
pos: Vec3::new(2.0, 4.0, 6.0),
skill_tree: SkillGroupKind::Weapon(ToolKind::Empty),
total_points: 99,
})?;
check_eq(&vec[2], Outcome::BreakBlock {
pos: Vec3::new(1, 2, 3),
color: Some(Rgb::new(0u8, 8u8, 13u8)),
})?;
},
_ => return Err(()),
}
trace!("check Terrain (3)");
match validate_stream.recv::<ServerGeneral>().await {
Ok(ServerGeneral::TerrainChunkUpdate { key, chunk }) => {
check_eq(&key, Vec2::new(42, 1337))?;
let chunk = chunk?;
check_eq(chunk.meta(), TerrainChunkMeta::void())?;
check_eq(&chunk.get_min_z(), 5)?;
},
_ => return Err(()),
}
trace!("check Componity Sync (4)");
match validate_stream.recv::<ServerGeneral>().await {
Ok(ServerGeneral::CompSync(item)) => {
check_eq(&item.comp_updates.len(), 2)?;
//check_eq(&item.comp_updates[0],
// CompUpdateKind::Inserted(comp::Pos(Vec3::new(42.1337, 0.0,
// 0.0))))?; check_eq(&item.comp_updates[1],
// CompUpdateKind::Inserted(comp::Pos(Vec3::new(0.0, 42.1337,
// 0.0))))?;
},
_ => return Err(()),
}
trace!("check Pending Trade (5)");
match validate_stream.recv::<ServerGeneral>().await {
Ok(ServerGeneral::UpdatePendingTrade(tradeid, pending)) => {
let uid = Uid::from(70);
check_eq(&tradeid, Trades::default().begin_trade(uid, uid))?;
check_eq(&pending, PendingTrade::new(uid, Uid::from(71)))?;
},
_ => return Err(()),
}
trace!("check Economy (6)");
match validate_stream.recv::<ServerGeneral>().await {
Ok(ServerGeneral::SiteEconomy(info)) => {
check_eq(&info, EconomyInfo {
id: 99,
population: 55,
stock: vec![
(Good::Wood, 50.0),
(Good::Tools, 33.3),
(Good::Coin, 9000.1),
]
.into_iter()
.collect::<HashMap<Good, f32>>(),
labor_values: HashMap::new(),
values: vec![
(Good::RoadSecurity, 1.0),
(Good::Terrain(BiomeKind::Forest), 1.0),
]
.into_iter()
.collect::<HashMap<Good, f32>>(),
labors: Vec::new(),
last_exports: HashMap::new(),
resources: HashMap::new(),
})?;
},
_ => return Err(()),
}
Ok(())
}
fn check_eq<T: PartialEq + core::fmt::Debug>(one: &T, two: T) -> Result<(), ()> {
if one == &two {
Ok(())
} else {
debug!(?one, ?two, "failed check");
Err(())
}
}
fn check(b: bool) -> Result<(), ()> { if b { Ok(()) } else { Err(()) } }

View File

@ -145,7 +145,7 @@ pub enum SiteKind {
Tree,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct EconomyInfo {
pub id: SiteId,
pub population: u32,

View File

@ -8,7 +8,7 @@ use vek::*;
/// occur, nor is it something that may be cancelled or otherwise altered. Its
/// primary purpose is to act as something for frontends (both server and
/// client) to listen to in order to receive feedback about events in the world.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum Outcome {
Explosion {
pos: Vec3<f32>,

View File

@ -78,7 +78,7 @@ impl TerrainChunkSize {
// TerrainChunkMeta
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TerrainChunkMeta {
name: Option<String>,
biome: BiomeKind,

View File

@ -88,6 +88,121 @@ impl ConnectionHandler {
}
}
/// This code just generates some messages so that the client can assume
/// deserialization This is completely random chosen with the goal to
/// cover as much as possible and to stop xMAC94x being annoyed in
/// discord by people that just have a old version
fn dump_messages_so_client_can_validate_itself(
mut validate_stream: network::Stream,
) -> Result<(), network::StreamError> {
use common::{
character::{Character, CharacterItem},
comp,
comp::{
humanoid,
inventory::Inventory,
item::{tool::ToolKind, Reagent},
skills::SkillGroupKind,
},
outcome::Outcome,
terrain::{biome::BiomeKind, Block, BlockKind, TerrainChunk, TerrainChunkMeta},
trade::{Good, PendingTrade, Trades},
uid::Uid,
};
use common_net::{
msg::{world_msg::EconomyInfo, EcsCompPacket, ServerGeneral},
sync::CompSyncPackage,
};
use rand::SeedableRng;
use std::collections::HashMap;
use vek::*;
// 1. Simulate Character
let mut rng = rand::rngs::SmallRng::from_seed([42; 32]);
let item = CharacterItem {
character: Character {
id: Some(1337),
alias: "foobar".to_owned(),
},
body: comp::Body::Humanoid(humanoid::Body::random_with(
&mut rng,
&humanoid::Species::Undead,
)),
inventory: Inventory::new_empty(),
};
validate_stream.send(ServerGeneral::CharacterListUpdate(vec![item]))?;
// 2. Simulate Outcomes
let item1 = Outcome::Explosion {
pos: Vec3::new(1.0, 2.0, 3.0),
power: 4.0,
radius: 5.0,
is_attack: true,
reagent: Some(Reagent::Blue),
};
let item2 = Outcome::SkillPointGain {
uid: Uid::from(1337u64),
pos: Vec3::new(2.0, 4.0, 6.0),
skill_tree: SkillGroupKind::Weapon(ToolKind::Empty),
total_points: 99,
};
let item3 = Outcome::BreakBlock {
pos: Vec3::new(1, 2, 3),
color: Some(Rgb::new(0u8, 8u8, 13u8)),
};
validate_stream.send(ServerGeneral::Outcomes(vec![item1, item2, item3]))?;
// 3. Simulate Terrain
let item = TerrainChunk::new(
5,
Block::new(BlockKind::Water, Rgb::zero()),
Block::new(BlockKind::Air, Rgb::zero()),
TerrainChunkMeta::void(),
);
validate_stream.send(ServerGeneral::TerrainChunkUpdate {
key: Vec2::new(42, 1337),
chunk: Ok(Box::new(item)),
})?;
// 4. Simulate Componity Sync
let mut item = CompSyncPackage::<EcsCompPacket>::new();
let uid = Uid::from(70);
item.comp_inserted(uid, comp::Pos(Vec3::new(42.1337, 0.0, 0.0)));
item.comp_inserted(uid, comp::Vel(Vec3::new(0.0, 42.1337, 0.0)));
validate_stream.send(ServerGeneral::CompSync(item))?;
// 5. Pending Trade
let uid = Uid::from(70);
validate_stream.send(ServerGeneral::UpdatePendingTrade(
Trades::default().begin_trade(uid, uid),
PendingTrade::new(uid, Uid::from(71)),
))?;
// 6. Economy Info
validate_stream.send(ServerGeneral::SiteEconomy(EconomyInfo {
id: 99,
population: 55,
stock: vec![
(Good::Wood, 50.0),
(Good::Tools, 33.3),
(Good::Coin, 9000.1),
]
.into_iter()
.collect::<HashMap<Good, f32>>(),
labor_values: HashMap::new(),
values: vec![
(Good::RoadSecurity, 1.0),
(Good::Terrain(BiomeKind::Forest), 1.0),
]
.into_iter()
.collect::<HashMap<Good, f32>>(),
labors: Vec::new(),
last_exports: HashMap::new(),
resources: HashMap::new(),
}))?;
Ok(())
}
async fn init_participant(
participant: Participant,
client_sender: Sender<IncomingClient>,
@ -106,6 +221,7 @@ impl ConnectionHandler {
let character_screen_stream = participant.open(3, reliablec, 500).await?;
let in_game_stream = participant.open(3, reliablec, 100_000).await?;
let terrain_stream = participant.open(4, reliablec, 20_000).await?;
let validate_stream = participant.open(4, reliablec, 1_000_000).await?;
let server_data = receiver.recv()?;
@ -123,6 +239,11 @@ impl ConnectionHandler {
Some(client_type) => client_type?,
};
if let Err(e) = Self::dump_messages_so_client_can_validate_itself(validate_stream) {
trace!(?e, ?client_type, "a client dropped as he failed validation");
return Err(e.into());
};
let client = Client::new(
client_type,
participant,

View File

@ -139,8 +139,14 @@ impl PlayState for MainMenuState {
client::Error::AuthServerNotTrusted => localized_strings
.get("main.login.untrusted_auth_server")
.into(),
client::Error::ServerValidationFailed(server, client) => format!(
"{}: server version {}, client version {}",
localized_strings.get("main.login.outdated_client_or_server"),
server,
client,
),
client::Error::ServerWentMad => localized_strings
.get("main.login.outdated_client_or_server")
.get("main.login.prob_outdated_client_or_server")
.into(),
client::Error::ServerTimeout => {
localized_strings.get("main.login.timeout").into()