valence_network/lib.rs
1#![doc = include_str!("../README.md")]
2#![deny(
3 rustdoc::broken_intra_doc_links,
4 rustdoc::private_intra_doc_links,
5 rustdoc::missing_crate_level_docs,
6 rustdoc::invalid_codeblock_attributes,
7 rustdoc::invalid_rust_codeblocks,
8 rustdoc::bare_urls,
9 rustdoc::invalid_html_tags
10)]
11#![warn(
12 trivial_casts,
13 trivial_numeric_casts,
14 unused_lifetimes,
15 unused_import_braces,
16 unreachable_pub,
17 clippy::dbg_macro
18)]
19
20mod byte_channel;
21mod connect;
22mod legacy_ping;
23mod packet_io;
24
25use std::borrow::Cow;
26use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4};
27use std::sync::atomic::{AtomicUsize, Ordering};
28use std::sync::Arc;
29use std::time::Duration;
30
31use anyhow::Context;
32pub use async_trait::async_trait;
33use bevy_app::prelude::*;
34use bevy_ecs::prelude::*;
35use connect::do_accept_loop;
36pub use connect::HandshakeData;
37use flume::{Receiver, Sender};
38pub use legacy_ping::{ServerListLegacyPingPayload, ServerListLegacyPingResponse};
39use rand::rngs::OsRng;
40use rsa::{PublicKeyParts, RsaPrivateKey};
41use serde::Serialize;
42use tokio::net::UdpSocket;
43use tokio::runtime::{Handle, Runtime};
44use tokio::sync::Semaphore;
45use tokio::time;
46use tracing::error;
47use uuid::Uuid;
48use valence_protocol::text::IntoText;
49use valence_server::client::{ClientBundle, ClientBundleArgs, Properties, SpawnClientsSet};
50use valence_server::{CompressionThreshold, Server, Text, MINECRAFT_VERSION, PROTOCOL_VERSION};
51
52pub struct NetworkPlugin;
53
54impl Plugin for NetworkPlugin {
55 fn build(&self, app: &mut App) {
56 if let Err(e) = build_plugin(app) {
57 error!("failed to build network plugin: {e:#}");
58 }
59 }
60}
61
62fn build_plugin(app: &mut App) -> anyhow::Result<()> {
63 let threshold = app
64 .world
65 .get_resource::<Server>()
66 .context("missing server resource")?
67 .compression_threshold();
68
69 let settings = app
70 .world
71 .get_resource_or_insert_with(NetworkSettings::default);
72
73 let (new_clients_send, new_clients_recv) = flume::bounded(64);
74
75 let rsa_key = RsaPrivateKey::new(&mut OsRng, 1024)?;
76
77 let public_key_der =
78 rsa_der::public_key_to_der(&rsa_key.n().to_bytes_be(), &rsa_key.e().to_bytes_be())
79 .into_boxed_slice();
80
81 let runtime = if settings.tokio_handle.is_none() {
82 Some(Runtime::new()?)
83 } else {
84 None
85 };
86
87 let tokio_handle = match &runtime {
88 Some(rt) => rt.handle().clone(),
89 None => settings.tokio_handle.clone().unwrap(),
90 };
91
92 let shared = SharedNetworkState(Arc::new(SharedNetworkStateInner {
93 callbacks: settings.callbacks.clone(),
94 address: settings.address,
95 incoming_byte_limit: settings.incoming_byte_limit,
96 outgoing_byte_limit: settings.outgoing_byte_limit,
97 connection_sema: Arc::new(Semaphore::new(
98 settings.max_connections.min(Semaphore::MAX_PERMITS),
99 )),
100 player_count: AtomicUsize::new(0),
101 max_players: settings.max_players,
102 connection_mode: settings.connection_mode.clone(),
103 threshold,
104 tokio_handle,
105 _tokio_runtime: runtime,
106 new_clients_send,
107 new_clients_recv,
108 rsa_key,
109 public_key_der,
110 http_client: reqwest::Client::new(),
111 }));
112
113 app.insert_resource(shared.clone());
114
115 // System for starting the accept loop.
116 let start_accept_loop = move |shared: Res<SharedNetworkState>| {
117 let _guard = shared.0.tokio_handle.enter();
118
119 // Start accepting new connections.
120 tokio::spawn(do_accept_loop(shared.clone()));
121 };
122
123 let start_broadcast_to_lan_loop = move |shared: Res<SharedNetworkState>| {
124 let _guard = shared.0.tokio_handle.enter();
125
126 tokio::spawn(do_broadcast_to_lan_loop(shared.clone()));
127 };
128
129 // System for spawning new clients.
130 let spawn_new_clients = move |world: &mut World| {
131 for _ in 0..shared.0.new_clients_recv.len() {
132 match shared.0.new_clients_recv.try_recv() {
133 Ok(args) => world.spawn(ClientBundle::new(args)),
134 Err(_) => break,
135 };
136 }
137 };
138
139 // Start accepting connections in `PostStartup` to allow user startup code to
140 // run first.
141 app.add_systems(PostStartup, start_accept_loop);
142
143 // Start the loop that will broadcast messages for the LAN discovery list.
144 app.add_systems(PostStartup, start_broadcast_to_lan_loop);
145
146 // Spawn new clients before the event loop starts.
147 app.add_systems(PreUpdate, spawn_new_clients.in_set(SpawnClientsSet));
148
149 Ok(())
150}
151
152#[derive(Resource, Clone)]
153pub struct SharedNetworkState(Arc<SharedNetworkStateInner>);
154
155impl SharedNetworkState {
156 pub fn connection_mode(&self) -> &ConnectionMode {
157 &self.0.connection_mode
158 }
159
160 pub fn player_count(&self) -> &AtomicUsize {
161 &self.0.player_count
162 }
163
164 pub fn max_players(&self) -> usize {
165 self.0.max_players
166 }
167}
168struct SharedNetworkStateInner {
169 callbacks: ErasedNetworkCallbacks,
170 address: SocketAddr,
171 incoming_byte_limit: usize,
172 outgoing_byte_limit: usize,
173 /// Limits the number of simultaneous connections to the server before the
174 /// play state.
175 connection_sema: Arc<Semaphore>,
176 //// The number of clients in the play state, past the login state.
177 player_count: AtomicUsize,
178 max_players: usize,
179 connection_mode: ConnectionMode,
180 threshold: CompressionThreshold,
181 tokio_handle: Handle,
182 // Holding a runtime handle is not enough to keep tokio working. We need
183 // to store the runtime here so we don't drop it.
184 _tokio_runtime: Option<Runtime>,
185 /// Sender for new clients past the login stage.
186 new_clients_send: Sender<ClientBundleArgs>,
187 /// Receiver for new clients past the login stage.
188 new_clients_recv: Receiver<ClientBundleArgs>,
189 /// The RSA keypair used for encryption with clients.
190 rsa_key: RsaPrivateKey,
191 /// The public part of `rsa_key` encoded in DER, which is an ASN.1 format.
192 /// This is sent to clients during the authentication process.
193 public_key_der: Box<[u8]>,
194 /// For session server requests.
195 http_client: reqwest::Client,
196}
197
198/// Contains information about a new client joining the server.
199#[derive(Debug)]
200#[non_exhaustive]
201pub struct NewClientInfo {
202 /// The username of the new client.
203 pub username: String,
204 /// The UUID of the new client.
205 pub uuid: Uuid,
206 /// The remote address of the new client.
207 pub ip: IpAddr,
208 /// The client's properties from the game profile. Typically contains a
209 /// `textures` property with the skin and cape of the player.
210 pub properties: Properties,
211}
212
213/// Settings for [`NetworkPlugin`]. Note that mutations to these fields have no
214/// effect after the plugin is built.
215#[derive(Resource, Clone)]
216pub struct NetworkSettings {
217 pub callbacks: ErasedNetworkCallbacks,
218 /// The [`Handle`] to the tokio runtime the server will use. If `None` is
219 /// provided, the server will create its own tokio runtime at startup.
220 ///
221 /// # Default Value
222 ///
223 /// `None`
224 pub tokio_handle: Option<Handle>,
225 /// The maximum number of simultaneous initial connections to the server.
226 ///
227 /// This only considers the connections _before_ the play state where the
228 /// client is spawned into the world..
229 ///
230 /// # Default Value
231 ///
232 /// The default value is left unspecified and may change in future versions.
233 pub max_connections: usize,
234 /// # Default Value
235 ///
236 /// `20`
237 pub max_players: usize,
238 /// The socket address the server will be bound to.
239 ///
240 /// # Default Value
241 ///
242 /// `0.0.0.0:25565`, which will listen on every available network interface.
243 pub address: SocketAddr,
244 /// The connection mode. This determines if client authentication and
245 /// encryption should take place and if the server should get the player
246 /// data from a proxy.
247 ///
248 /// **NOTE:** Mutations to this field have no effect if
249 ///
250 /// # Default Value
251 ///
252 /// [`ConnectionMode::Online`]
253 pub connection_mode: ConnectionMode,
254 /// The maximum capacity (in bytes) of the buffer used to hold incoming
255 /// packet data.
256 ///
257 /// A larger capacity reduces the chance that a client needs to be
258 /// disconnected due to a full buffer, but increases potential
259 /// memory usage.
260 ///
261 /// # Default Value
262 ///
263 /// The default value is left unspecified and may change in future versions.
264 pub incoming_byte_limit: usize,
265 /// The maximum capacity (in bytes) of the buffer used to hold outgoing
266 /// packet data.
267 ///
268 /// A larger capacity reduces the chance that a client needs to be
269 /// disconnected due to a full buffer, but increases potential
270 /// memory usage.
271 ///
272 /// # Default Value
273 ///
274 /// The default value is left unspecified and may change in future versions.
275 pub outgoing_byte_limit: usize,
276}
277
278impl Default for NetworkSettings {
279 fn default() -> Self {
280 Self {
281 callbacks: ErasedNetworkCallbacks::default(),
282 tokio_handle: None,
283 max_connections: 1024,
284 max_players: 20,
285 address: SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), 25565).into(),
286 connection_mode: ConnectionMode::Online {
287 prevent_proxy_connections: false,
288 },
289 incoming_byte_limit: 2097152, // 2 MiB
290 outgoing_byte_limit: 8388608, // 8 MiB
291 }
292 }
293}
294
295/// A type-erased wrapper around an [`NetworkCallbacks`] object.
296#[derive(Clone)]
297pub struct ErasedNetworkCallbacks {
298 // TODO: do some shenanigans when async-in-trait is stabilized.
299 inner: Arc<dyn NetworkCallbacks>,
300}
301
302impl ErasedNetworkCallbacks {
303 pub fn new(callbacks: impl NetworkCallbacks) -> Self {
304 Self {
305 inner: Arc::new(callbacks),
306 }
307 }
308}
309
310impl Default for ErasedNetworkCallbacks {
311 fn default() -> Self {
312 Self {
313 inner: Arc::new(()),
314 }
315 }
316}
317
318impl<T: NetworkCallbacks> From<T> for ErasedNetworkCallbacks {
319 fn from(value: T) -> Self {
320 Self::new(value)
321 }
322}
323
324/// This trait uses [`mod@async_trait`].
325#[async_trait]
326pub trait NetworkCallbacks: Send + Sync + 'static {
327 /// Called when the server receives a Server List Ping query.
328 /// Data for the response can be provided or the query can be ignored.
329 ///
330 /// This function is called from within a tokio runtime.
331 ///
332 /// # Default Implementation
333 ///
334 /// A default placeholder response is returned.
335 async fn server_list_ping(
336 &self,
337 shared: &SharedNetworkState,
338 remote_addr: SocketAddr,
339 handshake_data: &HandshakeData,
340 ) -> ServerListPing {
341 #![allow(unused_variables)]
342
343 ServerListPing::Respond {
344 online_players: shared.player_count().load(Ordering::Relaxed) as i32,
345 max_players: shared.max_players() as i32,
346 player_sample: vec![],
347 description: "A Valence Server".into_text(),
348 favicon_png: &[],
349 version_name: MINECRAFT_VERSION.to_owned(),
350 protocol: PROTOCOL_VERSION,
351 }
352 }
353
354 /// Called when the server receives a Server List Legacy Ping query.
355 /// Data for the response can be provided or the query can be ignored.
356 ///
357 /// This function is called from within a tokio runtime.
358 ///
359 /// # Default Implementation
360 ///
361 /// [`server_list_ping`][Self::server_list_ping] re-used.
362 async fn server_list_legacy_ping(
363 &self,
364 shared: &SharedNetworkState,
365 remote_addr: SocketAddr,
366 payload: ServerListLegacyPingPayload,
367 ) -> ServerListLegacyPing {
368 #![allow(unused_variables)]
369
370 let handshake_data = match payload {
371 ServerListLegacyPingPayload::Pre1_7 {
372 protocol,
373 hostname,
374 port,
375 } => HandshakeData {
376 protocol_version: protocol,
377 server_address: hostname,
378 server_port: port,
379 },
380 _ => HandshakeData::default(),
381 };
382
383 match self
384 .server_list_ping(shared, remote_addr, &handshake_data)
385 .await
386 {
387 ServerListPing::Respond {
388 online_players,
389 max_players,
390 player_sample,
391 description,
392 favicon_png,
393 version_name,
394 protocol,
395 } => ServerListLegacyPing::Respond(
396 ServerListLegacyPingResponse::new(protocol, online_players, max_players)
397 .version(version_name)
398 .description(description.to_legacy_lossy()),
399 ),
400 ServerListPing::Ignore => ServerListLegacyPing::Ignore,
401 }
402 }
403
404 /// This function is called every 1.5 seconds to broadcast a packet over the
405 /// local network in order to advertise the server to the multiplayer
406 /// screen with a configurable MOTD.
407 ///
408 /// # Default Implementation
409 ///
410 /// The default implementation returns [BroadcastToLan::Disabled], disabling
411 /// LAN discovery.
412 async fn broadcast_to_lan(&self, shared: &SharedNetworkState) -> BroadcastToLan {
413 #![allow(unused_variables)]
414
415 BroadcastToLan::Disabled
416 }
417
418 /// Called for each client (after successful authentication if online mode
419 /// is enabled) to determine if they can join the server.
420 /// - If `Err(reason)` is returned, then the client is immediately
421 /// disconnected with `reason` as the displayed message.
422 /// - Otherwise, `Ok(f)` is returned and the client will continue the login
423 /// process. This _may_ result in a new client being spawned with the
424 /// [`ClientBundle`] components. `f` is stored along with the client and
425 /// is called when the client is disconnected.
426 ///
427 /// `f` is a callback function used for handling resource cleanup when the
428 /// client is dropped. This is useful because a new client entity is not
429 /// necessarily spawned into the world after a successful login.
430 ///
431 /// This method is called from within a tokio runtime, and is the
432 /// appropriate place to perform asynchronous operations such as
433 /// database queries which may take some time to complete.
434 ///
435 /// # Default Implementation
436 ///
437 /// TODO
438 ///
439 /// [`Client`]: valence::client::Client
440 async fn login(
441 &self,
442 shared: &SharedNetworkState,
443 info: &NewClientInfo,
444 ) -> Result<CleanupFn, Text> {
445 let _ = info;
446
447 let max_players = shared.max_players();
448
449 let success = shared
450 .player_count()
451 .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |n| {
452 if n < max_players {
453 Some(n + 1)
454 } else {
455 None
456 }
457 })
458 .is_ok();
459
460 if success {
461 let shared = shared.clone();
462
463 Ok(Box::new(move || {
464 let prev = shared.player_count().fetch_sub(1, Ordering::SeqCst);
465 debug_assert_ne!(prev, 0, "player count underflowed");
466 }))
467 } else {
468 // TODO: use correct translation key.
469 Err("Server Full".into_text())
470 }
471 }
472
473 /// Called upon every client login to obtain the full URL to use for session
474 /// server requests. This is done to authenticate player accounts. This
475 /// method is not called unless [online mode] is enabled.
476 ///
477 /// It is assumed that upon successful request, a structure matching the
478 /// description in the [wiki](https://wiki.vg/Protocol_Encryption#Server) was obtained.
479 /// Providing a URL that does not return such a structure will result in a
480 /// disconnect for every client that connects.
481 ///
482 /// The arguments are described in the linked wiki article.
483 ///
484 /// # Default Implementation
485 ///
486 /// Uses the official Minecraft session server. This is formatted as
487 /// `https://sessionserver.mojang.com/session/minecraft/hasJoined?username=<username>&serverId=<auth-digest>&ip=<player-ip>`.
488 ///
489 /// [online mode]: ConnectionMode::Online
490 async fn session_server(
491 &self,
492 shared: &SharedNetworkState,
493 username: &str,
494 auth_digest: &str,
495 player_ip: &IpAddr,
496 ) -> String {
497 if shared.connection_mode()
498 == (&ConnectionMode::Online {
499 prevent_proxy_connections: true,
500 })
501 {
502 format!("https://sessionserver.mojang.com/session/minecraft/hasJoined?username={username}&serverId={auth_digest}&ip={player_ip}")
503 } else {
504 format!("https://sessionserver.mojang.com/session/minecraft/hasJoined?username={username}&serverId={auth_digest}")
505 }
506 }
507}
508
509/// A callback function called when the associated client is dropped. See
510/// [`NetworkCallbacks::login`] for more information.
511pub type CleanupFn = Box<dyn FnOnce() + Send + Sync + 'static>;
512struct CleanupOnDrop(Option<CleanupFn>);
513
514impl Drop for CleanupOnDrop {
515 fn drop(&mut self) {
516 if let Some(f) = self.0.take() {
517 f();
518 }
519 }
520}
521
522/// The default network callbacks. Useful as a placeholder.
523impl NetworkCallbacks for () {}
524
525/// Describes how new connections to the server are handled.
526#[derive(Clone, PartialEq)]
527#[non_exhaustive]
528pub enum ConnectionMode {
529 /// The "online mode" fetches all player data (username, UUID, and
530 /// properties) from the [configured session server] and enables
531 /// encryption.
532 ///
533 /// This mode should be used by all publicly exposed servers which are not
534 /// behind a proxy.
535 ///
536 /// [configured session server]: NetworkCallbacks::session_server
537 Online {
538 /// Determines if client IP validation should take place during
539 /// authentication.
540 ///
541 /// When `prevent_proxy_connections` is enabled, clients can no longer
542 /// log-in if they connected to the Yggdrasil server using a different
543 /// IP than the one used to connect to this server.
544 ///
545 /// This is used by the default implementation of
546 /// [`NetworkCallbacks::session_server`]. A different implementation may
547 /// choose to ignore this value.
548 prevent_proxy_connections: bool,
549 },
550 /// Disables client authentication with the configured session server.
551 /// Clients can join with any username and UUID they choose, potentially
552 /// gaining privileges they would not otherwise have. Additionally,
553 /// encryption is disabled and Minecraft's default skins will be used.
554 ///
555 /// This mode should be used for development purposes only and not for
556 /// publicly exposed servers.
557 Offline,
558 /// This mode should be used under one of the following situations:
559 /// - The server is behind a [BungeeCord]/[Waterfall] proxy with IP
560 /// forwarding enabled.
561 /// - The server is behind a [Velocity] proxy configured to use the `legacy`
562 /// forwarding mode.
563 ///
564 /// All player data (username, UUID, and properties) is fetched from the
565 /// proxy, but no attempt is made to stop connections originating from
566 /// elsewhere. As a result, you must ensure clients connect through the
567 /// proxy and are unable to connect to the server directly. Otherwise,
568 /// clients can use any username or UUID they choose similar to
569 /// [`ConnectionMode::Offline`].
570 ///
571 /// To protect against this, a firewall can be used. However,
572 /// [`ConnectionMode::Velocity`] is recommended as a secure alternative.
573 ///
574 /// [BungeeCord]: https://www.spigotmc.org/wiki/bungeecord/
575 /// [Waterfall]: https://github.com/PaperMC/Waterfall
576 /// [Velocity]: https://velocitypowered.com/
577 BungeeCord,
578 /// This mode is used when the server is behind a [Velocity] proxy
579 /// configured with the forwarding mode `modern`.
580 ///
581 /// All player data (username, UUID, and properties) is fetched from the
582 /// proxy and all connections originating from outside Velocity are
583 /// blocked.
584 ///
585 /// [Velocity]: https://velocitypowered.com/
586 Velocity {
587 /// The secret key used to prevent connections from outside Velocity.
588 /// The proxy and Valence must be configured to use the same secret key.
589 secret: Arc<str>,
590 },
591}
592
593/// The result of the Server List Ping [callback].
594///
595/// [callback]: NetworkCallbacks::server_list_ping
596#[derive(Clone, Default, Debug)]
597pub enum ServerListPing<'a> {
598 /// Responds to the server list ping with the given information.
599 Respond {
600 /// Displayed as the number of players on the server.
601 online_players: i32,
602 /// Displayed as the maximum number of players allowed on the server at
603 /// a time.
604 max_players: i32,
605 /// The list of players visible by hovering over the player count.
606 ///
607 /// Has no effect if this list is empty.
608 player_sample: Vec<PlayerSampleEntry>,
609 /// A description of the server.
610 description: Text,
611 /// The server's icon as the bytes of a PNG image.
612 /// The image must be 64x64 pixels.
613 ///
614 /// No icon is used if the slice is empty.
615 favicon_png: &'a [u8],
616 /// The version name of the server. Displayed when client is using a
617 /// different protocol.
618 ///
619 /// Can be formatted using `ยง` and format codes. Or use
620 /// [`valence_protocol::text::Text::to_legacy_lossy`].
621 version_name: String,
622 /// The protocol version of the server.
623 protocol: i32,
624 },
625 /// Ignores the query and disconnects from the client.
626 #[default]
627 Ignore,
628}
629
630/// The result of the Server List Legacy Ping [callback].
631///
632/// [callback]: NetworkCallbacks::server_list_legacy_ping
633#[derive(Clone, Default, Debug)]
634pub enum ServerListLegacyPing {
635 /// Responds to the server list legacy ping with the given information.
636 Respond(ServerListLegacyPingResponse),
637 /// Ignores the query and disconnects from the client.
638 #[default]
639 Ignore,
640}
641
642/// The result of the Broadcast To Lan [callback].
643///
644/// [callback]: NetworkCallbacks::broadcast_to_lan
645#[derive(Clone, Default, Debug)]
646pub enum BroadcastToLan<'a> {
647 /// Disabled Broadcast To Lan.
648 #[default]
649 Disabled,
650 /// Send packet to broadcast to LAN every 1.5 seconds with specified MOTD.
651 Enabled(Cow<'a, str>),
652}
653
654/// Represents an individual entry in the player sample.
655#[derive(Clone, Debug, Serialize)]
656pub struct PlayerSampleEntry {
657 /// The name of the player.
658 ///
659 /// This string can contain
660 /// [legacy formatting codes](https://minecraft.fandom.com/wiki/Formatting_codes).
661 pub name: String,
662 /// The player UUID.
663 pub id: Uuid,
664}
665
666async fn do_broadcast_to_lan_loop(shared: SharedNetworkState) {
667 let port = shared.0.address.port();
668
669 let Ok(socket) = UdpSocket::bind("0.0.0.0:0").await else {
670 tracing::error!("Failed to bind to UDP socket for broadcast to LAN");
671 return;
672 };
673
674 loop {
675 let motd = match shared.0.callbacks.inner.broadcast_to_lan(&shared).await {
676 BroadcastToLan::Disabled => {
677 time::sleep(Duration::from_millis(1500)).await;
678 continue;
679 }
680 BroadcastToLan::Enabled(motd) => motd,
681 };
682
683 let message = format!("[MOTD]{motd}[/MOTD][AD]{port}[/AD]");
684
685 if let Err(e) = socket.send_to(message.as_bytes(), "224.0.2.60:4445").await {
686 tracing::warn!("Failed to send broadcast to LAN packet: {}", e);
687 }
688
689 // wait 1.5 seconds
690 tokio::time::sleep(std::time::Duration::from_millis(1500)).await;
691 }
692}