elytra_ping/
bedrock.rs

1use bytes::{Buf, BufMut};
2use chrono::Utc;
3use snafu::{Backtrace, OptionExt, ResultExt, Snafu};
4use std::fmt::Write;
5use std::{
6    io::{Cursor, Read},
7    net::AddrParseError,
8    str::FromStr,
9    time::Duration,
10    vec,
11};
12use tokio::net::{lookup_host, UdpSocket};
13use tracing::{debug, trace};
14
15#[derive(Debug, Hash, Clone, PartialEq, Eq)]
16#[non_exhaustive]
17pub struct BedrockServerInfo {
18    /// Usually "MCPE" for bedrock or "MCEE" for education edition.
19    pub edition: String,
20    pub name: String,
21    pub protocol_version: u32,
22    pub mc_version: String,
23    pub online_players: u32,
24    pub max_players: u32,
25    pub server_id: Option<u64>,
26    pub map_name: Option<String>,
27    pub game_mode: Option<String>,
28    pub numeric_game_mode: Option<u64>,
29    pub ipv4_port: Option<u16>,
30    pub ipv6_port: Option<u16>,
31    pub extra: Vec<String>,
32}
33
34#[cfg(feature = "java_parse")]
35impl From<BedrockServerInfo> for crate::JavaServerInfo {
36    fn from(value: BedrockServerInfo) -> Self {
37        let mut description = value.name;
38        if let Some(map_name) = value.map_name {
39            write!(description, "\n§r{map_name}").unwrap();
40        }
41        crate::JavaServerInfo {
42            version: None,
43            players: Some(crate::parse::ServerPlayers {
44                max: value.max_players,
45                online: value.online_players,
46                sample: None,
47            }),
48            description: crate::parse::TextComponent::Plain(description),
49            favicon: None,
50            mod_info: None,
51            enforces_secure_chat: None,
52            prevents_chat_reports: None,
53            previews_chat: None,
54        }
55    }
56}
57
58/// Server MOTD string is missing information.
59#[derive(Debug, Snafu)]
60pub struct ServerInfoParseError;
61
62impl FromStr for BedrockServerInfo {
63    type Err = ServerInfoParseError;
64
65    fn from_str(s: &str) -> Result<Self, Self::Err> {
66        fn parse_impl(s: &str) -> Option<BedrockServerInfo> {
67            let mut components = s.split(';').map(|component| component.to_owned());
68            Some(BedrockServerInfo {
69                edition: components.next()?,
70                name: components.next()?,
71                protocol_version: components.next().and_then(|s| s.parse().ok())?,
72                mc_version: components.next()?,
73                online_players: components.next().and_then(|s| s.parse().ok())?,
74                max_players: components.next().and_then(|s| s.parse().ok())?,
75                server_id: components.next().and_then(|s| s.parse().ok()),
76                map_name: components.next(),
77                game_mode: components.next(),
78                numeric_game_mode: components.next().and_then(|s| s.parse().ok()),
79                ipv4_port: components.next().and_then(|s| s.parse().ok()),
80                ipv6_port: components.next().and_then(|s| s.parse().ok()),
81                extra: components.collect(),
82            })
83        }
84
85        parse_impl(s).ok_or(ServerInfoParseError)
86    }
87}
88
89#[derive(Debug, Snafu)]
90pub enum BedrockPingError {
91    /// Failed to parse address.
92    #[snafu(display("Failed to parse address {address:?}: {source}"))]
93    AddressParse {
94        source: AddrParseError,
95        address: String,
96        backtrace: Backtrace,
97    },
98    /// The server did not respond to the ping request.
99    NoResponse { backtrace: Backtrace },
100    /// Failed to parse server info.
101    #[snafu(display("Failed to parse server info: {source}"), context(false))]
102    ServerInfoParse {
103        source: ServerInfoParseError,
104        backtrace: Backtrace,
105    },
106    /// I/O error.
107    #[snafu(display("I/O error: {source}"), context(false))]
108    Io {
109        source: std::io::Error,
110        backtrace: Backtrace,
111    },
112    /// DNS lookup failed.
113    #[snafu(display("DNS lookup failed for address `{address}`"))]
114    DNSLookupFailed {
115        address: String,
116        backtrace: Backtrace,
117    },
118    /// Failed to open socket.
119    #[snafu(display("Failed to open socket: {source}"))]
120    ConnectFailed {
121        source: std::io::Error,
122        backtrace: Backtrace,
123    },
124}
125
126pub type BedrockPingResult<T> = Result<T, BedrockPingError>;
127
128/// Random number that must be in ping packets.
129/// https://wiki.vg/Raknet_Protocol#Data_types
130const MAGIC: u128 = 0x00ffff00fefefefefdfdfdfd12345678;
131
132struct PingRequestFrame {
133    time: i64,
134    magic: u128,
135    guid: i64,
136}
137
138impl PingRequestFrame {
139    const PACKET_ID: u8 = 0x01;
140    pub fn to_vec(&self) -> Vec<u8> {
141        let mut buf = Vec::with_capacity(1028);
142        buf.put_u8(Self::PACKET_ID);
143        buf.put_i64(self.time);
144        buf.put_u128(self.magic);
145        buf.put_i64(self.guid);
146        buf
147    }
148}
149
150struct PingResponseFrame {
151    time: i64,
152    /// "Server ID string" on wiki.vg
153    motd: String,
154}
155
156impl PingResponseFrame {
157    const SIZE: usize = 8 + 8 + 16 + 2;
158    const PACKET_ID: u8 = 0x1c;
159    pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
160        if bytes.len() < Self::SIZE {
161            return None;
162        }
163        let mut cursor = Cursor::new(bytes);
164
165        let packet_id = cursor.get_u8();
166        if packet_id != Self::PACKET_ID {
167            return None;
168        }
169
170        let time = cursor.get_i64();
171        let _guid = cursor.get_i64();
172        let magic = cursor.get_u128();
173
174        if magic != MAGIC {
175            return None;
176        }
177
178        let motd_len = cursor.get_u16();
179        let mut motd_bytes = vec![0u8; motd_len as usize];
180        cursor.read_exact(&mut motd_bytes).ok()?;
181        let motd = String::from_utf8(motd_bytes).ok()?;
182
183        Some(PingResponseFrame { time, motd })
184    }
185}
186
187/// Ping a bedrock server and return the info and latency. Timeout is `retry_timeout * retries`.
188pub async fn ping(
189    address: (String, u16),
190    retry_timeout: Duration,
191    retries: u64,
192) -> BedrockPingResult<(BedrockServerInfo, Duration)> {
193    let resolved = lookup_host(address.clone())
194        .await?
195        .next()
196        .context(DNSLookupFailedSnafu { address: address.0 })?;
197    trace!("host resolved to {resolved}");
198
199    let socket = UdpSocket::bind("0.0.0.0:0")
200        .await
201        .context(ConnectFailedSnafu)?;
202    socket.connect(resolved).await.context(ConnectFailedSnafu)?;
203    trace!("opened udp socket");
204
205    let mut response = None;
206    for retry in 0..retries {
207        debug!("pinging raknet server, attempt {}", retry + 1);
208        tokio::select! {
209            biased;
210            _ = tokio::time::sleep(retry_timeout) => continue,
211            res = attempt_ping(&socket) => response = res,
212        }
213        if response.is_some() {
214            break;
215        }
216    }
217    let (response, latency) = response.context(NoResponseSnafu)?;
218
219    trace!("ping finished");
220
221    Ok((response.motd.parse()?, latency))
222}
223
224/// See: https://wiki.vg/Raknet_Protocol#Unconnected_Ping
225async fn attempt_ping(socket: &UdpSocket) -> Option<(PingResponseFrame, Duration)> {
226    let outgoing_packet = PingRequestFrame {
227        time: Utc::now().timestamp_millis(),
228        magic: MAGIC,
229        guid: rand::random(),
230    };
231    socket.send(&outgoing_packet.to_vec()).await.ok()?;
232    let mut buffer = Vec::with_capacity(1024);
233    socket.recv_buf(&mut buffer).await.ok()?;
234    let incoming_packet = PingResponseFrame::from_bytes(&buffer)?;
235    let latency_millis = Utc::now().timestamp_millis() - incoming_packet.time;
236    let latency = Duration::from_millis(latency_millis as u64);
237
238    Some((incoming_packet, latency))
239}
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244
245    #[tokio::test]
246    async fn cubecraft() {
247        ping(
248            ("play.cubecraft.net".to_owned(), 19132),
249            Duration::from_secs(2),
250            3,
251        )
252        .await
253        .unwrap();
254    }
255
256    #[tokio::test]
257    async fn the_hive() {
258        ping(
259            ("geo.hivebedrock.network".to_owned(), 19132),
260            Duration::from_secs(2),
261            3,
262        )
263        .await
264        .unwrap();
265    }
266
267    #[tokio::test]
268    #[should_panic]
269    async fn invalid_address() {
270        ping(("example.com".to_owned(), 19132), Duration::from_secs(2), 3)
271            .await
272            .unwrap();
273    }
274}