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}