diff --git a/Cargo.lock b/Cargo.lock index 0ce851c..d1ef4d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -142,6 +142,17 @@ dependencies = [ "pin-utils", ] +[[package]] +name = "getrandom" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "hermit-abi" version = "0.1.19" @@ -164,6 +175,7 @@ dependencies = [ "bytes", "futures", "minecraft-protocol", + "rand 0.8.4", "tokio", ] @@ -291,6 +303,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "ppv-lite86" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba" + [[package]] name = "proc-macro2" version = "1.0.32" @@ -317,9 +335,9 @@ checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" dependencies = [ "autocfg 0.1.7", "libc", - "rand_chacha", + "rand_chacha 0.1.1", "rand_core 0.4.2", - "rand_hc", + "rand_hc 0.1.0", "rand_isaac", "rand_jitter", "rand_os", @@ -328,6 +346,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "rand" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.3", + "rand_hc 0.3.1", +] + [[package]] name = "rand_chacha" version = "0.1.1" @@ -338,6 +368,16 @@ dependencies = [ "rand_core 0.3.1", ] +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.3", +] + [[package]] name = "rand_core" version = "0.3.1" @@ -353,6 +393,15 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom", +] + [[package]] name = "rand_hc" version = "0.1.0" @@ -362,6 +411,15 @@ dependencies = [ "rand_core 0.3.1", ] +[[package]] +name = "rand_hc" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" +dependencies = [ + "rand_core 0.6.3", +] + [[package]] name = "rand_isaac" version = "0.1.1" @@ -512,10 +570,16 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90dbc611eb48397705a6b0f6e917da23ae517e4d127123d2cf7674206627d32a" dependencies = [ - "rand", + "rand 0.6.5", "serde", ] +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index d17e7b7..2ffac5d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,4 +22,5 @@ edition = "2021" bytes = "1.1" futures = { version = "0.3", default-features = false } minecraft-protocol = { git = "https://github.com/timvisee/minecraft-protocol", rev = "c578492" } -tokio = { version = "1", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "macros", "sync"] } +rand = "0.8" +tokio = { version = "1", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "macros", "sync", "time"] } diff --git a/README.md b/README.md index 66e0f82..e8491ba 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # lazymc -`lazymc` puts your Minecraft server at rest when idle, and wakes it up when +`lazymc` puts your Minecraft server to rest when idle, and wakes it up when players connect. ## License diff --git a/src/main.rs b/src/main.rs index 4e05fab..935541f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ pub mod config; +pub mod monitor; pub mod protocol; pub mod types; @@ -25,13 +26,19 @@ use protocol::{Client, ClientState, RawPacket}; async fn main() -> Result<(), ()> { println!( "Proxying public {} to internal {}", - ADDRESS_PUBLIC, ADDRESS_PROXY + ADDRESS_PUBLIC, ADDRESS_PROXY, ); + let server_state = monitor::ServerState::default().shared(); + // Listen for new connections // TODO: do not drop error here let listener = TcpListener::bind(ADDRESS_PUBLIC).await.map_err(|_| ())?; + // Spawn server monitor + let addr = ADDRESS_PROXY.parse().expect("invalid server IP"); + tokio::spawn(monitor::monitor_server(addr, server_state)); + // Proxy all incomming connections while let Ok((inbound, _)) = listener.accept().await { let client = Client::default(); @@ -53,7 +60,7 @@ async fn main() -> Result<(), ()> { } /// Read raw packet from stream. -async fn read_packet<'a>( +pub async fn read_packet<'a>( buf: &mut BytesMut, stream: &mut ReadHalf<'a>, ) -> Result)>, ()> { @@ -113,7 +120,7 @@ async fn proxy(client: Client, mut inbound: TcpStream, addr_target: String) -> R let (client_send_queue, mut client_to_send) = unbounded_channel::>(); - let server_available = false; + let server_available = true; let client_to_server = async { // Incoming buffer diff --git a/src/monitor.rs b/src/monitor.rs new file mode 100644 index 0000000..f21508f --- /dev/null +++ b/src/monitor.rs @@ -0,0 +1,179 @@ +// TODO: remove all unwraps/expects here! + +use std::net::SocketAddr; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::time::Duration; + +use bytes::BytesMut; +use minecraft_protocol::decoder::Decoder; +use minecraft_protocol::encoder::Encoder; +use minecraft_protocol::version::v1_14_4::handshake::Handshake; +use minecraft_protocol::version::v1_14_4::status::{PingRequest, PingResponse}; +use rand::Rng; +use tokio::io::AsyncWriteExt; +use tokio::net::TcpStream; + +use crate::protocol::{self, ClientState, RawPacket}; + +/// Minecraft protocol version used when polling server status. +const PROTOCOL_VERSION: i32 = 754; + +/// Monitor ping inverval in seconds. +const MONITOR_PING_INTERVAL: u64 = 2; + +/// Ping timeout in seconds. +const PING_TIMEOUT: u64 = 8; + +/// Shared server state. +#[derive(Default, Debug)] +pub struct ServerState { + /// Whether the server is online. + online: AtomicBool, + + /// Whether the server is starting. + starting: AtomicBool, +} + +impl ServerState { + /// Transform into shared instance. + pub fn shared(self) -> Arc { + Arc::new(self) + } + + /// Whether the server is online. + pub fn online(&self) -> bool { + self.online.load(Ordering::Relaxed) + } + + /// Set whether the server is online. + pub fn set_online(&self, online: bool) { + self.online.store(online, Ordering::Relaxed) + } + + /// Whether the server is starting. + pub fn starting(&self) -> bool { + self.starting.load(Ordering::Relaxed) + } + + /// Set whether the server is starting. + pub fn set_starting(&self, starting: bool) { + self.starting.store(starting, Ordering::Relaxed) + } +} + +/// Poll server state. +/// +/// Returns `true` if a ping succeeded. +pub async fn poll_server(addr: SocketAddr) -> bool { + attempt_connect(addr).await.is_ok() +} + +/// Monitor server. +pub async fn monitor_server(addr: SocketAddr, state: Arc) { + loop { + eprint!("Polling {}: ", addr); + let online = poll_server(addr).await; + + state.set_online(online); + + tokio::time::sleep(Duration::from_secs(MONITOR_PING_INTERVAL)).await; + } +} + +/// Attemp to connect to the given server. +async fn attempt_connect(addr: SocketAddr) -> Result<(), ()> { + let mut stream = TcpStream::connect(addr).await.map_err(|_| ())?; + + // Send handshake + send_handshake(&mut stream, addr).await?; + + // Send ping request + let token = send_ping(&mut stream).await?; + + // Wait for ping with timeout + wait_for_ping_timeout(&mut stream, token).await?; + + Ok(()) +} + +/// Send handshake. +async fn send_handshake(stream: &mut TcpStream, addr: SocketAddr) -> Result<(), ()> { + let handshake = Handshake { + protocol_version: PROTOCOL_VERSION, + server_addr: addr.ip().to_string(), + server_port: addr.port(), + next_state: ClientState::Status.to_id(), + }; + + let mut packet = Vec::new(); + handshake.encode(&mut packet).map_err(|_| ())?; + + let raw = RawPacket::new(protocol::HANDSHAKE_PACKET_ID_HANDSHAKE, packet) + .encode() + .map_err(|_| ())?; + + stream.write_all(&raw).await.map_err(|_| ())?; + + Ok(()) +} + +/// Send ping requets. +/// +/// Returns sent ping time token on success. +async fn send_ping(stream: &mut TcpStream) -> Result { + // Generate a random ping token + let token = rand::thread_rng().gen(); + + let ping = PingRequest { time: token }; + + let mut packet = Vec::new(); + ping.encode(&mut packet).map_err(|_| ())?; + + let raw = RawPacket::new(protocol::STATUS_PACKET_ID_PING, packet) + .encode() + .map_err(|_| ())?; + + stream.write_all(&raw).await.map_err(|_| ())?; + + Ok(token) +} + +/// Wait for a ping response. +async fn wait_for_ping(stream: &mut TcpStream, token: u64) -> Result<(), ()> { + // Get stream reader, set up buffer + let (mut reader, mut _writer) = stream.split(); + let mut buf = BytesMut::new(); + + loop { + // Read packet from stream + let (packet, _raw) = match crate::read_packet(&mut buf, &mut reader).await { + Ok(Some(packet)) => packet, + Ok(None) => break, + Err(_) => continue, + }; + + // Catch ping response + if packet.id == protocol::STATUS_PACKET_ID_PING { + let ping = PingResponse::decode(&mut packet.data.as_slice()).map_err(|_| ())?; + + // Ensure ping token is correct + if ping.time != token { + break; + } + + return Ok(()); + } + } + + // Some error occurred + Err(()) +} + +/// Wait for a ping response with timeout. +async fn wait_for_ping_timeout(stream: &mut TcpStream, token: u64) -> Result<(), ()> { + let ping = wait_for_ping(stream, token); + tokio::time::timeout(Duration::from_secs(PING_TIMEOUT), ping) + .await + .map_err(|_| ())? +} diff --git a/src/protocol.rs b/src/protocol.rs index 35d1770..f97f347 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -2,6 +2,7 @@ use std::sync::Mutex; use crate::types; +pub const HANDSHAKE_PACKET_ID_HANDSHAKE: i32 = 0; pub const STATUS_PACKET_ID_STATUS: i32 = 0; pub const STATUS_PACKET_ID_PING: i32 = 1; pub const LOGIN_PACKET_ID_LOGIN_START: i32 = 0;