mirror of
https://github.com/italicsjenga/valence.git
synced 2024-12-24 06:51:30 +11:00
c5557e744d
## Description - Move all packets out of `valence_core` and into the places where they're actually used. This has a few benefits: - Avoids compiling code for packets that go unused when feature flags are disabled. - Code is distributed more uniformly across crates, improving compilation times. - Improves local reasoning when everything relevant to a module is defined in the same place. - Easier to share code between the packet consumer and the packet. - Tweak `Packet` macro syntax. - Update `syn` to 2.0. - Reorganize some code in `valence_client` (needs further work). - Impl `WritePacket` for `Instance`. - Remove packet enums such as `S2cPlayPacket` and `C2sPlayPacket`. - Replace `assert_packet_count` and `assert_packet_order` macros with non-macro methods. To prevent this PR from getting out of hand, I've disabled the packet inspector and stresser until they have been rewritten to account for these changes.
556 lines
19 KiB
Rust
556 lines
19 KiB
Rust
#![doc = include_str!("../README.md")]
|
|
#![deny(
|
|
rustdoc::broken_intra_doc_links,
|
|
rustdoc::private_intra_doc_links,
|
|
rustdoc::missing_crate_level_docs,
|
|
rustdoc::invalid_codeblock_attributes,
|
|
rustdoc::invalid_rust_codeblocks,
|
|
rustdoc::bare_urls,
|
|
rustdoc::invalid_html_tags
|
|
)]
|
|
#![warn(
|
|
trivial_casts,
|
|
trivial_numeric_casts,
|
|
unused_lifetimes,
|
|
unused_import_braces,
|
|
unreachable_pub,
|
|
clippy::dbg_macro
|
|
)]
|
|
|
|
mod byte_channel;
|
|
mod connect;
|
|
pub mod packet;
|
|
mod packet_io;
|
|
|
|
use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4};
|
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
use std::sync::Arc;
|
|
|
|
use anyhow::Context;
|
|
pub use async_trait::async_trait;
|
|
use bevy_app::prelude::*;
|
|
use bevy_ecs::prelude::*;
|
|
use connect::do_accept_loop;
|
|
use flume::{Receiver, Sender};
|
|
use rand::rngs::OsRng;
|
|
use rsa::{PublicKeyParts, RsaPrivateKey};
|
|
use serde::Serialize;
|
|
use tokio::runtime::{Handle, Runtime};
|
|
use tokio::sync::Semaphore;
|
|
use tracing::error;
|
|
use uuid::Uuid;
|
|
use valence_client::{ClientBundle, ClientBundleArgs, Properties, SpawnClientsSet};
|
|
use valence_core::text::Text;
|
|
use valence_core::Server;
|
|
|
|
pub struct NetworkPlugin;
|
|
|
|
impl Plugin for NetworkPlugin {
|
|
fn build(&self, app: &mut App) {
|
|
if let Err(e) = build_plugin(app) {
|
|
error!("failed to build network plugin: {e:#}");
|
|
}
|
|
}
|
|
}
|
|
|
|
fn build_plugin(app: &mut App) -> anyhow::Result<()> {
|
|
let compression_threshold = app
|
|
.world
|
|
.get_resource::<Server>()
|
|
.context("missing server resource")?
|
|
.compression_threshold();
|
|
|
|
let settings = app
|
|
.world
|
|
.get_resource_or_insert_with(NetworkSettings::default);
|
|
|
|
let (new_clients_send, new_clients_recv) = flume::bounded(64);
|
|
|
|
let rsa_key = RsaPrivateKey::new(&mut OsRng, 1024)?;
|
|
|
|
let public_key_der =
|
|
rsa_der::public_key_to_der(&rsa_key.n().to_bytes_be(), &rsa_key.e().to_bytes_be())
|
|
.into_boxed_slice();
|
|
|
|
let runtime = if settings.tokio_handle.is_none() {
|
|
Some(Runtime::new()?)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let tokio_handle = match &runtime {
|
|
Some(rt) => rt.handle().clone(),
|
|
None => settings.tokio_handle.clone().unwrap(),
|
|
};
|
|
|
|
let shared = SharedNetworkState(Arc::new(SharedNetworkStateInner {
|
|
callbacks: settings.callbacks.clone(),
|
|
address: settings.address,
|
|
incoming_byte_limit: settings.incoming_byte_limit,
|
|
outgoing_byte_limit: settings.outgoing_byte_limit,
|
|
connection_sema: Arc::new(Semaphore::new(
|
|
settings.max_connections.min(Semaphore::MAX_PERMITS),
|
|
)),
|
|
player_count: AtomicUsize::new(0),
|
|
max_players: settings.max_players,
|
|
connection_mode: settings.connection_mode.clone(),
|
|
compression_threshold,
|
|
tokio_handle,
|
|
_tokio_runtime: runtime,
|
|
new_clients_send,
|
|
new_clients_recv,
|
|
rsa_key,
|
|
public_key_der,
|
|
http_client: reqwest::Client::new(),
|
|
}));
|
|
|
|
app.insert_resource(shared.clone());
|
|
|
|
// System for starting the accept loop.
|
|
let start_accept_loop = move |shared: Res<SharedNetworkState>| {
|
|
let _guard = shared.0.tokio_handle.enter();
|
|
|
|
// Start accepting new connections.
|
|
tokio::spawn(do_accept_loop(shared.clone()));
|
|
};
|
|
|
|
// System for spawning new clients.
|
|
let spawn_new_clients = move |world: &mut World| {
|
|
for _ in 0..shared.0.new_clients_recv.len() {
|
|
match shared.0.new_clients_recv.try_recv() {
|
|
Ok(args) => world.spawn(ClientBundle::new(args)),
|
|
Err(_) => break,
|
|
};
|
|
}
|
|
};
|
|
|
|
// Start accepting connections in `PostStartup` to allow user startup code to
|
|
// run first.
|
|
app.add_system(
|
|
start_accept_loop
|
|
.in_schedule(CoreSchedule::Startup)
|
|
.in_base_set(StartupSet::PostStartup),
|
|
);
|
|
|
|
// Spawn new clients before the event loop starts.
|
|
app.add_system(spawn_new_clients.in_set(SpawnClientsSet));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[derive(Resource, Clone)]
|
|
pub struct SharedNetworkState(Arc<SharedNetworkStateInner>);
|
|
|
|
impl SharedNetworkState {
|
|
pub fn connection_mode(&self) -> &ConnectionMode {
|
|
&self.0.connection_mode
|
|
}
|
|
|
|
pub fn player_count(&self) -> &AtomicUsize {
|
|
&self.0.player_count
|
|
}
|
|
|
|
pub fn max_players(&self) -> usize {
|
|
self.0.max_players
|
|
}
|
|
}
|
|
struct SharedNetworkStateInner {
|
|
callbacks: ErasedNetworkCallbacks,
|
|
address: SocketAddr,
|
|
incoming_byte_limit: usize,
|
|
outgoing_byte_limit: usize,
|
|
/// Limits the number of simultaneous connections to the server before the
|
|
/// play state.
|
|
connection_sema: Arc<Semaphore>,
|
|
//// The number of clients in the play state, past the login state.
|
|
player_count: AtomicUsize,
|
|
max_players: usize,
|
|
connection_mode: ConnectionMode,
|
|
compression_threshold: Option<u32>,
|
|
tokio_handle: Handle,
|
|
// Holding a runtime handle is not enough to keep tokio working. We need
|
|
// to store the runtime here so we don't drop it.
|
|
_tokio_runtime: Option<Runtime>,
|
|
/// Sender for new clients past the login stage.
|
|
new_clients_send: Sender<ClientBundleArgs>,
|
|
/// Receiver for new clients past the login stage.
|
|
new_clients_recv: Receiver<ClientBundleArgs>,
|
|
/// The RSA keypair used for encryption with clients.
|
|
rsa_key: RsaPrivateKey,
|
|
/// The public part of `rsa_key` encoded in DER, which is an ASN.1 format.
|
|
/// This is sent to clients during the authentication process.
|
|
public_key_der: Box<[u8]>,
|
|
/// For session server requests.
|
|
http_client: reqwest::Client,
|
|
}
|
|
|
|
/// Contains information about a new client joining the server.
|
|
#[derive(Debug)]
|
|
#[non_exhaustive]
|
|
pub struct NewClientInfo {
|
|
/// The username of the new client.
|
|
pub username: String,
|
|
/// The UUID of the new client.
|
|
pub uuid: Uuid,
|
|
/// The remote address of the new client.
|
|
pub ip: IpAddr,
|
|
/// The client's properties from the game profile. Typically contains a
|
|
/// `textures` property with the skin and cape of the player.
|
|
pub properties: Properties,
|
|
}
|
|
|
|
/// Settings for [`NetworkPlugin`]. Note that mutations to these fields have no
|
|
/// effect after the plugin is built.
|
|
#[derive(Resource, Clone)]
|
|
pub struct NetworkSettings {
|
|
pub callbacks: ErasedNetworkCallbacks,
|
|
/// The [`Handle`] to the tokio runtime the server will use. If `None` is
|
|
/// provided, the server will create its own tokio runtime at startup.
|
|
///
|
|
/// # Default Value
|
|
///
|
|
/// `None`
|
|
pub tokio_handle: Option<Handle>,
|
|
/// The maximum number of simultaneous initial connections to the server.
|
|
///
|
|
/// This only considers the connections _before_ the play state where the
|
|
/// client is spawned into the world..
|
|
///
|
|
/// # Default Value
|
|
///
|
|
/// The default value is left unspecified and may change in future versions.
|
|
pub max_connections: usize,
|
|
/// # Default Value
|
|
///
|
|
/// `20`
|
|
pub max_players: usize,
|
|
/// The socket address the server will be bound to.
|
|
///
|
|
/// # Default Value
|
|
///
|
|
/// `0.0.0.0:25565`, which will listen on every available network interface.
|
|
pub address: SocketAddr,
|
|
/// The connection mode. This determines if client authentication and
|
|
/// encryption should take place and if the server should get the player
|
|
/// data from a proxy.
|
|
///
|
|
/// **NOTE:** Mutations to this field have no effect if
|
|
///
|
|
/// # Default Value
|
|
///
|
|
/// [`ConnectionMode::Online`]
|
|
pub connection_mode: ConnectionMode,
|
|
/// The maximum capacity (in bytes) of the buffer used to hold incoming
|
|
/// packet data.
|
|
///
|
|
/// A larger capacity reduces the chance that a client needs to be
|
|
/// disconnected due to a full buffer, but increases potential
|
|
/// memory usage.
|
|
///
|
|
/// # Default Value
|
|
///
|
|
/// The default value is left unspecified and may change in future versions.
|
|
pub incoming_byte_limit: usize,
|
|
/// The maximum capacity (in bytes) of the buffer used to hold outgoing
|
|
/// packet data.
|
|
///
|
|
/// A larger capacity reduces the chance that a client needs to be
|
|
/// disconnected due to a full buffer, but increases potential
|
|
/// memory usage.
|
|
///
|
|
/// # Default Value
|
|
///
|
|
/// The default value is left unspecified and may change in future versions.
|
|
pub outgoing_byte_limit: usize,
|
|
}
|
|
|
|
impl Default for NetworkSettings {
|
|
fn default() -> Self {
|
|
Self {
|
|
callbacks: ErasedNetworkCallbacks::default(),
|
|
tokio_handle: None,
|
|
max_connections: 1024,
|
|
max_players: 20,
|
|
address: SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), 25565).into(),
|
|
connection_mode: ConnectionMode::Online {
|
|
prevent_proxy_connections: false,
|
|
},
|
|
incoming_byte_limit: 2097152, // 2 MiB
|
|
outgoing_byte_limit: 8388608, // 8 MiB
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A type-erased wrapper around an [`NetworkCallbacks`] object.
|
|
#[derive(Clone)]
|
|
pub struct ErasedNetworkCallbacks {
|
|
// TODO: do some shenanigans when async-in-trait is stabilized.
|
|
inner: Arc<dyn NetworkCallbacks>,
|
|
}
|
|
|
|
impl ErasedNetworkCallbacks {
|
|
pub fn new(callbacks: impl NetworkCallbacks) -> Self {
|
|
Self {
|
|
inner: Arc::new(callbacks),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for ErasedNetworkCallbacks {
|
|
fn default() -> Self {
|
|
Self {
|
|
inner: Arc::new(()),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<T: NetworkCallbacks> From<T> for ErasedNetworkCallbacks {
|
|
fn from(value: T) -> Self {
|
|
Self::new(value)
|
|
}
|
|
}
|
|
|
|
/// This trait uses [`mod@async_trait`].
|
|
#[async_trait]
|
|
pub trait NetworkCallbacks: Send + Sync + 'static {
|
|
/// Called when the server receives a Server List Ping query.
|
|
/// Data for the response can be provided or the query can be ignored.
|
|
///
|
|
/// This function is called from within a tokio runtime.
|
|
///
|
|
/// # Default Implementation
|
|
///
|
|
/// A default placeholder response is returned.
|
|
async fn server_list_ping(
|
|
&self,
|
|
shared: &SharedNetworkState,
|
|
remote_addr: SocketAddr,
|
|
protocol_version: i32,
|
|
) -> ServerListPing {
|
|
#![allow(unused_variables)]
|
|
|
|
ServerListPing::Respond {
|
|
online_players: shared.player_count().load(Ordering::Relaxed) as i32,
|
|
max_players: shared.max_players() as i32,
|
|
player_sample: vec![],
|
|
description: "A Valence Server".into(),
|
|
favicon_png: &[],
|
|
}
|
|
}
|
|
|
|
/// Called for each client (after successful authentication if online mode
|
|
/// is enabled) to determine if they can join the server.
|
|
/// - If `Err(reason)` is returned, then the client is immediately
|
|
/// disconnected with `reason` as the displayed message.
|
|
/// - Otherwise, `Ok(f)` is returned and the client will continue the login
|
|
/// process. This _may_ result in a new client being spawned with the
|
|
/// [`ClientBundle`] components. `f` is stored along with the client and
|
|
/// is called when the client is disconnected.
|
|
///
|
|
/// `f` is a callback function used for handling resource cleanup when the
|
|
/// client is dropped. This is useful because a new client entity is not
|
|
/// necessarily spawned into the world after a successful login.
|
|
///
|
|
/// This method is called from within a tokio runtime, and is the
|
|
/// appropriate place to perform asynchronous operations such as
|
|
/// database queries which may take some time to complete.
|
|
///
|
|
/// # Default Implementation
|
|
///
|
|
/// TODO
|
|
///
|
|
/// [`Client`]: valence::client::Client
|
|
async fn login(
|
|
&self,
|
|
shared: &SharedNetworkState,
|
|
info: &NewClientInfo,
|
|
) -> Result<CleanupFn, Text> {
|
|
let _ = info;
|
|
|
|
let max_players = shared.max_players();
|
|
|
|
let success = shared
|
|
.player_count()
|
|
.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |n| {
|
|
if n < max_players {
|
|
Some(n + 1)
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.is_ok();
|
|
|
|
if success {
|
|
let shared = shared.clone();
|
|
|
|
Ok(Box::new(move || {
|
|
let prev = shared.player_count().fetch_sub(1, Ordering::SeqCst);
|
|
debug_assert_ne!(prev, 0, "player count underflowed");
|
|
}))
|
|
} else {
|
|
// TODO: use correct translation key.
|
|
Err("Server Full".into())
|
|
}
|
|
}
|
|
|
|
/// Called upon every client login to obtain the full URL to use for session
|
|
/// server requests. This is done to authenticate player accounts. This
|
|
/// method is not called unless [online mode] is enabled.
|
|
///
|
|
/// It is assumed that upon successful request, a structure matching the
|
|
/// description in the [wiki](https://wiki.vg/Protocol_Encryption#Server) was obtained.
|
|
/// Providing a URL that does not return such a structure will result in a
|
|
/// disconnect for every client that connects.
|
|
///
|
|
/// The arguments are described in the linked wiki article.
|
|
///
|
|
/// # Default Implementation
|
|
///
|
|
/// Uses the official Minecraft session server. This is formatted as
|
|
/// `https://sessionserver.mojang.com/session/minecraft/hasJoined?username=<username>&serverId=<auth-digest>&ip=<player-ip>`.
|
|
///
|
|
/// [online mode]: ConnectionMode::Online
|
|
async fn session_server(
|
|
&self,
|
|
shared: &SharedNetworkState,
|
|
username: &str,
|
|
auth_digest: &str,
|
|
player_ip: &IpAddr,
|
|
) -> String {
|
|
if shared.connection_mode()
|
|
== (&ConnectionMode::Online {
|
|
prevent_proxy_connections: true,
|
|
})
|
|
{
|
|
format!("https://sessionserver.mojang.com/session/minecraft/hasJoined?username={username}&serverId={auth_digest}&ip={player_ip}")
|
|
} else {
|
|
format!("https://sessionserver.mojang.com/session/minecraft/hasJoined?username={username}&serverId={auth_digest}")
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A callback function called when the associated client is dropped. See
|
|
/// [`NetworkCallbacks::login`] for more information.
|
|
pub type CleanupFn = Box<dyn FnOnce() + Send + Sync + 'static>;
|
|
struct CleanupOnDrop(Option<CleanupFn>);
|
|
|
|
impl Drop for CleanupOnDrop {
|
|
fn drop(&mut self) {
|
|
if let Some(f) = self.0.take() {
|
|
f();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The default network callbacks. Useful as a placeholder.
|
|
impl NetworkCallbacks for () {}
|
|
|
|
/// Describes how new connections to the server are handled.
|
|
#[derive(Clone, PartialEq)]
|
|
#[non_exhaustive]
|
|
pub enum ConnectionMode {
|
|
/// The "online mode" fetches all player data (username, UUID, and
|
|
/// properties) from the [configured session server] and enables
|
|
/// encryption.
|
|
///
|
|
/// This mode should be used by all publicly exposed servers which are not
|
|
/// behind a proxy.
|
|
///
|
|
/// [configured session server]: NetworkCallbacks::session_server
|
|
Online {
|
|
/// Determines if client IP validation should take place during
|
|
/// authentication.
|
|
///
|
|
/// When `prevent_proxy_connections` is enabled, clients can no longer
|
|
/// log-in if they connected to the Yggdrasil server using a different
|
|
/// IP than the one used to connect to this server.
|
|
///
|
|
/// This is used by the default implementation of
|
|
/// [`NetworkCallbacks::session_server`]. A different implementation may
|
|
/// choose to ignore this value.
|
|
prevent_proxy_connections: bool,
|
|
},
|
|
/// Disables client authentication with the configured session server.
|
|
/// Clients can join with any username and UUID they choose, potentially
|
|
/// gaining privileges they would not otherwise have. Additionally,
|
|
/// encryption is disabled and Minecraft's default skins will be used.
|
|
///
|
|
/// This mode should be used for development purposes only and not for
|
|
/// publicly exposed servers.
|
|
Offline,
|
|
/// This mode should be used under one of the following situations:
|
|
/// - The server is behind a [BungeeCord]/[Waterfall] proxy with IP
|
|
/// forwarding enabled.
|
|
/// - The server is behind a [Velocity] proxy configured to use the `legacy`
|
|
/// forwarding mode.
|
|
///
|
|
/// All player data (username, UUID, and properties) is fetched from the
|
|
/// proxy, but no attempt is made to stop connections originating from
|
|
/// elsewhere. As a result, you must ensure clients connect through the
|
|
/// proxy and are unable to connect to the server directly. Otherwise,
|
|
/// clients can use any username or UUID they choose similar to
|
|
/// [`ConnectionMode::Offline`].
|
|
///
|
|
/// To protect against this, a firewall can be used. However,
|
|
/// [`ConnectionMode::Velocity`] is recommended as a secure alternative.
|
|
///
|
|
/// [BungeeCord]: https://www.spigotmc.org/wiki/bungeecord/
|
|
/// [Waterfall]: https://github.com/PaperMC/Waterfall
|
|
/// [Velocity]: https://velocitypowered.com/
|
|
BungeeCord,
|
|
/// This mode is used when the server is behind a [Velocity] proxy
|
|
/// configured with the forwarding mode `modern`.
|
|
///
|
|
/// All player data (username, UUID, and properties) is fetched from the
|
|
/// proxy and all connections originating from outside Velocity are
|
|
/// blocked.
|
|
///
|
|
/// [Velocity]: https://velocitypowered.com/
|
|
Velocity {
|
|
/// The secret key used to prevent connections from outside Velocity.
|
|
/// The proxy and Valence must be configured to use the same secret key.
|
|
secret: Arc<str>,
|
|
},
|
|
}
|
|
|
|
/// The result of the Server List Ping [callback].
|
|
///
|
|
/// [callback]: NetworkCallbacks::server_list_ping
|
|
#[derive(Clone, Default, Debug)]
|
|
pub enum ServerListPing<'a> {
|
|
/// Responds to the server list ping with the given information.
|
|
Respond {
|
|
/// Displayed as the number of players on the server.
|
|
online_players: i32,
|
|
/// Displayed as the maximum number of players allowed on the server at
|
|
/// a time.
|
|
max_players: i32,
|
|
/// The list of players visible by hovering over the player count.
|
|
///
|
|
/// Has no effect if this list is empty.
|
|
player_sample: Vec<PlayerSampleEntry>,
|
|
/// A description of the server.
|
|
description: Text,
|
|
/// The server's icon as the bytes of a PNG image.
|
|
/// The image must be 64x64 pixels.
|
|
///
|
|
/// No icon is used if the slice is empty.
|
|
favicon_png: &'a [u8],
|
|
},
|
|
/// Ignores the query and disconnects from the client.
|
|
#[default]
|
|
Ignore,
|
|
}
|
|
|
|
/// Represents an individual entry in the player sample.
|
|
#[derive(Clone, Debug, Serialize)]
|
|
pub struct PlayerSampleEntry {
|
|
/// The name of the player.
|
|
///
|
|
/// This string can contain
|
|
/// [legacy formatting codes](https://minecraft.fandom.com/wiki/Formatting_codes).
|
|
pub name: String,
|
|
/// The player UUID.
|
|
pub id: Uuid,
|
|
}
|