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}