valence/crates/valence_network/src/lib.rs

555 lines
19 KiB
Rust
Raw Normal View History

Reorganize Project (#321) ## Description - `valence` and `valence_protocol` have been divided into smaller crates in order to parallelize the build and improve IDE responsiveness. In the process, code architecture has been made clearer by removing circular dependencies between modules. `valence` is now just a shell around the other crates. - `workspace.packages` and `workspace.dependencies` are now used. This makes dependency managements and crate configuration much easier. - `valence_protocol` is no more. Most things from `valence_protocol` ended up in `valence_core`. We won't advertise `valence_core` as a general-purpose protocol library since it contains too much valence-specific stuff. Closes #308. - Networking code (login, initial TCP connection handling, etc.) has been extracted into the `valence_network` crate. The API has been expanded and improved with better defaults. Player counts and initial connections to the server are now tracked separately. Player counts function by default without any user configuration. - Some crates like `valence_anvil`, `valence_network`, `valence_player_list`, `valence_inventory`, etc. are now optional. They can be enabled/disabled with feature flags and `DefaultPlugins` just like bevy. - Whole-server unit tests have been moved to `valence/src/tests` in order to avoid [cyclic dev-dependencies](https://github.com/rust-lang/cargo/issues/4242). - Tools like `valence_stresser` and `packet_inspector` have been moved to a new `tools` directory. Renamed `valence_stresser` to `stresser`. Closes #241. - Moved all benches to `valence/benches/` to make them easier to run and organize. Ignoring transitive dependencies and `valence_core`, here's what the dependency graph looks like now: ```mermaid graph TD network --> client client --> instance biome --> registry dimension --> registry instance --> biome instance --> dimension instance --> entity player_list --> client inventory --> client anvil --> instance entity --> block ``` ### Issues - Inventory tests inspect many private implementation details of the inventory module, forcing us to mark things as `pub` and `#[doc(hidden)]`. It would be ideal if the tests only looked at observable behavior. - Consider moving packets in `valence_core` elsewhere. `Particle` wants to use `BlockState`, but that's defined in `valence_block`, so we can't use it without causing cycles. - Unsure what exactly should go in `valence::prelude`. - This could use some more tests of course, but I'm holding off on that until I'm confident this is the direction we want to take things. ## TODOs - [x] Update examples. - [x] Update benches. - [x] Update main README. - [x] Add short READMEs to crates. - [x] Test new schedule to ensure behavior is the same. - [x] Update tools. - [x] Copy lints to all crates. - [x] Fix docs, clippy, etc.
2023-04-22 07:43:59 +10:00
#![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;
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,
}