-
Notifications
You must be signed in to change notification settings - Fork 6
Add motd docs #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Shadowsniper784
wants to merge
6
commits into
bedrock-crustaceans:main
Choose a base branch
from
Shadowsniper784:Add-MOTD-Docs
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 4 commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
cbc804a
Create MOTD.md
Shadowsniper784 f3cbedf
Fix formatting in MOTD
Shadowsniper784 89667b2
Fix missing OfflineMessageId
Shadowsniper784 893576f
Redo, add code examples
Shadowsniper784 650ded1
Fix the intro of MOTD.md
Shadowsniper784 18b46c5
Fixed some issues
Shadowsniper784 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,362 @@ | ||
# MOTD Protocol Documentation | ||
|
||
## Overview | ||
|
||
The server operates by listening on port `19132` for incoming RakNet unconnected pings. Upon receiving a valid ping, the server responds with an unconnected pong that contains the MOTD and other info. This interaction ensures that clients receive essential details about the server before establishing a connection. | ||
Shadowsniper784 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
## Packet Structure | ||
|
||
### Unconnected Ping | ||
|
||
When a client wants to display the server it sends an unconnected ping | ||
|
||
- **Packet ID (1 byte):** `0x01` | ||
theaddonn marked this conversation as resolved.
Show resolved
Hide resolved
|
||
- **Timestamp (8 bytes, Big-Endian):** The time since start of the client. Just setting it to current unix timestamp works fine | ||
- **Magic Offline Message ID (16 bytes):** | ||
``` | ||
[ | ||
0x00, 0xFF, 0xFF, 0x00, | ||
0xFE, 0xFE, 0xFE, 0xFE, | ||
0xFD, 0xFD, 0xFD, 0xFD, | ||
0x12, 0x34, 0x56, 0x78 | ||
] | ||
``` | ||
- **GUID (8 bytes, Big-Endian):** Unique identifier for the client. | ||
|
||
#### Validation Criteria | ||
|
||
1. **Packet Size:** | ||
The total size must be exactly 33 bytes. | ||
2. **Packet ID:** | ||
Must strictly be `0x01`. | ||
3. **Timestamp:** | ||
It is just a timestamp of uptime, but we can check it is not infront of unix timestamp | ||
4. **Magic Offline Message ID:** | ||
Must match the predefined 16-byte sequence. | ||
5. **GUID:** | ||
It doesn't really need to be checked, but it is there | ||
|
||
### Unconnected Pong | ||
|
||
In response to a valid unconnected ping, the server dispatches an unconnected pong packet containing the MOTD and additional server details. | ||
|
||
- **Packet ID (1 byte):** `0x1C` (Denotes Unconnected Pong) | ||
- **Timestamp (8 bytes, Big-Endian):** The time since start of the server. Just setting it to current unix timestamp works fine | ||
- **Server GUID (8 bytes, Big-Endian):** Unique server identifier. | ||
- **Magic Offline Message ID (16 bytes):** Identical to the one used in the ping. | ||
- **MOTD Length (2 bytes, Big-Endian):** Specifies the length of the MOTD string. | ||
- **MOTD (Variable Length):** The actual Message of the Day in byte format. | ||
|
||
## Message of the Day (MOTD) Generation | ||
|
||
The MOTD is what shows up in the server list, it is what people see before joining the server. | ||
|
||
### MOTD Format | ||
|
||
The MOTD follows a semicolon-separated (`;`) format comprising various fields, semi colons can be escaped with a backslash (`\`) | ||
``` | ||
<Edition>;<MOTD>;<Protocol Version>;<Game Version>;<Player Count>;<Max Player Count>;<Server GUID>;<World Name>;<Game Mode>;<Nintendo Limited>;<IpV4Port>;<IpV6Port>;<Extra Data>; | ||
``` | ||
|
||
#### Field Breakdown & Data Types | ||
|
||
- **Edition:** string | ||
Specifies the game edition (e.g., `MCPE` for Minecraft Pocket Edition), seems to be hardcoded to `MCPE` | ||
Shadowsniper784 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
- **MOTD:** string | ||
The actual message that is displayed, it can have Minecraft colors. | ||
- **Protocol Version:** uint16 | ||
Numeric identifier indicating the game protocol version (e.g., `748`). | ||
- **Game Version:** string | ||
Denotes the server's game version (e.g., `1.20.40`, or on CubeCraft: `1`). | ||
Shadowsniper784 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
- **players online:** uint32 | ||
Current number of active players (e.g., `10`). | ||
- **players max:** uint32 | ||
Maximum number of players allowed on the server (e.g., `100`). | ||
- **server id:** uint64 | ||
Unique identifier for the server (the server GUID but as a number). | ||
- **level name:** string | ||
Name of the game world (e.g., `world`, some docs call this submotd). | ||
- **gamemode:** string | ||
Current game mode (e.g., `Survival`, `Creative`, `Adventure`). | ||
- **NintendoLimited:** boolean | ||
Returns the status of Nintendo's limitation to join (0 or 1). | ||
- **port v4:** uint16 | ||
The port of the server for IPv4, useful for servers that have both IPv4 and IPv6. | ||
- **port v6:** uint16 | ||
The port of the server for IPv6, useful for servers that have both IPv4 and IPv6. | ||
- **Extra Data:** | ||
Ends with `0` to signify the conclusion of the MOTD string. | ||
|
||
## Code Examples | ||
|
||
::: code-group | ||
Shadowsniper784 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
```rust [src/main.rs] | ||
use std::io; | ||
use std::net::UdpSocket; | ||
use std::thread; | ||
use std::time::{SystemTime, UNIX_EPOCH}; | ||
|
||
const MAGIC_OFFLINE_MESSAGE_ID: [u8; 16] = [ | ||
0x00, 0xFF, 0xFF, 0x00, | ||
0xFE, 0xFE, 0xFE, 0xFE, | ||
0xFD, 0xFD, 0xFD, 0xFD, | ||
0x12, 0x34, 0x56, 0x78, | ||
]; | ||
|
||
fn gen_motd(server_guid: u64, listen_port: u16) -> String { | ||
let edition = "MCPE"; | ||
let motd = "Welcome to my Minecraft server!"; | ||
let protocol_version = "748"; | ||
let version = "1.20.40"; | ||
let player_count = "10"; | ||
let max_player_count = "100"; | ||
let world_name = "world"; | ||
let gamemode = "Survival"; | ||
let nintendo_limited = "1"; | ||
let port = listen_port.to_string(); | ||
let server_guid = server_guid.to_string(); | ||
|
||
return format!( | ||
"{};{};{};{};{};{};{};{};{};{};{};{};0;", | ||
edition, | ||
motd, | ||
protocol_version, | ||
version, | ||
player_count, | ||
max_player_count, | ||
server_guid, | ||
world_name, | ||
gamemode, | ||
nintendo_limited, | ||
port, | ||
port | ||
); | ||
} | ||
|
||
fn check_valid_ping(buf: &[u8], size: usize) -> bool { | ||
// Check length of buffer. If we just do .len() it will be 1024 not 33, so we have to pass the size | ||
if size != 33 { | ||
return false; | ||
} | ||
|
||
if buf[0] != 0x01 { | ||
return false; | ||
} | ||
|
||
let time: [u8; 8] = buf[1..9].try_into().unwrap(); | ||
let current_time = SystemTime::now() | ||
.duration_since(UNIX_EPOCH) | ||
.expect("Time went backwards") | ||
.as_secs(); | ||
let time = u64::from_be_bytes(time); | ||
if time > current_time { | ||
return false; | ||
} | ||
|
||
let magic_offline_message_id: [u8; 16] = buf[9..25].try_into().unwrap(); | ||
if magic_offline_message_id != MAGIC_OFFLINE_MESSAGE_ID { | ||
return false; | ||
} | ||
|
||
let guid: [u8; 8] = buf[25..33].try_into().unwrap(); | ||
let guid = u64::from_be_bytes(guid); | ||
|
||
println!("Received packet with guid: {}, time: {}", guid, time); | ||
|
||
return true; | ||
} | ||
|
||
fn write_pong(server_guid: u64, motd: String) -> Vec<u8> { | ||
let current_time = SystemTime::now() | ||
.duration_since(UNIX_EPOCH) | ||
.expect("Time went backwards") | ||
.as_secs(); | ||
|
||
let mut response = Vec::new(); | ||
response.push(0x1C); // Packet ID for Unconnected Pong | ||
response.extend(¤t_time.to_be_bytes()); | ||
response.extend(&server_guid.to_be_bytes()); | ||
response.extend(&MAGIC_OFFLINE_MESSAGE_ID); | ||
|
||
let motd_bytes = motd.as_bytes(); | ||
let motd_length = motd_bytes.len() as u16; | ||
response.extend(&motd_length.to_be_bytes()); | ||
response.extend(motd_bytes); | ||
|
||
return response; | ||
} | ||
|
||
fn listen(listen_port: u16) { | ||
let socket: UdpSocket = match UdpSocket::bind(format!("127.0.0.1:{}", listen_port)) { | ||
Ok(s) => s, | ||
Err(e) => { | ||
eprintln!("Failed to bind to port 19132: {}", e); | ||
return; | ||
} | ||
}; | ||
socket | ||
.set_nonblocking(true) | ||
.expect("Cannot set non-blocking"); | ||
|
||
let server_guid: u64 = 1234567890123456789; // Replace with your server's GUID | ||
|
||
let mut buf = [0u8; 1024]; | ||
|
||
loop { | ||
match socket.recv_from(&mut buf) { | ||
Ok((size, src)) => { | ||
if !check_valid_ping(&buf, size) { | ||
println!("Received invalid ping"); | ||
continue; | ||
} | ||
|
||
// Unconnected ping received | ||
let motd = gen_motd(server_guid, listen_port); | ||
let response = write_pong(server_guid, motd); | ||
|
||
if let Err(e) = socket.send_to(&response, src) { | ||
eprintln!("Failed to send unconnected pong: {}", e); | ||
} else { | ||
println!( | ||
"Sent unconnected pong to {}, from {}", | ||
src, listen_port | ||
); | ||
} | ||
} | ||
Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => { | ||
// No data received, continue listening | ||
thread::sleep(std::time::Duration::from_millis(100)); | ||
} | ||
Err(e) => { | ||
eprintln!("Error receiving data: {}", e); | ||
} | ||
} | ||
} | ||
} | ||
|
||
fn main() { | ||
listen(19132); | ||
} | ||
``` | ||
|
||
```ts [src/main.ts BUN] | ||
let MAGIC_OFFLINE_MESSAGE_ID = new Buffer([0x00, 0xFF, 0xFF, 0x00, 0xFE, 0xFE, 0xFE, 0xFE, 0xFD, 0xFD, 0xFD, 0xFD, 0x12, 0x34, 0x56, 0x78]); | ||
|
||
function genMotd(serverGuid: number, listenPort: number) { | ||
let edition = "MCPE"; | ||
let motd = "Welcome to my Minecraft server!"; | ||
let protocolVersion = "748"; | ||
let version = "1.20.40"; | ||
let playerCount = "10"; | ||
let maxPlayerCount = "100"; | ||
let worldName = "world"; | ||
let gamemode = "Survival"; | ||
let nintendoLimited = "1"; | ||
let port = listenPort.toString(); | ||
let guid = serverGuid.toString(); | ||
|
||
let order = [ | ||
edition, | ||
motd, | ||
protocolVersion, | ||
version, | ||
playerCount, | ||
maxPlayerCount, | ||
guid, | ||
worldName, | ||
gamemode, | ||
nintendoLimited, | ||
port, | ||
port, | ||
"0;", | ||
]; | ||
|
||
return order.join(";"); | ||
} | ||
|
||
function checkValidPing(buf: Buffer, size: number) { | ||
if (size !== 33) { | ||
return false; | ||
} | ||
|
||
if (buf[0] !== 0x01) { | ||
return false; | ||
} | ||
|
||
let time = buf.subarray(1, 9); | ||
time = time.readBigUInt64BE(); | ||
|
||
|
||
let magicOfflineMessageId = buf.subarray(9, 25); | ||
if (!magicOfflineMessageId.equals(MAGIC_OFFLINE_MESSAGE_ID)) { | ||
return false; | ||
} | ||
|
||
let guid = buf.subarray(25, 33); | ||
guid = guid.readBigUInt64BE(); | ||
|
||
|
||
return true; | ||
} | ||
|
||
function writePong(serverGuid: number, motd: string) { | ||
const currentTime = BigInt(Math.floor(Date.now() / 1000)); // Current time in seconds | ||
|
||
const response = Buffer.alloc(1 + 8 + 8 + MAGIC_OFFLINE_MESSAGE_ID.length + 2 + motd.length); | ||
|
||
let offset = 0; | ||
response.writeUInt8(0x1C, offset); // Packet ID for Unconnected Pong | ||
offset += 1; | ||
|
||
// Write current time | ||
response.writeBigUInt64BE(currentTime, offset); | ||
offset += 8; | ||
|
||
// Write server GUID | ||
response.writeBigUInt64BE(BigInt(serverGuid), offset); | ||
offset += 8; | ||
|
||
// Write MAGIC_OFFLINE_MESSAGE_ID | ||
MAGIC_OFFLINE_MESSAGE_ID.copy(response, offset); | ||
offset += MAGIC_OFFLINE_MESSAGE_ID.length; | ||
|
||
// Write MOTD length and bytes | ||
response.writeUInt16BE(motd.length, offset); | ||
offset += 2; | ||
|
||
response.write(motd, offset); | ||
|
||
return response; | ||
} | ||
|
||
function listen(listenPort: number) { | ||
let socket = Bun.udpSocket({ | ||
port: listenPort, | ||
socket: { | ||
data: (socket, buf, port, addr) => { | ||
let serverGuid = 1234567890123456789; | ||
|
||
if (!checkValidPing(buf, buf.length)) { | ||
console.log("Received invalid ping"); | ||
return; | ||
} | ||
|
||
let motd = genMotd(serverGuid, listenPort); | ||
let response = writePong(serverGuid, motd); | ||
|
||
socket.send(response, port, addr); | ||
}, | ||
}, | ||
}); | ||
} | ||
|
||
listen(19132); | ||
``` | ||
## Sources | ||
- [Go Raknet](https://github.com/Sandertv/go-raknet/blob/master/internal/message/unconnected_ping.go#L8)<br> | ||
- [PHP MC Query](https://github.com/xPaw/PHP-Minecraft-Query/blob/master/src/MinecraftQuery.php#L212)<br> | ||
- [Raknet](https://github.com/facebookarchive/RakNet/blob/1a169895a900c9fc4841c556e16514182b75faf8/Source/RakPeer.cpp#L135)<br> | ||
- [Phantom](https://github.com/jhead/phantom/blob/44056d83afbfe60b253faff01308d171ac21e8d6/internal/proto/proto.go#L23)<br> | ||
- [Geyser](https://github.com/GeyserMC/Protocol/blob/46b4ad37b159b1fc59e45871f7572101f5ed43ab/bedrock-connection/src/main/java/org/cloudburstmc/protocol/bedrock/BedrockPong.java#L23)<br> | ||
- Wireshark |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.