valence/crates/valence_network/src/lib.rs
Ryan Johnson c5557e744d
Move packets out of valence_core. (#335)
## 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.
2023-05-29 01:36:11 -07:00

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,
}