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