Skip to main content

netget/server/rcon/
wire.rs

1//! Source RCON framing (the protocol Minecraft's RCON also speaks): a little-endian i32
2//! size, then id, type, a NUL-terminated body and an empty NUL-terminated string.
3use anyhow::{bail, ensure, Context, Result};
4use std::time::Duration;
5use tokio::io::{AsyncRead, AsyncReadExt};
6
7/// Smallest legal size field: id, type and the two NULs.
8pub const MIN_SIZE: usize = 10;
9/// Largest packet a peer may send (size field), as Source servers enforce.
10pub const MAX_PACKET: usize = 4096;
11/// Body bytes per outgoing response packet.
12pub const MAX_BODY_OUT: usize = MAX_PACKET - MIN_SIZE;
13/// Largest command output the server sends, and the client accepts, in total.
14pub const MAX_RESPONSE_BYTES: usize = 1024 * 1024;
15/// Write and reply deadline.
16pub const IO_TIMEOUT: Duration = Duration::from_secs(30);
17
18pub const SERVERDATA_AUTH: i32 = 3;
19pub const SERVERDATA_AUTH_RESPONSE: i32 = 2;
20pub const SERVERDATA_EXECCOMMAND: i32 = 2;
21pub const SERVERDATA_RESPONSE_VALUE: i32 = 0;
22
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct Packet {
25    pub id: i32,
26    pub kind: i32,
27    pub body: Vec<u8>,
28}
29
30impl Packet {
31    pub fn new(id: i32, kind: i32, body: impl Into<Vec<u8>>) -> Self {
32        Self {
33            id,
34            kind,
35            body: body.into(),
36        }
37    }
38    pub fn encode(&self) -> Vec<u8> {
39        let size = (8 + self.body.len() + 2) as i32;
40        let mut out = Vec::with_capacity(size as usize + 4);
41        out.extend_from_slice(&size.to_le_bytes());
42        out.extend_from_slice(&self.id.to_le_bytes());
43        out.extend_from_slice(&self.kind.to_le_bytes());
44        out.extend_from_slice(&self.body);
45        out.extend_from_slice(&[0, 0]);
46        out
47    }
48    pub fn text(&self) -> String {
49        String::from_utf8_lossy(&self.body).into_owned()
50    }
51}
52
53/// Read one packet. `None` on a clean EOF before the size field; the size is checked against
54/// `max` before the packet is read.
55pub async fn read_packet<R: AsyncRead + Unpin>(
56    reader: &mut R,
57    deadline: Duration,
58    max: usize,
59) -> Result<Option<Packet>> {
60    tokio::time::timeout(deadline, async {
61        let mut size = [0u8; 4];
62        match reader.read_exact(&mut size).await {
63            Ok(_) => {}
64            Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(None),
65            Err(e) => return Err(e.into()),
66        }
67        let size = i32::from_le_bytes(size);
68        ensure!(
69            size >= MIN_SIZE as i32,
70            "RCON packet size {size} is below {MIN_SIZE}"
71        );
72        ensure!(
73            size as usize <= max,
74            "RCON packet size {size} exceeds {max}"
75        );
76        let mut rest = vec![0u8; size as usize];
77        reader.read_exact(&mut rest).await?;
78        let id = i32::from_le_bytes(rest[0..4].try_into()?);
79        let kind = i32::from_le_bytes(rest[4..8].try_into()?);
80        let mut body = rest[8..].to_vec();
81        // The body and the trailing empty string each end in NUL; be lenient about the second.
82        if body.last() != Some(&0) {
83            bail!("RCON packet body is not NUL-terminated");
84        }
85        while body.last() == Some(&0) {
86            body.pop();
87        }
88        Ok(Some(Packet { id, kind, body }))
89    })
90    .await
91    .context("RCON read deadline")?
92}
93
94/// Split command output into response bodies of at most `MAX_BODY_OUT` bytes, at least one.
95pub fn chunks(output: &[u8]) -> Vec<Vec<u8>> {
96    if output.is_empty() {
97        return vec![Vec::new()];
98    }
99    output.chunks(MAX_BODY_OUT).map(<[u8]>::to_vec).collect()
100}
101
102/// RCON bodies are text; strip NUL, which would end a body early on the wire.
103pub fn body_from_text(text: &str) -> Vec<u8> {
104    text.bytes().filter(|b| *b != 0).collect()
105}
106
107/// Constant-time comparison for the configured password.
108pub fn same_secret(a: &[u8], b: &[u8]) -> bool {
109    if a.len() != b.len() {
110        return false;
111    }
112    a.iter().zip(b).fold(0u8, |acc, (x, y)| acc | (x ^ y)) == 0
113}