89 Commits

Author SHA1 Message Date
timvisee
1d92964802 Bump version to 0.2.3 2021-11-22 18:55:50 +01:00
timvisee
9b8d569628 Update dependencies 2021-11-22 18:53:52 +01:00
timvisee
94f2fa01e2 Update TODO 2021-11-22 18:53:23 +01:00
timvisee
9b71052b61 Add extras document with recommendations and tips after installing 2021-11-22 18:51:06 +01:00
timvisee
0049ad456c Bump rcon to 0.5.2, which now includes changes from our fork 2021-11-22 18:36:12 +01:00
timvisee
0f2d7720af Add documentation for proper IP proxying with PROXY header 2021-11-22 18:35:40 +01:00
timvisee
723ebabcfb Minor monitoring tweaks 2021-11-22 17:57:44 +01:00
timvisee
f95682fcd5 Only enable RCON by default on Windows 2021-11-22 17:57:28 +01:00
timvisee
d5c854d16f Add option to send HAProxy header with RCON connections 2021-11-22 17:55:51 +01:00
timvisee
493e24ff4d Send proxy header with monitor requests 2021-11-22 16:53:35 +01:00
timvisee
6916800aeb Add send_proxy_v2 option to send HAProxy header to server 2021-11-22 14:37:07 +01:00
timvisee
e7c31f2619 Add protocol version documentation 2021-11-22 13:37:21 +01:00
timvisee
7da467ff8c Bump version to 0.2.2 2021-11-18 11:37:02 +01:00
timvisee
c9d7af0e3c Add server favicon to status responses 2021-11-18 11:35:49 +01:00
timvisee
0715baed8c Bump version to 0.2.1 2021-11-17 20:31:22 +01:00
timvisee
1f4ec11ad1 Resolve clippy warnings 2021-11-17 20:26:49 +01:00
timvisee
acf6768b49 Show warning if 127.0.0.1 is IP banned 2021-11-17 20:25:49 +01:00
timvisee
75f7b62b16 Kick user with proper ban message, tweak IP file change debounce time 2021-11-17 20:21:22 +01:00
timvisee
9cc1958bbd Automatically reload banned IPs when file changes 2021-11-17 20:15:30 +01:00
timvisee
785bd2f33e Make banned IP format less strict to support possible future changes 2021-11-17 18:46:45 +01:00
timvisee
b168dcefde Respect IP ban expiry times 2021-11-17 18:42:26 +01:00
timvisee
74d772ab42 Respond with real server MOTD if it is currently started 2021-11-17 18:28:55 +01:00
timvisee
a71b3cb24f Add option to drop all connections from banned IPs
This instantly disconnects clients from banned IPs. Clients won't be
able to request or ping the server status. Clients won't get a kick
message with their ban reason either. Clients simply get a
'Disconnected' message on login.
2021-11-17 18:23:17 +01:00
timvisee
28dbcdbfd6 Show MOTD for banned players, kick with reason on login 2021-11-17 18:14:02 +01:00
timvisee
e816d4ff6c Use more efficient structure to manage banned IPs 2021-11-17 17:46:51 +01:00
timvisee
168cbceb4c Disconnect banned IPs based on server banned-ips.json file 2021-11-17 17:36:28 +01:00
timvisee
b1bd9e1837 Add support for host names in config address fields, resolve them to IP 2021-11-17 16:14:05 +01:00
timvisee
ec24f088b2 Fix lobby error due to invalid packet IDs 2021-11-16 18:09:45 +01:00
timvisee
6321999489 Update dependencies 2021-11-16 17:58:09 +01:00
timvisee
47fe7d0387 Extract all packet writing logic to single function 2021-11-16 17:57:34 +01:00
timvisee
7df3829e00 Use u8 for packet IDs 2021-11-16 17:13:30 +01:00
timvisee
b06f26b3e8 Refactor, cleanup status logic, extract join occupy logic into modules 2021-11-16 17:05:44 +01:00
timvisee
4510586169 Bump version to 0.2.0 2021-11-15 21:24:13 +01:00
timvisee
73d0c86780 Describe lobby feature in README, add documentation page with warning 2021-11-15 20:53:24 +01:00
timvisee
aca09dff7b Cleanup 2021-11-15 20:36:15 +01:00
timvisee
de516cf62c Resolve clippy warnings 2021-11-15 20:33:48 +01:00
timvisee
1da8c60323 Add lobby compiler feature flag 2021-11-15 20:30:22 +01:00
timvisee
d213612225 Remove lobby debug code 2021-11-15 20:22:34 +01:00
timvisee
ffc4f00430 Increase buffer size for reading Minecraft packets 2021-11-15 20:19:50 +01:00
timvisee
90e64297c0 Add support for packet compression 2021-11-15 20:18:52 +01:00
timvisee
ae6e877f17 Add lobby method to configuration 2021-11-15 18:17:02 +01:00
timvisee
4907780f7c Patch lobby logic for rebase on latest master 2021-11-15 18:04:34 +01:00
timvisee
40be4bae89 Reset player position when we play lobby ready sound effect 2021-11-15 18:04:34 +01:00
timvisee
d390f866cd Implement timeouts for various lobby client relaying stages 2021-11-15 18:04:34 +01:00
timvisee
374a9fab75 Improve waiting for server for lobby client
Broadcast to server state changes instead of polling the server state
constantly.
2021-11-15 18:04:33 +01:00
timvisee
f7d89a28aa Play sound effect in lobby when server is ready 2021-11-15 18:04:33 +01:00
timvisee
8b88cb16c5 Improve lobby handling, resolve various TODOs 2021-11-15 18:04:32 +01:00
timvisee
2cc64b29e0 Drop obsolete lobby packet, add lobby server warmup, some fixes 2021-11-15 18:04:32 +01:00
timvisee
802fd2990a Resolved all compiler warnings, clean-up proto, remove obsolete code 2021-11-15 18:04:32 +01:00
timvisee
3e7f5719cd Forward excess server packets in lobby to client before proxying 2021-11-15 18:04:31 +01:00
timvisee
32317a4c2f Remove obsolete file 2021-11-15 18:04:31 +01:00
timvisee
c9290827be Send client protocol & username to server from lobby, send server brand 2021-11-15 18:04:31 +01:00
timvisee
518fca90eb Improve lobby handling, implement teleport and proxy to real server 2021-11-15 18:04:30 +01:00
timvisee
e01fd212f7 Start experimenting with lobby, implement loading into lobby with text 2021-11-15 18:04:29 +01:00
timvisee
db99289ea7 Add join forward method to proxy to other address while server starts 2021-11-15 18:03:03 +01:00
timvisee
32cd9ffc73 Improve join occupy method descriptions in config 2021-11-15 17:27:38 +01:00
timvisee
d125140bee Update README for new join method feature 2021-11-15 16:54:45 +01:00
timvisee
e11eca1d5a Implement join method configuration and handling 2021-11-15 16:52:18 +01:00
timvisee
17ec663e15 Show warning if config version is outdated or invalid 2021-11-15 16:32:13 +01:00
timvisee
6b38dce5ab Restructure configuration file 2021-11-15 16:21:51 +01:00
timvisee
234a30aecb Add lockout mode, enable to prevent all players from connecting 2021-11-15 15:46:13 +01:00
timvisee
7a99781a05 Check and test no-rcon builds on GitLab CI 2021-11-15 15:25:54 +01:00
timvisee
5a5fa785b7 Bump version to 0.1.3 2021-11-15 15:18:53 +01:00
timvisee
78e9abec59 Fix binary release on GitLab CI 2021-11-15 15:18:19 +01:00
timvisee
dde9fdeab4 Bump version to 0.1.2 2021-11-15 14:53:05 +01:00
timvisee
901fb62f25 Use future tokio supported sync types throughout server handling logic 2021-11-15 14:41:01 +01:00
timvisee
dabeabeff4 Increase server monitoring timeouts to 20 seconds
Should improve polling reliability for overloaded servers.
2021-11-15 13:59:51 +01:00
timvisee
96d7fc9dec Grab exclusive lock for RCON invocations to server 2021-11-15 13:57:41 +01:00
timvisee
5ffc6ee911 Add RCON cooldown, do not require active PID to stop server
This hopefully improves server stopping reliability.
2021-11-15 13:42:58 +01:00
timvisee
b71d0d1013 Add delay between RCON commands, hopefully improve reliablity
The Minecraft RCON implementation is very broken/brittle. With this we
hope to improve reliablity.
2021-11-15 13:21:12 +01:00
Tim Visée
261acafab0 Merge branch 'ci-arm' into 'master'
Add ARMv7 and aarch64 builds to CI

See merge request timvisee/lazymc!2
2021-11-15 10:07:30 +00:00
timvisee
a04a5f93e9 Add ARMv7 and aarch64 builds to release job 2021-11-15 00:40:52 +01:00
timvisee
10c57f87ea Add GitLab CI builds for ARMv7 and aarch64 2021-11-15 00:20:23 +01:00
timvisee
88fc5892a3 Simplify waiting for server logic even further 2021-11-14 16:37:09 +01:00
timvisee
38d90681c7 Improve waiting for server when holding client
Instead of constantly polling the server state until it is ready, this
now subscribes to server state changes and uses a proper timeout.
2021-11-14 16:21:54 +01:00
timvisee
2af20945cc Bump version to 0.1.1 2021-11-14 12:54:34 +01:00
timvisee
ad638b5b3e Update dependencies 2021-11-14 12:54:09 +01:00
timvisee
69c2d580d5 Only disable prevent-proxy-connections if server is on non-loopback IP 2021-11-14 12:18:56 +01:00
timvisee
c6cd08c993 Add setup testing recommendation to README, note repository watch button 2021-11-14 12:14:10 +01:00
timvisee
69812f5b55 Make server sleeping errors a bit more descriptive 2021-11-14 12:09:26 +01:00
timvisee
f172587fd5 Add server quit cooldown period 2021-11-14 12:00:03 +01:00
timvisee
e6021502d9 Add TODO file, minor fixes 2021-11-12 13:12:17 +01:00
timvisee
157905f140 Update minecraft-protocol crate repository 2021-11-12 12:30:19 +01:00
timvisee
72d132ae8b Rewrite enable-status and prevent-proxy-connections in server.properties 2021-11-12 12:24:41 +01:00
timvisee
7a00c2df9e List crbanman/papermc-lazymc Docker image as 3rd-party implementation 2021-11-12 11:46:21 +01:00
Tim Visée
fe3bf63401 Update demo video in README 2021-11-11 23:34:54 +01:00
timvisee
e9e58a766b Improve Windows instructions 2021-11-11 15:15:47 +01:00
timvisee
f9be5c5a0f Update README, improve usage/compile instructions, add Windows guide 2021-11-11 15:05:05 +01:00
Tim Visée
7c7595dcd3 Update demo video in README 2021-11-11 14:38:03 +01:00
45 changed files with 6199 additions and 860 deletions

1
.gitignore vendored
View File

@@ -4,3 +4,4 @@
# Test server
/mcserver
/bettermc

View File

@@ -43,7 +43,9 @@ before_script:
<<: *rust-build-cache
script:
- cargo check --verbose
- cargo check --no-default-features --verbose
- cargo check --no-default-features --features rcon --verbose
- cargo check --no-default-features --features lobby --verbose
check:
<<: *check-base
check-macos:
@@ -67,6 +69,7 @@ check-windows:
script:
- cargo check --locked --verbose
- cargo check --locked --no-default-features --features rcon --verbose
- cargo check --locked --no-default-features --features lobby --verbose
# Build using Rust stable on Linux
build-x86_64-linux-gnu:
@@ -106,6 +109,76 @@ build-x86_64-linux-musl:
- lazymc-$TARGET
expire_in: 1 month
# Build using Rust stable on Linux for ARMv7
build-armv7-linux-gnu:
stage: build
image: ubuntu
needs: []
variables:
TARGET: armv7-unknown-linux-gnueabihf
cache:
<<: *rust-build-cache
before_script:
- apt-get update
- apt-get install -y --no-install-recommends build-essential
- |
apt-get install -y curl
curl https://sh.rustup.rs -sSf | sh -s -- -y
source $HOME/.cargo/env
- |
rustc --version
cargo --version
script:
- apt-get install -y gcc-arm-linux-gnueabihf
- rustup target add $TARGET
- mkdir -p ~/.cargo
- 'echo "[target.$TARGET]" >> ~/.cargo/config'
- 'echo "linker = \"arm-linux-gnueabihf-gcc\"" >> ~/.cargo/config'
- cargo build --target=$TARGET --release --locked --verbose
- mv target/$TARGET/release/lazymc ./lazymc-$TARGET
artifacts:
name: lazymc-armv7-linux-gnu
paths:
- lazymc-$TARGET
expire_in: 1 month
# Build using Rust stable on Linux for aarch64
build-aarch64-linux-gnu:
stage: build
image: ubuntu
needs: []
variables:
TARGET: aarch64-unknown-linux-gnu
cache:
<<: *rust-build-cache
before_script:
- apt-get update
- apt-get install -y --no-install-recommends build-essential
- |
apt-get install -y curl
curl https://sh.rustup.rs -sSf | sh -s -- -y
source $HOME/.cargo/env
- |
rustc --version
cargo --version
script:
- apt-get install -y gcc-aarch64-linux-gnu
- rustup target add $TARGET
- mkdir -p ~/.cargo
- 'echo "[target.$TARGET]" >> ~/.cargo/config'
- 'echo "linker = \"aarch64-linux-gnu-gcc\"" >> ~/.cargo/config'
- cargo build --target=$TARGET --release --locked --verbose
- mv target/$TARGET/release/lazymc ./lazymc-$TARGET
artifacts:
name: lazymc-aarch64-linux-gnu
paths:
- lazymc-$TARGET
expire_in: 1 month
# Build using Rust stable on macOS
build-macos:
stage: build
@@ -158,6 +231,9 @@ test-cargo-x86_64-linux-gnu:
<<: *rust-build-cache
script:
- cargo test --locked --verbose
- cargo test --locked --no-default-features --verbose
- cargo test --locked --no-default-features --features rcon --verbose
- cargo test --locked --no-default-features --features lobby --verbose
# Run the unit tests through Cargo on Windows
test-cargo-x86_64-windows:
@@ -170,6 +246,8 @@ test-cargo-x86_64-windows:
<<: *before_script-windows
script:
- cargo test --locked --verbose
- cargo test --locked --no-default-features --features rcon --verbose
- cargo test --locked --no-default-features --features lobby --verbose
# Release binaries on GitLab as generic package
release-gitlab-generic-package:
@@ -178,6 +256,8 @@ release-gitlab-generic-package:
dependencies:
- build-x86_64-linux-gnu
- build-x86_64-linux-musl
- build-armv7-linux-gnu
- build-aarch64-linux-gnu
- build-macos
- build-x86_64-windows
only:
@@ -185,6 +265,8 @@ release-gitlab-generic-package:
variables:
LINUX_GNU_BIN: "lazymc-x86_64-unknown-linux-gnu"
LINUX_MUSL_BIN: "lazymc-x86_64-unknown-linux-musl"
LINUX_ARMV7_GNU_BIN: "lazymc-armv7-unknown-linux-gnueabihf"
LINUX_AARCH64_GNU_BIN: "lazymc-aarch64-unknown-linux-gnu"
MACOS_BIN: "lazymc-x86_64-apple-darwin"
WINDOWS_BIN: "lazymc-x86_64-pc-windows-msvc.exe"
before_script: []
@@ -198,6 +280,10 @@ release-gitlab-generic-package:
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file ${LINUX_GNU_BIN} ${PACKAGE_REGISTRY_URL}/${LINUX_GNU_BIN}
- |
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file ${LINUX_MUSL_BIN} ${PACKAGE_REGISTRY_URL}/${LINUX_MUSL_BIN}
- |
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file ${LINUX_ARMV7_GNU_BIN} ${PACKAGE_REGISTRY_URL}/${LINUX_ARMV7_GNU_BIN}
- |
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file ${LINUX_AARCH64_GNU_BIN} ${PACKAGE_REGISTRY_URL}/${LINUX_AARCH64_GNU_BIN}
- |
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file ${MACOS_BIN} ${PACKAGE_REGISTRY_URL}/${MACOS_BIN}
- |
@@ -212,6 +298,8 @@ release-gitlab-release:
variables:
LINUX_GNU_BIN: "lazymc-x86_64-unknown-linux-gnu"
LINUX_MUSL_BIN: "lazymc-x86_64-unknown-linux-musl"
LINUX_ARMV7_GNU_BIN: "lazymc-armv7-unknown-linux-gnueabihf"
LINUX_AARCH64_GNU_BIN: "lazymc-aarch64-unknown-linux-gnu"
MACOS_BIN: "lazymc-x86_64-apple-darwin"
WINDOWS_BIN: "lazymc-x86_64-pc-windows-msvc.exe"
before_script: []
@@ -225,6 +313,8 @@ release-gitlab-release:
release-cli create --name "lazymc $CI_COMMIT_TAG" --tag-name $CI_COMMIT_TAG \
--assets-link "{\"name\":\"${LINUX_GNU_BIN}\",\"url\":\"${PACKAGE_REGISTRY_URL}/${LINUX_GNU_BIN}\"}" \
--assets-link "{\"name\":\"${LINUX_MUSL_BIN}\",\"url\":\"${PACKAGE_REGISTRY_URL}/${LINUX_MUSL_BIN}\"}" \
--assets-link "{\"name\":\"${LINUX_ARMV7_GNU_BIN}\",\"url\":\"${PACKAGE_REGISTRY_URL}/${LINUX_ARMV7_GNU_BIN}\"}" \
--assets-link "{\"name\":\"${LINUX_AARCH64_GNU_BIN}\",\"url\":\"${PACKAGE_REGISTRY_URL}/${LINUX_AARCH64_GNU_BIN}\"}" \
--assets-link "{\"name\":\"${MACOS_BIN}\",\"url\":\"${PACKAGE_REGISTRY_URL}/${MACOS_BIN}\"}" \
--assets-link "{\"name\":\"${WINDOWS_BIN}\",\"url\":\"${PACKAGE_REGISTRY_URL}/${WINDOWS_BIN}\"}"
@@ -236,6 +326,8 @@ release-github:
dependencies:
- build-x86_64-linux-gnu
- build-x86_64-linux-musl
- build-armv7-linux-gnu
- build-aarch64-linux-gnu
- build-macos
- build-x86_64-windows
before_script: []
@@ -253,5 +345,7 @@ release-github:
- ./github-release release --token "$GITHUB_TOKEN" --owner timvisee --repo lazymc --tag "$CI_COMMIT_REF_NAME" --title "lazymc $CI_COMMIT_REF_NAME"
- ./github-release upload --token "$GITHUB_TOKEN" --owner timvisee --repo lazymc --tag "$CI_COMMIT_REF_NAME" --file ./lazymc-x86_64-unknown-linux-gnu --name lazymc-$CI_COMMIT_REF_NAME-linux-x64
- ./github-release upload --token "$GITHUB_TOKEN" --owner timvisee --repo lazymc --tag "$CI_COMMIT_REF_NAME" --file ./lazymc-x86_64-unknown-linux-musl --name lazymc-$CI_COMMIT_REF_NAME-linux-x64-static
- ./github-release upload --token "$GITHUB_TOKEN" --owner timvisee --repo lazymc --tag "$CI_COMMIT_REF_NAME" --file ./lazymc-armv7-unknown-linux-gnueabihf --name lazymc-$CI_COMMIT_REF_NAME-linux-armv7
- ./github-release upload --token "$GITHUB_TOKEN" --owner timvisee --repo lazymc --tag "$CI_COMMIT_REF_NAME" --file ./lazymc-aarch64-unknown-linux-gnu --name lazymc-$CI_COMMIT_REF_NAME-linux-aarch64
- ./github-release upload --token "$GITHUB_TOKEN" --owner timvisee --repo lazymc --tag "$CI_COMMIT_REF_NAME" --file ./lazymc-x86_64-apple-darwin --name lazymc-$CI_COMMIT_REF_NAME-macos
- ./github-release upload --token "$GITHUB_TOKEN" --owner timvisee --repo lazymc --tag "$CI_COMMIT_REF_NAME" --file ./lazymc-x86_64-pc-windows-msvc.exe --name lazymc-$CI_COMMIT_REF_NAME-windows.exe

View File

@@ -1,5 +1,59 @@
# Changelog
## 0.2.3 (2021-11-22)
- Add support for `PROXY` header to notify Minecraft server of real client IP
- Only enable RCON by default on Windows
- Update dependencies
## 0.2.2 (2021-11-18)
- Add server favicon to status response
## 0.2.1 (2021-11-17)
- Add support for using host names in config address fields
- Handle banned players within `lazymc` based on server `banned-ips.json`
- Update dependencies
## 0.2.0 (2021-11-15)
- Add lockout feature, enable to kick all connecting clients with a message
- Add option to configure list of join methods to occupy client with while server is starting (kick, hold, forward, lobby)
- Add lobby join method, keeps client in lobby world on emulated server, teleports to real server when it is ready (highly experimental)
- Add forward join method to forward (proxy) client to other host while server is starting
- Restructure `lazymc.toml` configuration
- Increase packet reading buffer size to speed things up
- Add support for Minecraft packet compression
- Show warning if config version is outdated or invalid
- Various fixes and improvements
## 0.1.3 (2021-11-15)
- Fix binary release
## 0.1.2 (2021-11-15)
- Add Linux ARMv7 and aarch64 releases
- RCON now works if server is running while server command already quit
- Various RCON tweaks in an attempt to make it more robust and reliable (cooldown, exclusive lock, invocation spacing)
- Increase server monitoring timeout to 20 seconds
- Improve waiting for server logic when holding client
- Various fixes and improvements
## 0.1.1 (2021-11-14)
- Make server sleeping errors more descriptive
- Add server quit cooldown period, intended to prevent RCON errors due to RCON
server thread something quitting after main server
- Rewrite `enable-status = true` in `server.properties`
- Rewrite `prevent-proxy-connections = false` in `server.properties` if
Minecraft server has non-loopback address (other public IP)
- Add compile from source instructions to README
- Add Windows instructions to README
- Update dependencies
- Various fixes and improvements
## 0.1.0 (2021-11-11)
- Initial release

462
Cargo.lock generated
View File

@@ -19,9 +19,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.45"
version = "1.0.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee10e43ae4a853c0a3591d4e2ada1719e553be18199d9da9d4a83f5927c2f5c7"
checksum = "62e1f47f7dc0422027a4e370dd4548d4d66b26782e513e98dca1e689e058a80e"
[[package]]
name = "async-channel"
@@ -80,7 +80,7 @@ dependencies = [
"slab",
"socket2",
"waker-fn",
"winapi",
"winapi 0.3.9",
]
[[package]]
@@ -148,7 +148,7 @@ checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi",
"libc",
"winapi",
"winapi 0.3.9",
]
[[package]]
@@ -163,6 +163,12 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
[[package]]
name = "base64"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
[[package]]
name = "bitflags"
version = "1.3.2"
@@ -213,12 +219,37 @@ version = "1.0.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee"
[[package]]
name = "cesu8"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
[[package]]
name = "cfg-if"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
dependencies = [
"libc",
"num-integer",
"num-traits",
"time",
"winapi 0.3.9",
]
[[package]]
name = "clap"
version = "3.0.0-beta.5"
@@ -253,7 +284,7 @@ checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd"
dependencies = [
"atty",
"lazy_static",
"winapi",
"winapi 0.3.9",
]
[[package]]
@@ -271,7 +302,7 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
]
[[package]]
@@ -280,7 +311,7 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"lazy_static",
]
@@ -360,6 +391,12 @@ dependencies = [
"syn",
]
[[package]]
name = "doc-comment"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
[[package]]
name = "dotenv"
version = "0.15.0"
@@ -408,13 +445,25 @@ dependencies = [
"instant",
]
[[package]]
name = "filetime"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "975ccf83d8d9d0d84682850a38c8169027be83368805971cc4f238c2b245bc98"
dependencies = [
"cfg-if 1.0.0",
"libc",
"redox_syscall",
"winapi 0.3.9",
]
[[package]]
name = "flate2"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"crc32fast",
"libc",
"miniz_oxide",
@@ -426,12 +475,47 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "fsevent"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ab7d1bd1bd33cc98b0889831b72da23c0aa4df9cec7e0702f46ecea04b35db6"
dependencies = [
"bitflags",
"fsevent-sys",
]
[[package]]
name = "fsevent-sys"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f41b048a94555da0f42f1d632e2e19510084fb8e303b0daa2816e733fb3644a0"
dependencies = [
"libc",
]
[[package]]
name = "fuchsia-cprng"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
[[package]]
name = "fuchsia-zircon"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"
dependencies = [
"bitflags",
"fuchsia-zircon-sys",
]
[[package]]
name = "fuchsia-zircon-sys"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
[[package]]
name = "futures"
version = "0.3.17"
@@ -440,6 +524,7 @@ checksum = "a12aa0eb539080d55c3f2d45a67c3b58b6b0773c1a3ca2dfec66d58c97fd66ca"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
@@ -462,6 +547,17 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d"
[[package]]
name = "futures-executor"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45025be030969d763025784f7f355043dc6bc74093e4ecc5000ca4dc50d8745c"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.17"
@@ -502,11 +598,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481"
dependencies = [
"autocfg 1.0.1",
"futures-channel",
"futures-core",
"futures-io",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"slab",
]
[[package]]
@@ -515,7 +615,7 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"libc",
"wasi",
]
@@ -573,13 +673,42 @@ dependencies = [
"hashbrown",
]
[[package]]
name = "inotify"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4816c66d2c8ae673df83366c18341538f234a26d65a9ecea5c348b453ac1d02f"
dependencies = [
"bitflags",
"inotify-sys",
"libc",
]
[[package]]
name = "inotify-sys"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
dependencies = [
"libc",
]
[[package]]
name = "instant"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
]
[[package]]
name = "iovec"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e"
dependencies = [
"libc",
]
[[package]]
@@ -597,6 +726,16 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "kernel32-sys"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
dependencies = [
"winapi 0.2.8",
"winapi-build",
]
[[package]]
name = "kv-log-macro"
version = "1.0.7"
@@ -612,36 +751,53 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "lazycell"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]]
name = "lazymc"
version = "0.1.0"
version = "0.2.3"
dependencies = [
"anyhow",
"async-std",
"base64",
"bytes",
"chrono",
"clap",
"colored",
"derive_builder",
"dotenv",
"flate2",
"futures",
"libc",
"log",
"minecraft-protocol",
"named-binary-tag",
"notify",
"pretty_env_logger",
"proxy-protocol",
"quartz_nbt",
"rand 0.8.4",
"rcon",
"serde",
"serde_json",
"shlex",
"thiserror",
"tokio",
"toml",
"winapi",
"uuid",
"version-compare",
"winapi 0.3.9",
]
[[package]]
name = "libc"
version = "0.2.107"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbe5e23404da5b4f555ef85ebed98fb4083e55a00c317800bc2a50ede9f3d219"
checksum = "8521a1b57e76b1ec69af7599e75e38e7b7fad6610f037db8c79b127201b5d119"
[[package]]
name = "linked-hash-map"
@@ -655,10 +811,16 @@ version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"value-bag",
]
[[package]]
name = "md5"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e6bcd6433cff03a4bfc3d9834d504467db1f1cf6d0ea765d37d330249ed629d"
[[package]]
name = "memchr"
version = "2.4.1"
@@ -668,7 +830,7 @@ checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
[[package]]
name = "minecraft-protocol"
version = "0.1.0"
source = "git+https://github.com/timvisee/minecraft-protocol?rev=31041b8#31041b8fe2bc7e512d12476b958c1fe9e9077394"
source = "git+https://github.com/timvisee/rust-minecraft-protocol?rev=356ea54#356ea5424374c5a7249be2f0f13fd3e0e2db5b58"
dependencies = [
"byteorder",
"minecraft-protocol-derive",
@@ -681,7 +843,7 @@ dependencies = [
[[package]]
name = "minecraft-protocol-derive"
version = "0.0.0"
source = "git+https://github.com/timvisee/minecraft-protocol?rev=31041b8#31041b8fe2bc7e512d12476b958c1fe9e9077394"
source = "git+https://github.com/timvisee/rust-minecraft-protocol?rev=356ea54#356ea5424374c5a7249be2f0f13fd3e0e2db5b58"
dependencies = [
"proc-macro2",
"quote",
@@ -698,6 +860,25 @@ dependencies = [
"autocfg 1.0.1",
]
[[package]]
name = "mio"
version = "0.6.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4"
dependencies = [
"cfg-if 0.1.10",
"fuchsia-zircon",
"fuchsia-zircon-sys",
"iovec",
"kernel32-sys",
"libc",
"log",
"miow 0.2.2",
"net2",
"slab",
"winapi 0.2.8",
]
[[package]]
name = "mio"
version = "0.7.14"
@@ -706,9 +887,33 @@ checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc"
dependencies = [
"libc",
"log",
"miow",
"miow 0.3.7",
"ntapi",
"winapi",
"winapi 0.3.9",
]
[[package]]
name = "mio-extras"
version = "2.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19"
dependencies = [
"lazycell",
"log",
"mio 0.6.23",
"slab",
]
[[package]]
name = "miow"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d"
dependencies = [
"kernel32-sys",
"net2",
"winapi 0.2.8",
"ws2_32-sys",
]
[[package]]
@@ -717,27 +922,75 @@ version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21"
dependencies = [
"winapi",
"winapi 0.3.9",
]
[[package]]
name = "named-binary-tag"
version = "0.2.3"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d654702943d37d67f1491769ed46484c306f9b9d0d258348904bd63ffb101e8"
checksum = "523298fac63bd954f9a2e03b962b8a4a0e95110ad1b2fa3e0d7048660ffecec3"
dependencies = [
"byteorder",
"flate2",
"linked-hash-map",
]
[[package]]
name = "net2"
version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae"
dependencies = [
"cfg-if 0.1.10",
"libc",
"winapi 0.3.9",
]
[[package]]
name = "notify"
version = "4.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae03c8c853dba7bfd23e571ff0cff7bc9dceb40a4cd684cd1681824183f45257"
dependencies = [
"bitflags",
"filetime",
"fsevent",
"fsevent-sys",
"inotify",
"libc",
"mio 0.6.23",
"mio-extras",
"walkdir",
"winapi 0.3.9",
]
[[package]]
name = "ntapi"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44"
dependencies = [
"winapi",
"winapi 0.3.9",
]
[[package]]
name = "num-integer"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
dependencies = [
"autocfg 1.0.1",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
dependencies = [
"autocfg 1.0.1",
]
[[package]]
@@ -789,11 +1042,11 @@ version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "685404d509889fade3e86fe3a5803bca2ec09b0c0778d5ada6ec8bf7a8de5259"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"libc",
"log",
"wepoll-ffi",
"winapi",
"winapi 0.3.9",
]
[[package]]
@@ -845,6 +1098,40 @@ dependencies = [
"unicode-xid",
]
[[package]]
name = "proxy-protocol"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e50c72c21c738f5c5f350cc33640aee30bf7cd20f9d9da20ed41bce2671d532"
dependencies = [
"bytes",
"snafu",
]
[[package]]
name = "quartz_nbt"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24532990479062a9c515987986225879bd115ccb97672b1fb56d788a5adb7d39"
dependencies = [
"anyhow",
"byteorder",
"cesu8",
"flate2",
"quartz_nbt_macros",
]
[[package]]
name = "quartz_nbt_macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "289baa0c8a4d1f840d2de528a7f8c29e0e9af48b3018172b3edad4f716e8daed"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "quick-error"
version = "1.2.3"
@@ -876,7 +1163,7 @@ dependencies = [
"rand_os",
"rand_pcg",
"rand_xorshift",
"winapi",
"winapi 0.3.9",
]
[[package]]
@@ -970,7 +1257,7 @@ checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b"
dependencies = [
"libc",
"rand_core 0.4.2",
"winapi",
"winapi 0.3.9",
]
[[package]]
@@ -984,7 +1271,7 @@ dependencies = [
"libc",
"rand_core 0.4.2",
"rdrand",
"winapi",
"winapi 0.3.9",
]
[[package]]
@@ -1008,9 +1295,9 @@ dependencies = [
[[package]]
name = "rcon"
version = "0.5.1"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "465a6f903164a399084787547a026b83e7937bc576d8acdbd9e41ebf5de90a85"
checksum = "6b7fdd146f86bd90fa2d4cf83a28b45f058e90bcf11ed0cce134e757928771e6"
dependencies = [
"async-std",
"bytes",
@@ -1026,6 +1313,15 @@ dependencies = [
"rand_core 0.3.1",
]
[[package]]
name = "redox_syscall"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff"
dependencies = [
"bitflags",
]
[[package]]
name = "regex"
version = "1.5.4"
@@ -1055,6 +1351,15 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "serde"
version = "1.0.130"
@@ -1077,9 +1382,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.69"
version = "1.0.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e466864e431129c7e0d3476b92f20458e5879919a0596c6472738d9fa2d342f8"
checksum = "063bf466a64011ac24040a49009724ee60a57da1b437617ceb32e53ad61bfb19"
dependencies = [
"itoa",
"ryu",
@@ -1107,6 +1412,27 @@ version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5"
[[package]]
name = "snafu"
version = "0.6.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eab12d3c261b2308b0d80c26fffb58d17eba81a4be97890101f416b478c79ca7"
dependencies = [
"doc-comment",
"snafu-derive",
]
[[package]]
name = "snafu-derive"
version = "0.6.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1508efa03c362e23817f96cde18abed596a25219a8b2c66e8db33c03543d315b"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "socket2"
version = "0.4.2"
@@ -1114,7 +1440,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dc90fe6c7be1a323296982db1836d1ea9e47b6839496dde9a541bc496df3516"
dependencies = [
"libc",
"winapi",
"winapi 0.3.9",
]
[[package]]
@@ -1185,29 +1511,39 @@ dependencies = [
]
[[package]]
name = "tokio"
version = "1.13.0"
name = "time"
version = "0.1.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "588b2d10a336da58d877567cd8fb8a14b463e2104910f8132cd054b4b96e29ee"
checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438"
dependencies = [
"libc",
"winapi 0.3.9",
]
[[package]]
name = "tokio"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70e992e41e0d2fb9f755b37446f20900f64446ef54874f40a60c78f021ac6144"
dependencies = [
"autocfg 1.0.1",
"bytes",
"libc",
"memchr",
"mio",
"mio 0.7.14",
"num_cpus",
"once_cell",
"pin-project-lite",
"signal-hook-registry",
"tokio-macros",
"winapi",
"winapi 0.3.9",
]
[[package]]
name = "tokio-macros"
version = "1.5.1"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "114383b041aa6212c579467afa0075fbbdd0718de036100bc0ba7961d8cb9095"
checksum = "c9efc1aba077437943f7515666aa2b882dfabfbfdf89c819ea75a8d6e9eaba5e"
dependencies = [
"proc-macro2",
"quote",
@@ -1250,6 +1586,7 @@ version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90dbc611eb48397705a6b0f6e917da23ae517e4d127123d2cf7674206627d32a"
dependencies = [
"md5",
"rand 0.6.5",
"serde",
]
@@ -1264,6 +1601,12 @@ dependencies = [
"version_check",
]
[[package]]
name = "version-compare"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe88247b92c1df6b6de80ddc290f3976dbdf2f5f5d3fd049a9fb598c6dd5ca73"
[[package]]
name = "version_check"
version = "0.9.3"
@@ -1276,6 +1619,17 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca"
[[package]]
name = "walkdir"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56"
dependencies = [
"same-file",
"winapi 0.3.9",
"winapi-util",
]
[[package]]
name = "wasi"
version = "0.10.2+wasi-snapshot-preview1"
@@ -1288,7 +1642,7 @@ version = "0.2.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"wasm-bindgen-macro",
]
@@ -1313,7 +1667,7 @@ version = "0.4.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e8d7523cb1f2a4c96c1317ca690031b714a51cc14e05f712446691f413f5d39"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"js-sys",
"wasm-bindgen",
"web-sys",
@@ -1367,6 +1721,12 @@ dependencies = [
"cc",
]
[[package]]
name = "winapi"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
[[package]]
name = "winapi"
version = "0.3.9"
@@ -1377,6 +1737,12 @@ dependencies = [
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-build"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
@@ -1389,7 +1755,7 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
dependencies = [
"winapi",
"winapi 0.3.9",
]
[[package]]
@@ -1397,3 +1763,13 @@ name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "ws2_32-sys"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e"
dependencies = [
"winapi 0.2.8",
"winapi-build",
]

View File

@@ -1,6 +1,6 @@
[package]
name = "lazymc"
version = "0.1.0"
version = "0.2.3"
authors = ["Tim Visee <3a4fb3964f@sinenomine.email>"]
license = "GPL-3.0"
readme = "README.md"
@@ -19,29 +19,50 @@ exclude = [
edition = "2021"
[features]
default = ["rcon"]
rcon = ["rust_rcon"]
default = ["rcon", "lobby"]
# RCON support
# Allow use of RCON to manage (stop) server.
# Required on Windows.
rcon = ["rust_rcon", "async-std"]
# Lobby support
# Add lobby join method, keeps client in fake lobby world until server is ready.
lobby = ["named-binary-tag", "quartz_nbt", "uuid"]
[dependencies]
anyhow = "1.0"
base64 = "0.13"
bytes = "1.1"
chrono = "0.4"
clap = { version = "3.0.0-beta.5", default-features = false, features = [ "std", "cargo", "color", "env", "suggestions", "unicode" ]}
colored = "2.0"
derive_builder = "0.10"
dotenv = "0.15"
futures = { version = "0.3", default-features = false }
flate2 = { version = "1.0", default-features = false, features = ["default"] }
futures = { version = "0.3", default-features = false, features = ["executor"] }
log = "0.4"
minecraft-protocol = { git = "https://github.com/timvisee/minecraft-protocol", rev = "31041b8" }
minecraft-protocol = { git = "https://github.com/timvisee/rust-minecraft-protocol", rev = "356ea54" }
notify = "4.0"
pretty_env_logger = "0.4"
proxy-protocol = "0.5"
rand = "0.8"
serde = "1.0"
serde_json = "1.0"
shlex = "1.1"
thiserror = "1.0"
tokio = { version = "1", default-features = false, features = ["rt-multi-thread", "io-util", "net", "macros", "time", "process", "signal"] }
tokio = { version = "1", default-features = false, features = ["rt-multi-thread", "io-util", "net", "macros", "time", "process", "signal", "sync", "fs"] }
toml = "0.5"
version-compare = "0.1"
# Feature: rcon
rust_rcon = { package = "rcon", version = "0.5", optional = true }
rust_rcon = { package = "rcon", version = "0.5.2", optional = true }
async-std = { version = "1.9.0", deafult-features = false, optional = true }
# Feature: lobby
named-binary-tag = { version = "0.6", optional = true }
quartz_nbt = { version = "0.2", optional = true }
uuid = { version = "0.7", optional = true, features = ["v3"] }
[target.'cfg(unix)'.dependencies]
libc = "0.2"

View File

@@ -18,9 +18,8 @@ lazymc functions as proxy between clients and the server. It handles all
incoming status connections until the server is started and then transparently
relays/proxies the rest. All without them noticing.
_Note: this is a prototype and may be incomplete._
https://user-images.githubusercontent.com/856222/141378688-882082be-9efa-4cfe-81cc-5a7ab8b8e86b.mp4
https://user-images.githubusercontent.com/856222/140804726-ba1a8e59-85d9-413b-8229-03be84b55d51.mp4
<details><summary>Click to see screenshots</summary>
<p>
@@ -37,11 +36,17 @@ https://user-images.githubusercontent.com/856222/140804726-ba1a8e59-85d9-413b-82
- Very efficient, lightweight & low-profile (~3KB RAM)
- Supports Minecraft Java Edition 1.6+, supports modded (e.g. Forge, FTB)
- Transparent join: hold clients when server starts, relay when ready, without them noticing
- Configure joining client occupation methods:
- Hold: hold clients when server starts, relay when ready, without them noticing
- Kick: kick clients when server starts, with a starting message
- Forward: forward client to another IP when server starts
- _Lobby: keep client in emulated server with lobby world, teleport to real server when ready ([experimental*](./docs/join-method-lobby.md))_
- Customizable MOTD and login messages
- Automatically manages `server.properties` (host, port and RCON settings)
- Automatically block banned IPs from server within `lazymc`
- Graceful server sleep/shutdown through RCON (with `SIGTERM` fallback on Linux/Unix)
- Restart server on crash
- Lockout mode
## Requirements
@@ -55,28 +60,27 @@ won't be able to set this up._
## Usage
_Note: these instructions are for Linux & macOS, for Windows look
[here](./docs/usage-windows.md)._
Make sure you meet all [requirements](#requirements).
_Note: Installation options are limited at this moment. Ready-to-go binaries
will be published later. For now we compile and install from source._
Download the appropriate binary for your system from the [latest
release][latest-release] page.
To compile and install you need Rust, install it through `rustup`: https://rustup.rs/
When Rust is installed, compile and install `lazymc` from this git repository:
Place the binary in your Minecraft server directory, rename it if you like.
Open a terminal, go to the directory, and make sure you can invoke it:
```bash
# Compile and install lazymc from source
cargo install -f --git https://github.com/timvisee/lazymc
# Ensure lazymc works
lazymc --help
chmod a+x ./lazymc
./lazymc --help
```
When `lazymc` is available, change into your server directory. Then set up the
[configuration](./res/lazymc.toml) and start it up:
When `lazymc` is set-up, change into your server directory if you haven't
already. Then set up the [configuration](./res/lazymc.toml) and start it up:
```bash
# Change into your server directory
# Change into your server directory (if you haven't already)
cd server
# Generate lazymc configuration
@@ -90,8 +94,55 @@ nano lazymc.toml
lazymc start
```
Everything should now be running. Connect with your Minecraft client to wake
your server up!
Please see [extras](./docs/extras.md) for recommendations and additional things
to set up (e.g. how to fix incorrect client IPs and IP banning on your server).
After you've read through the [extras](./docs/extras.md), everything should now
be ready to go! Connect with your Minecraft client to wake your server up!
_Note: If a binary for your system isn't provided, please [compile from
source](#compile-from-source). Installation options are limited at this moment. More will be added
later._
[latest-release]: https://github.com/timvisee/lazymc/releases/latest
## Compile from source
Make sure you meet all [requirements](#requirements).
To compile from source you need Rust, install it through `rustup`: https://rustup.rs/
When Rust is installed, compile and install `lazymc` from this git repository
directly:
```bash
# Compile and install lazymc from source
cargo install -f --git https://github.com/timvisee/lazymc
# Ensure lazymc works
lazymc --help
```
Or clone the repository and build it yourself:
```bash
# Clone repository
git clone https://github.com/timvisee/lazymc
cd lazymc
# Compile
cargo build --release
# Run lazymc
./target/release/lazymc --help
```
## Third-party usage & implementations
A list of third-party implementations, projects using `lazymc`, that you might
find useful:
- Docker: [crbanman/papermc-lazymc](https://hub.docker.com/r/crbanman/papermc-lazymc) _(PaperMC with lazymc in Docker)_
## License

27
TODO.md Normal file
View File

@@ -0,0 +1,27 @@
# TODO
- Better organize code
- Resolve TODOs in code
- Don't drop errors, handle everywhere where needed (some were dropped while
prototyping to speed up development)
## Nice to have
- Use server whitelist/blacklist
- Console error if server already started on port, not through `lazymc`
- Kick with message if proxy-to-server connection fails for new client.
- Test configuration on start (server dir exists, command not empty)
- Dynamically increase/decrease server polling interval based on server state
- Server polling through query (`enable-query` in `server.properties`, uses GameSpy4 protocol)
## Experiment
- `io_uring` on Linux for efficient proxying (see `tokio-uring`)
## Lobby join method
- add support for more Minecraft versions (with changed protocols)
- support online mode (encryption)
- hold back packets (whitelist), forward to server at connect before joining
- add support for forge (emulate mod list communication)
- on login plugin request during login state, respond with empty payload, not supported

28
docs/extras.md Normal file
View File

@@ -0,0 +1,28 @@
# Extras
Some extra steps and recommendations when using lazymc:
Before you use this in production, always ensure starting and stopping the
server works as expected by connecting to it once. Watch `lazymc`s output while
it starts and stops. If stopping results in errors, fix this first to prevent
corrupting world/user data.
Follow this repository with the _Watch_ button on the top right to be notified
of new releases.
## Recommended
- [Protocol version](./protocol-version.md):
_set correct Minecraft protocol version for the best client compatability_
- [Proxy IP](./proxy-ip.md):
_fix incorrect client IPs on Minecraft server, notify server of correct IP with `PROXY` header_
## Tips
- [bash with start command](./command_bash.md):
_how to properly use a bash script as server start command_
## Experimental features
- [Join method: lobby](./join-method-lobby.md):
_keep clients in fake lobby world while server starts, teleport to real server when ready_

87
docs/join-method-lobby.md Normal file
View File

@@ -0,0 +1,87 @@
# Join method: lobby
**Note: this is highly experimental, incomplete, and may break your game. See
[warning](#warning).**
The lobby join method allows you to keep clients in a lobby world while the
server is starting. When the server is ready, the player is _teleported_ to the
real server.
`lazymc` emulates a fake server with an empty lobby world. The player is put in
this world, floating in space. A custom message is shown on the client to notify
we're waiting on the server to start.
![Lobby screenshot](../res/screenshot/lobby.png)
## Warning
This feature is highly experimental, incomplete and unstable. This may break the
game and crash clients. Don't use this unless you know what you're doing. Never
enable this in a production environment.
Current limitations:
- Only works with offline mode
- Only works with vanilla Minecraft clients, does not work with modded (e.g. Forge, FTB)
- Probably only works with Minecraft 1.16-1.17.1 (tested with 1.17.1)
- This method will consume the client, following configured join methods won't be used.
At this time it is unknown if some of the above limitations will ever be lifted,
or if this will ever be implemented in a robust manner.
## Usage
_Note: you must use `lazymc v0.2.0` or above with the `lobby` feature enabled._
To try this out, simply add the `"lobby"` method to the `join.methods` list in
your `lazymc.toml` configuration file:
```toml
# -- snip --
[join]
methods = [
"lobby",
"kick",
]
# -- snip --
```
Then configure the lobby to your likings:
```toml
# -- snip --
[join.lobby]
# Lobby occupation method.
# The client joins a fake lobby server with an empty world, floating in space.
# A message is overlayed on screen to notify the server is starting.
# The client will be teleported to the real server once it is ready.
# This may keep the client occupied forever if no timeout is set.
# Consumes client, not allowing other join methods afterwards.
# !!! WARNING !!!
# This is highly experimental and unstable.
# This may break the game and crash clients.
# Don't enable this unless you know what you're doing.
#
# - Only works with offline mode
# - Only works with vanilla Minecraft clients, does not work with modded
# - Only tested with Minecraft 1.17.1
# Maximum time in seconds in the lobby while the server starts.
timeout = 600
# Message banner in lobby shown to client.
message = "§2Server is starting\n§7⌛ Please wait..."
# Sound effect to play when server is ready.
ready_sound = "block.note_block.chime"
# -- snip --
```
_Note: this might have changed, see the latest configuration
[here](../res/lazymc.toml)._

39
docs/protocol-version.md Normal file
View File

@@ -0,0 +1,39 @@
# Protocol version
The Minecraft protocol uses a version number to distinguish between different
protocol versions. Each new Minecraft version having a change in its protocol
gets a new protocol version.
## List of versions
- https://wiki.vg/Protocol_version_numbers#Versions_after_the_Netty_rewrite
## Configuration
In `lazymc` you may configure what protocol version to use:
[`lazymc.toml`](../res/lazymc.toml):
```bash
# -- snip --
[public]
# Server version & protocol hint.
# Sent to clients until actual server version is known.
# See: https://git.io/J1Fvx
version = "1.17.1"
protocol = 756
# -- snip --
```
It is highly recommended to set these to match that of your server version to
allow the best compatibility with clients.
- Set `public.protocol` to the number matching that of your server version
(see [this](#list-of-versions) list)
- Set `public.version` to any string you like. Shows up in read in clients that
have an incompatibel protocol version number
These are used as hint. `lazymc` will automatically use the protocol version of
your Minecraft server once it has started at least once.

79
docs/proxy-ip.md Normal file
View File

@@ -0,0 +1,79 @@
# Proxy IP
lazymc acts as a proxy most of the time. Because of this the Minecraft server
will think all clients connect from the same IP, being the IP lazymc proxies
from.
This breaks IP banning (`/ban-ip`, amongst other IP related things). This may be
a problematic issue for your server.
Luckily, this can be fixed with the [proxy header](#proxy-header). lazymc has
support for this, and can be used with a companion plugin on your server.
## Proxy header
The `PROXY` header may be used to notify the Minecraft server of the real client
IP.
When a new connection is opened to the Minecraft server, the Minecraft server
will read the `PROXY` header with client-IP information. Once read, it will set
the correct client IP internally and will resume communicating with the client
normally.
To enable this with lazymc you must do two things:
- [Modify the lazymc configuration](#configuration)
- [Install a companion plugin](#server-plugin)
## Configuration
To use the `PROXY` header with your Minecraft server, set `server.send_proxy_v2`
to `true`.
[`lazymc.toml`](../res/lazymc.toml):
```toml
# -- snip --
[server]
send_proxy_v2 = true
# -- snip --
```
Other related properties, you probably won't need to touch, include:
- `server.send_proxy_v2`: set to `true` to enable `PROXY` header for Minecraft server
- `join.forward.send_proxy_v2`: set to `true` to enable `PROXY` header forwarded server, if `forward` join method is used
- `rcon.send_proxy_v2`: set to `true` to enable `PROXY` header for RCON connections for Minecraft server
## Server plugin
Install one of these plugins as companion on your server to enable support for
the `PROXY` header. This requires Minecraft server software supporting plugins,
the vanilla Minecraft server does not support this.
If lazymc connects to a Spigot compatible server, use any of:
- https://github.com/riku6460/SpigotProxy ([JAR](https://github.com/riku6460/SpigotProxy/releases/latest))
- https://github.com/timvisee/spigot-proxy
If lazymc connects to a BungeeCord server, use any of:
- https://github.com/MinelinkNetwork/BungeeProxy
## Warning: connection failures
Use of the `PROXY` header must be enabled or disabled on both lazymc and your
Minecraft server using a companion plugin.
If either of the two is missing or misconfigured, it will result in connection
failures due to a missing or unrecognized header.
## Warning: fake IP
When enabling the `PROXY` header on your Minecraft server, malicious parties may
send this header to fake their real IP.
To solve this, make sure the Minecraft server is only publicly reachable through
lazymc. This can be done by setting the Minecraft server IP to a local address
only, or by setting up firewall rules.

44
docs/usage-windows.md Normal file
View File

@@ -0,0 +1,44 @@
## Usage on Windows
Make sure you meet all [requirements](../README.md#requirements).
Download the `lazymc-*-windows.exe` Windows executable for your system from the
[latest release][latest-release] page.
Place the binary in your Minecraft server directory, and rename it to
`lazymc.exe`.
Open a terminal, go to the server directory, and make sure you can execute it:
```bash
.\lazymc --help
```
When `lazymc` is ready, set up the [configuration](./res/lazymc.toml) and start
it up:
```bash
# In your Minecraft server directory:
# Generate lazymc configuration
.\lazymc config generate
# Edit configuration
# Set the correct server address, directory and start command
notepad lazymc.toml
# Start lazymc
.\lazymc start
```
Please see [extras](./extras.md) for recommendations and additional things
to set up (e.g. how to fix incorrect client IPs and IP banning on your server).
After you've read through the [extras](./extras.md), everything should now
be ready to go! Connect with your Minecraft client to wake your server up!
_Note: if you put `lazymc` in `PATH`, or if you
[install](../README.md#compile-from-source) it through Cargo, you can invoke
`lazymc` everywhere directly without the `.\` prefix.
[latest-release]: https://github.com/timvisee/lazymc/releases/latest

18
res/dimension.snbt Normal file
View File

@@ -0,0 +1,18 @@
{
piglin_safe: 1b,
natural: 0b,
ambient_light: 0.0f,
fixed_time: 0,
infiniburn: "minecraft:infiniburn_overworld",
respawn_anchor_works: 0b,
has_skylight: 1b,
bed_works: 0b,
effects: "minecraft:the_end",
has_raids: 0b,
min_y: 0,
height: 256,
logical_height: 256,
coordinate_scale: 1.0d,
ultrawarm: 0b,
has_ceiling: 0b
}

2093
res/dimension_codec.snbt Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@
# You can probably leave the rest as-is.
#
# You may generate a new configuration with: lazymc config generate
# Or find the latest at: https://is.gd/WWBIQu
# Or find the latest at: https://git.io/J1Fvq
[public]
# Public address. IP and port users connect to.
@@ -17,7 +17,7 @@
# Server version & protocol hint.
# Sent to clients until actual server version is known.
# See: https://is.gd/FTQKTP
# See: https://git.io/J1Fvx
#version = "1.17.1"
#protocol = 756
@@ -30,7 +30,7 @@
directory = "."
# Command to start the server.
# Warning: if using a bash script read: https://is.gd/k8SQYv
# Warning: if using a bash script read: https://git.io/J1FvZ
command = "java -Xmx1G -Xms1G -jar server.jar --nogui"
# Immediately wake server when starting lazymc.
@@ -39,6 +39,22 @@ command = "java -Xmx1G -Xms1G -jar server.jar --nogui"
# Immediately wake server after crash.
#wake_on_crash = false
# Server start/stop timeout in seconds. Force kill server process if it takes too long.
#start_timeout = 300
#stop_timeout = 150
# Block banned IPs as listed in banned-ips.json in server directory.
#block_banned_ips = true
# Drop connections from banned IPs.
# Banned IPs won't be able to ping or request server status.
# On connect, clients show a 'Disconnected' message rather than the ban reason.
#drop_banned_ips = false
# Add HAProxy v2 header to proxied connections.
# See: https://git.io/J1bYb
#send_proxy_v2 = false
[time]
# Sleep after number of seconds.
#sleep_after = 60
@@ -46,31 +62,94 @@ command = "java -Xmx1G -Xms1G -jar server.jar --nogui"
# Minimum time in seconds to stay online when server is started.
#minimum_online_time = 60
# Hold client for number of seconds on connect while server starts.
# 0 to disable and disconnect immediately, keep below Minecraft timeout of 30 seconds.
#hold_client_for = 25
# Server start/stop timeout in seconds. Force kill server process if it takes too long.
#start_timeout = 300
#stop_timeout = 150
[messages]
# MOTDs, shown in server browser.
#motd_sleeping = "☠ Server is sleeping\n§2☻ Join to start it up"
#motd_starting = "§2☻ Server is starting...\n§7⌛ Please wait..."
#motd_stopping = "☠ Server going to sleep...\n⌛ Please wait..."
[motd]
# MOTD, shown in server browser.
#sleeping = "☠ Server is sleeping\n§2☻ Join to start it up"
#starting = "§2☻ Server is starting...\n§7⌛ Please wait..."
#stopping = "☠ Server going to sleep...\n⌛ Please wait..."
# Use MOTD from Minecraft server once known.
#use_server_motd = false
#from_server = false
# Login messages, when user tries to connect.
#login_starting = "Server is starting... §c♥§r\n\nThis may take some time.\n\nPlease try to reconnect in a minute."
#login_stopping = "Server is going to sleep... §7☠§r\n\nPlease try to reconnect in a minute to wake it again."
[join]
# Methods to use to occupy a client on join while the server is starting.
# Read about all methods and configure them below.
# Methods are used in order, if none is set, the client disconnects without a message.
#methods = [
# "hold",
# "kick",
#]
[join.kick]
# Kick occupation method.
# Instantly kicks a client with a message.
# Message shown when client is kicked while server is starting/stopping.
#starting = "Server is starting... §c♥§r\n\nThis may take some time.\n\nPlease try to reconnect in a minute."
#stopping = "Server is going to sleep... §7☠§r\n\nPlease try to reconnect in a minute to wake it again."
[join.hold]
# Hold occupation method.
# Holds back a joining client while the server is started until it is ready.
# 'Connecting the server...' is shown on the client while it's held back.
# If the server starts fast enough, the client won't notice it was sleeping at all.
# This works for a limited time of 30 seconds, after which the Minecraft client times out.
# Hold client for number of seconds on connect while server starts.
# Keep below Minecraft timeout of 30 seconds.
#timeout = 25
[join.forward]
# Forward occupation method.
# Instantly forwards (proxies) the client to a different address.
# You may need to configure target server for it, such as allowing proxies.
# Consumes client, not allowing other join methods afterwards.
# IP and port to forward to.
# The target server will receive original client handshake and login request as received by lazymc.
#address = "127.0.0.1:25565"
# Add HAProxy v2 header to forwarded connections.
# See: https://git.io/J1bYb
#send_proxy_v2 = false
[join.lobby]
# Lobby occupation method.
# The client joins a fake lobby server with an empty world, floating in space.
# A message is overlayed on screen to notify the server is starting.
# The client will be teleported to the real server once it is ready.
# This may keep the client occupied forever if no timeout is set.
# Consumes client, not allowing other join methods afterwards.
# !!! WARNING !!!
# This is highly experimental, incomplete and unstable.
# This may break the game and crash clients.
# Don't enable this unless you know what you're doing.
#
# - Only works with offline mode
# - Only works with vanilla Minecraft clients, does not work with modded
# - Only tested with Minecraft 1.17.1
# Maximum time in seconds in the lobby while the server starts.
#timeout = 600
# Message banner in lobby shown to client.
#message = "§2Server is starting\n§7⌛ Please wait..."
# Sound effect to play when server is ready.
#ready_sound = "block.note_block.chime"
[lockout]
# Enable to prevent everybody from connecting through lazymc. Instantly kicks player.
#enabled = false
# Kick players with following message.
#message = "Server is closed §7☠§r\n\nPlease try to reconnect in a minute."
[rcon]
# Enable sleeping server through RCON.
# Must be enabled on Windows.
#enabled = true
#enabled = false # default: false, true on Windows
# Server RCON port. Must differ from public and server port.
#port = 25575
@@ -80,6 +159,15 @@ command = "java -Xmx1G -Xms1G -jar server.jar --nogui"
#password = ""
#randomize_password = true
# Add HAProxy v2 header to RCON connections.
# See: https://git.io/J1bYb
#send_proxy_v2 = false
[advanced]
# Automatically update values in Minecraft server.properties file as required.
#rewrite_server_properties = true
[config]
# lazymc version this configuration is for.
# Don't change unless you know what you're doing.
version = "0.2.3"

BIN
res/screenshot/lobby.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

View File

@@ -5,6 +5,7 @@ use clap::ArgMatches;
use crate::config::{self, Config};
use crate::mc::server_properties;
use crate::proto;
use crate::service;
/// RCON randomized password length.
@@ -132,9 +133,23 @@ fn rewrite_server_properties(config: &Config) {
let mut changes = HashMap::from([
("server-ip", config.server.address.ip().to_string()),
("server-port", config.server.address.port().to_string()),
("enable-status", "true".into()),
("query.port", config.server.address.port().to_string()),
]);
// If connecting to server over non-loopback address, disable proxy blocking
if !config.server.address.ip().is_loopback() {
changes.extend([("prevent-proxy-connections", "false".into())]);
}
// Update network compression threshold for lobby mode
if config.join.methods.contains(&config::Method::Lobby) {
changes.extend([(
"network-compression-threshold",
proto::COMPRESSION_THRESHOLD.to_string(),
)]);
}
// Add RCON configuration
#[cfg(feature = "rcon")]
if config.rcon.enabled {

View File

@@ -5,13 +5,18 @@ use std::path::{Path, PathBuf};
use clap::ArgMatches;
use serde::Deserialize;
use version_compare::Cmp;
use crate::proto;
use crate::util::error::{quit_error, quit_error_msg, ErrorHintsBuilder};
use crate::util::serde::to_socket_addrs;
/// Default configuration file location.
pub const CONFIG_FILE: &str = "lazymc.toml";
/// Configuration version user should be using, or warning will be shown.
const CONFIG_VERSION: &str = "0.2.1";
/// Load config from file, based on CLI arguments.
///
/// Quits with an error message on failure.
@@ -69,9 +74,17 @@ pub struct Config {
#[serde(default)]
pub time: Time,
/// Messages, shown to the user.
/// MOTD configuration.
#[serde(default)]
pub messages: Messages,
pub motd: Motd,
/// Join configuration.
#[serde(default)]
pub join: Join,
/// Lockout feature.
#[serde(default)]
pub lockout: Lockout,
/// RCON configuration.
#[serde(default)]
@@ -80,13 +93,32 @@ pub struct Config {
/// Advanced configuration.
#[serde(default)]
pub advanced: Advanced,
/// Config configuration.
#[serde(default)]
pub config: ConfigConfig,
}
impl Config {
/// Load configuration from file.
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self, io::Error> {
let data = fs::read(path)?;
let config = toml::from_slice(&data)?;
let config: Config = toml::from_slice(&data)?;
// Show warning if config version is problematic
match &config.config.version {
None => warn!(target: "lazymc::config", "Config version unknown, it may be outdated"),
Some(version) => match version_compare::compare_to(version, CONFIG_VERSION, Cmp::Ge) {
Ok(false) => {
warn!(target: "lazymc::config", "Config is for older lazymc version, you may need to update it")
}
Err(_) => {
warn!(target: "lazymc::config", "Config version is invalid, you may need to update it")
}
Ok(true) => {}
},
}
Ok(config)
}
}
@@ -96,6 +128,7 @@ impl Config {
#[serde(default)]
pub struct Public {
/// Public address.
#[serde(deserialize_with = "to_socket_addrs")]
pub address: SocketAddr,
/// Minecraft protocol version name hint.
@@ -126,7 +159,10 @@ pub struct Server {
pub command: String,
/// Server address.
#[serde(default = "server_address_default")]
#[serde(
deserialize_with = "to_socket_addrs",
default = "server_address_default"
)]
pub address: SocketAddr,
/// Immediately wake server when starting lazymc.
@@ -136,6 +172,26 @@ pub struct Server {
/// Immediately wake server after crash.
#[serde(default)]
pub wake_on_crash: bool,
/// Server starting timeout. Force kill server process if it takes longer.
#[serde(default = "u32_300")]
pub start_timeout: u32,
/// Server stopping timeout. Force kill server process if it takes longer.
#[serde(default = "u32_150")]
pub stop_timeout: u32,
/// Block banned IPs as listed in banned-ips.json in server directory.
#[serde(default = "bool_true")]
pub block_banned_ips: bool,
/// Drop connections from banned IPs.
#[serde(default)]
pub drop_banned_ips: bool,
/// Add HAProxy v2 header to proxied connections.
#[serde(default)]
pub send_proxy_v2: bool,
}
/// Time configuration.
@@ -148,24 +204,6 @@ pub struct Time {
/// Minimum time in seconds to stay online when server is started.
#[serde(default, alias = "minimum_online_time")]
pub min_online_time: u32,
/// Hold client for number of seconds while server starts, instead of kicking immediately.
pub hold_client_for: u32,
/// Server starting timeout. Force kill server process if it takes longer.
#[serde(alias = "starting_timeout")]
pub start_timeout: u32,
/// Server stopping timeout. Force kill server process if it takes longer.
#[serde(alias = "stopping_timeout")]
pub stop_timeout: u32,
}
impl Time {
/// Whether to hold clients.
pub fn hold(&self) -> bool {
self.hold_client_for > 0
}
}
impl Default for Time {
@@ -173,45 +211,186 @@ impl Default for Time {
Self {
sleep_after: 60,
min_online_time: 60,
hold_client_for: 25,
start_timeout: 300,
stop_timeout: 150,
}
}
}
/// Message configuration.
/// MOTD configuration.
#[derive(Debug, Deserialize)]
#[serde(default)]
pub struct Messages {
pub struct Motd {
/// MOTD when server is sleeping.
pub motd_sleeping: String,
pub sleeping: String,
/// MOTD when server is starting.
pub motd_starting: String,
pub starting: String,
/// MOTD when server is stopping.
pub motd_stopping: String,
pub stopping: String,
/// Use MOTD from Minecraft server once known.
pub use_server_motd: bool,
/// Login message when server is starting.
pub login_starting: String,
/// Login message when server is stopping.
pub login_stopping: String,
pub from_server: bool,
}
impl Default for Messages {
impl Default for Motd {
fn default() -> Self {
Self {
motd_sleeping: "☠ Server is sleeping\n§2☻ Join to start it up".into(),
motd_starting: "§2☻ Server is starting...\n§7⌛ Please wait...".into(),
motd_stopping: "☠ Server going to sleep...\n⌛ Please wait...".into(),
use_server_motd: false,
login_starting: "Server is starting... §c♥§r\n\nThis may take some time.\n\nPlease try to reconnect in a minute.".into(),
login_stopping: "Server is going to sleep... §7☠§r\n\nPlease try to reconnect in a minute to wake it again.".into(),
sleeping: "☠ Server is sleeping\n§2☻ Join to start it up".into(),
starting: "§2☻ Server is starting...\n§7⌛ Please wait...".into(),
stopping: "☠ Server going to sleep...\n⌛ Please wait...".into(),
from_server: false,
}
}
}
/// Join method types.
#[derive(Debug, Deserialize, Copy, Clone, Eq, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum Method {
/// Kick client with message.
Kick,
/// Hold client connection until server is ready.
Hold,
/// Forward connection to another host.
Forward,
/// Keep client in temporary fake lobby until server is ready.
Lobby,
}
/// Join configuration.
#[derive(Debug, Deserialize)]
#[serde(default)]
pub struct Join {
/// Join methods.
pub methods: Vec<Method>,
/// Join kick configuration.
#[serde(default)]
pub kick: JoinKick,
/// Join hold configuration.
#[serde(default)]
pub hold: JoinHold,
/// Join forward configuration.
#[serde(default)]
pub forward: JoinForward,
/// Join lobby configuration.
#[serde(default)]
pub lobby: JoinLobby,
}
impl Default for Join {
fn default() -> Self {
Self {
methods: vec![Method::Hold, Method::Kick],
kick: Default::default(),
hold: Default::default(),
forward: Default::default(),
lobby: Default::default(),
}
}
}
/// Join kick configuration.
#[derive(Debug, Deserialize)]
#[serde(default)]
pub struct JoinKick {
/// Kick message when server is starting.
pub starting: String,
/// Kick message when server is stopping.
pub stopping: String,
}
impl Default for JoinKick {
fn default() -> Self {
Self {
starting: "Server is starting... §c♥§r\n\nThis may take some time.\n\nPlease try to reconnect in a minute.".into(),
stopping: "Server is going to sleep... §7☠§r\n\nPlease try to reconnect in a minute to wake it again.".into(),
}
}
}
/// Join hold configuration.
#[derive(Debug, Deserialize)]
#[serde(default)]
pub struct JoinHold {
/// Hold client for number of seconds on connect while server starts.
pub timeout: u32,
}
impl Default for JoinHold {
fn default() -> Self {
Self { timeout: 25 }
}
}
/// Join forward configuration.
#[derive(Debug, Deserialize)]
#[serde(default)]
pub struct JoinForward {
/// IP and port to forward to.
#[serde(deserialize_with = "to_socket_addrs")]
pub address: SocketAddr,
/// Add HAProxy v2 header to proxied connections.
#[serde(default)]
pub send_proxy_v2: bool,
}
impl Default for JoinForward {
fn default() -> Self {
Self {
address: "127.0.0.1:25565".parse().unwrap(),
send_proxy_v2: false,
}
}
}
/// Join lobby configuration.
#[derive(Debug, Deserialize)]
#[serde(default)]
pub struct JoinLobby {
/// Hold client in lobby for number of seconds on connect while server starts.
pub timeout: u32,
/// Message banner in lobby shown to client.
pub message: String,
/// Sound effect to play when server is ready.
pub ready_sound: Option<String>,
}
impl Default for JoinLobby {
fn default() -> Self {
Self {
timeout: 10 * 60,
message: "§2Server is starting\n§7⌛ Please wait...".into(),
ready_sound: Some("block.note_block.chime".into()),
}
}
}
/// Lockout configuration.
#[derive(Debug, Deserialize)]
#[serde(default)]
pub struct Lockout {
/// Enable to prevent everybody from connecting through lazymc. Instantly kicks player.
pub enabled: bool,
/// Kick players with following message.
pub message: String,
}
impl Default for Lockout {
fn default() -> Self {
Self {
enabled: false,
message: "Server is closed §7☠§r\n\nPlease come back another time.".into(),
}
}
}
@@ -231,15 +410,19 @@ pub struct Rcon {
/// Randomize server RCON password on each start.
pub randomize_password: bool,
/// Add HAProxy v2 header to RCON connections.
pub send_proxy_v2: bool,
}
impl Default for Rcon {
fn default() -> Self {
Self {
enabled: true,
enabled: cfg!(windows),
port: 25575,
password: "".into(),
randomize_password: true,
send_proxy_v2: false,
}
}
}
@@ -260,6 +443,20 @@ impl Default for Advanced {
}
}
/// Config configuration.
#[derive(Debug, Deserialize)]
#[serde(default)]
pub struct ConfigConfig {
/// Configuration for lazymc version.
pub version: Option<String>,
}
impl Default for ConfigConfig {
fn default() -> Self {
Self { version: None }
}
}
fn option_pathbuf_dot() -> Option<PathBuf> {
Some(".".into())
}
@@ -267,3 +464,15 @@ fn option_pathbuf_dot() -> Option<PathBuf> {
fn server_address_default() -> SocketAddr {
"127.0.0.1:25566".parse().unwrap()
}
fn u32_300() -> u32 {
300
}
fn u32_150() -> u32 {
300
}
fn bool_true() -> bool {
true
}

32
src/join/forward.rs Normal file
View File

@@ -0,0 +1,32 @@
use std::sync::Arc;
use bytes::BytesMut;
use tokio::net::TcpStream;
use crate::config::*;
use crate::proxy::ProxyHeader;
use crate::service;
use super::MethodResult;
/// Forward the client.
pub async fn occupy(
config: Arc<Config>,
inbound: TcpStream,
inbound_history: &mut BytesMut,
) -> Result<MethodResult, ()> {
trace!(target: "lazymc", "Using forward method to occupy joining client");
debug!(target: "lazymc", "Forwarding client to {:?}!", config.join.forward.address);
service::server::route_proxy_address_queue(
inbound,
ProxyHeader::Proxy.not_none(config.join.forward.send_proxy_v2),
config.join.forward.address,
inbound_history.clone(),
);
// TODO: do not consume, continue on proxy connect failure
Ok(MethodResult::Consumed)
}

101
src/join/hold.rs Normal file
View File

@@ -0,0 +1,101 @@
use std::ops::Deref;
use std::sync::Arc;
use std::time::Duration;
use bytes::BytesMut;
use tokio::net::TcpStream;
use tokio::time;
use crate::config::*;
use crate::server::{Server, State};
use crate::service;
use super::MethodResult;
/// Hold the client.
pub async fn occupy(
config: Arc<Config>,
server: Arc<Server>,
inbound: TcpStream,
inbound_history: &mut BytesMut,
) -> Result<MethodResult, ()> {
trace!(target: "lazymc", "Using hold method to occupy joining client");
// Server must be starting
if server.state() != State::Starting {
return Ok(MethodResult::Continue(inbound));
}
// Start holding, consume client
if hold(&config, &server).await? {
service::server::route_proxy_queue(inbound, config, inbound_history.clone());
return Ok(MethodResult::Consumed);
}
Ok(MethodResult::Continue(inbound))
}
/// Hold a client while server starts.
///
/// Returns holding status. `true` if client is held and it should be proxied, `false` it was held
/// but it timed out.
async fn hold<'a>(config: &Config, server: &Server) -> Result<bool, ()> {
trace!(target: "lazymc", "Started holding client");
// A task to wait for suitable server state
// Waits for started state, errors if stopping/stopped state is reached
let task_wait = async {
let mut state = server.state_receiver();
loop {
// Wait for state change
state.changed().await.unwrap();
match state.borrow().deref() {
// Still waiting on server start
State::Starting => {
trace!(target: "lazymc", "Server not ready, holding client for longer");
continue;
}
// Server started, start relaying and proxy
State::Started => {
break true;
}
// Server stopping, this shouldn't happen, kick
State::Stopping => {
warn!(target: "lazymc", "Server stopping for held client, disconnecting");
break false;
}
// Server stopped, this shouldn't happen, disconnect
State::Stopped => {
error!(target: "lazymc", "Server stopped for held client, disconnecting");
break false;
}
}
}
};
// Wait for server state with timeout
let timeout = Duration::from_secs(config.join.hold.timeout as u64);
match time::timeout(timeout, task_wait).await {
// Relay client to proxy
Ok(true) => {
info!(target: "lazymc", "Server ready for held client, relaying to server");
Ok(true)
}
// Server stopping/stopped, this shouldn't happen, kick
Ok(false) => {
warn!(target: "lazymc", "Server stopping for held client");
Ok(false)
}
// Timeout reached, kick with starting message
Err(_) => {
warn!(target: "lazymc", "Held client reached timeout of {}s", config.join.hold.timeout);
Ok(false)
}
}
}

33
src/join/kick.rs Normal file
View File

@@ -0,0 +1,33 @@
use tokio::net::TcpStream;
use crate::config::*;
use crate::net;
use crate::proto::action;
use crate::proto::client::Client;
use crate::server::{self, Server};
use super::MethodResult;
/// Kick the client.
pub async fn occupy(
client: &Client,
config: &Config,
server: &Server,
mut inbound: TcpStream,
) -> Result<MethodResult, ()> {
trace!(target: "lazymc", "Using kick method to occupy joining client");
// Select message and kick
let msg = match server.state() {
server::State::Starting | server::State::Stopped | server::State::Started => {
&config.join.kick.starting
}
server::State::Stopping => &config.join.kick.stopping,
};
action::kick(client, msg, &mut inbound.split().1).await?;
// Gracefully close connection
net::close_tcp_stream(inbound).await.map_err(|_| ())?;
Ok(MethodResult::Consumed)
}

30
src/join/lobby.rs Normal file
View File

@@ -0,0 +1,30 @@
use std::sync::Arc;
use bytes::BytesMut;
use tokio::net::TcpStream;
use crate::config::*;
use crate::lobby;
use crate::proto::client::{Client, ClientInfo};
use crate::server::Server;
use super::MethodResult;
/// Lobby the client.
pub async fn occupy(
client: &Client,
client_info: ClientInfo,
config: Arc<Config>,
server: Arc<Server>,
inbound: TcpStream,
inbound_queue: BytesMut,
) -> Result<MethodResult, ()> {
trace!(target: "lazymc", "Using lobby method to occupy joining client");
// Start lobby
lobby::serve(client, client_info, inbound, config, server, inbound_queue).await?;
// TODO: do not consume client here, allow other join method on fail
Ok(MethodResult::Consumed)
}

106
src/join/mod.rs Normal file
View File

@@ -0,0 +1,106 @@
use std::sync::Arc;
use bytes::BytesMut;
use tokio::net::TcpStream;
use crate::config::*;
use crate::net;
use crate::proto::client::{Client, ClientInfo, ClientState};
use crate::server::Server;
pub mod forward;
pub mod hold;
pub mod kick;
#[cfg(feature = "lobby")]
pub mod lobby;
/// A result returned by a join occupy method.
pub enum MethodResult {
/// Client is consumed.
Consumed,
/// Method is done, continue with the next.
Continue(TcpStream),
}
/// Start occupying client.
///
/// This assumes the login start packet has just been received.
pub async fn occupy(
client: Client,
#[allow(unused_variables)] client_info: ClientInfo,
config: Arc<Config>,
server: Arc<Server>,
mut inbound: TcpStream,
mut inbound_history: BytesMut,
#[allow(unused_variables)] login_queue: BytesMut,
) -> Result<(), ()> {
// Assert state is correct
assert_eq!(
client.state(),
ClientState::Login,
"when occupying client, it should be in login state"
);
// Go through all configured join methods
for method in &config.join.methods {
// Invoke method, take result
let result = match method {
// Kick method, immediately kick client
Method::Kick => kick::occupy(&client, &config, &server, inbound).await?,
// Hold method, hold client connection while server starts
Method::Hold => {
hold::occupy(
config.clone(),
server.clone(),
inbound,
&mut inbound_history,
)
.await?
}
// Forward method, forward client connection while server starts
Method::Forward => {
forward::occupy(config.clone(), inbound, &mut inbound_history).await?
}
// Lobby method, keep client in lobby while server starts
#[cfg(feature = "lobby")]
Method::Lobby => {
lobby::occupy(
&client,
client_info.clone(),
config.clone(),
server.clone(),
inbound,
login_queue.clone(),
)
.await?
}
// Lobby method, keep client in lobby while server starts
#[cfg(not(feature = "lobby"))]
Method::Lobby => {
error!(target: "lazymc", "Lobby join method not supported in this lazymc build");
MethodResult::Continue(inbound)
}
};
// Handle method result
match result {
MethodResult::Consumed => return Ok(()),
MethodResult::Continue(stream) => {
inbound = stream;
continue;
}
}
}
debug!(target: "lazymc", "No method left to occupy joining client, disconnecting");
// Gracefully close connection
net::close_tcp_stream(inbound).await.map_err(|_| ())?;
Ok(())
}

826
src/lobby.rs Normal file
View File

@@ -0,0 +1,826 @@
use std::io::ErrorKind;
use std::ops::Deref;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::Duration;
use bytes::BytesMut;
use futures::FutureExt;
use minecraft_protocol::data::chat::{Message, Payload};
use minecraft_protocol::decoder::Decoder;
use minecraft_protocol::version::v1_14_4::handshake::Handshake;
use minecraft_protocol::version::v1_14_4::login::{LoginStart, LoginSuccess, SetCompression};
use minecraft_protocol::version::v1_17_1::game::{
ClientBoundKeepAlive, ClientBoundPluginMessage, JoinGame, NamedSoundEffect,
PlayerPositionAndLook, Respawn, SetTitleSubtitle, SetTitleText, SetTitleTimes, TimeUpdate,
};
use nbt::CompoundTag;
use tokio::io::AsyncWriteExt;
use tokio::net::tcp::{ReadHalf, WriteHalf};
use tokio::net::TcpStream;
use tokio::select;
use tokio::time;
use uuid::Uuid;
use crate::config::*;
use crate::mc;
use crate::net;
use crate::proto;
use crate::proto::client::{Client, ClientInfo, ClientState};
use crate::proto::{packet, packets};
use crate::proxy;
use crate::server::{Server, State};
/// Interval to send keep-alive packets at.
const KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(10);
/// Auto incrementing ID source for keep alive packets.
static KEEP_ALIVE_ID: AtomicU64 = AtomicU64::new(0);
/// Timeout for creating new server connection for lobby client.
const SERVER_CONNECT_TIMEOUT: Duration = Duration::from_secs(2 * 60);
/// Timeout for server sending join game packet.
///
/// When the play state is reached, the server should immeditely respond with a join game packet.
/// This defines the maximum timeout for waiting on it.
const SERVER_JOIN_GAME_TIMEOUT: Duration = Duration::from_secs(20);
/// Time to wait before responding to newly connected server.
///
/// Notchian servers are slow, we must wait a little before sending play packets, because the
/// server needs time to transition the client into this state.
/// See warning at: https://wiki.vg/Protocol#Login_Success
const SERVER_WARMUP: Duration = Duration::from_secs(1);
/// Server brand to send to client in lobby world.
///
/// Shown in F3 menu. Updated once client is relayed to real server.
const SERVER_BRAND: &[u8] = b"lazymc";
/// Serve lobby service for given client connection.
///
/// The client must be in the login state, or this will error.
// TODO: do not drop error here, return Box<dyn Error>
// TODO: on error, nicely kick client with message
pub async fn serve(
client: &Client,
client_info: ClientInfo,
mut inbound: TcpStream,
config: Arc<Config>,
server: Arc<Server>,
queue: BytesMut,
) -> Result<(), ()> {
let (mut reader, mut writer) = inbound.split();
// Client must be in login state
if client.state() != ClientState::Login {
error!(target: "lazymc::lobby", "Client reached lobby service with invalid state: {:?}", client.state());
return Err(());
}
// We must have useful client info
if client_info.username.is_none() {
error!(target: "lazymc::lobby", "Client username is unknown, closing connection");
return Err(());
}
// Incoming buffer
let mut inbound_buf = queue;
loop {
// Read packet from stream
let (packet, _raw) = match packet::read_packet(client, &mut inbound_buf, &mut reader).await
{
Ok(Some(packet)) => packet,
Ok(None) => break,
Err(_) => {
error!(target: "lazymc", "Closing connection, error occurred");
break;
}
};
// Grab client state
let client_state = client.state();
// Hijack login start
if client_state == ClientState::Login && packet.id == packets::login::SERVER_LOGIN_START {
// Parse login start packet
let login_start = LoginStart::decode(&mut packet.data.as_slice()).map_err(|_| ())?;
debug!(target: "lazymc::lobby", "Login on lobby server (user: {})", login_start.name);
// Respond with set compression if compression is enabled based on threshold
if proto::COMPRESSION_THRESHOLD >= 0 {
trace!(target: "lazymc::lobby", "Enabling compression for lobby client because server has it enabled (threshold: {})", proto::COMPRESSION_THRESHOLD);
respond_set_compression(client, &mut writer, proto::COMPRESSION_THRESHOLD).await?;
client.set_compression(proto::COMPRESSION_THRESHOLD);
}
// Respond with login success, switch to play state
respond_login_success(client, &mut writer, &login_start).await?;
client.set_state(ClientState::Play);
trace!(target: "lazymc::lobby", "Client login success, sending required play packets for lobby world");
// Send packets to client required to get into workable play state for lobby world
send_lobby_play_packets(client, &mut writer, &server).await?;
// Wait for server to come online, then set up new connection to it
stage_wait(client, &server, &config, &mut writer).await?;
let (server_client, mut outbound, mut server_buf) =
connect_to_server(client_info, &inbound, &config).await?;
let (returned_reader, returned_writer) = inbound.split();
reader = returned_reader;
writer = returned_writer;
// Grab join game packet from server
let join_game =
wait_for_server_join_game(&server_client, &mut outbound, &mut server_buf).await?;
// Reset lobby title
send_lobby_title(client, &mut writer, "").await?;
// Play ready sound if configured
play_lobby_ready_sound(client, &mut writer, &config).await?;
// Wait a second because Notchian servers are slow
// See: https://wiki.vg/Protocol#Login_Success
trace!(target: "lazymc::lobby", "Waiting a second before relaying client connection...");
time::sleep(SERVER_WARMUP).await;
// Send respawn packet, initiates teleport to real server world
send_respawn_from_join(client, &mut writer, join_game).await?;
// Drain inbound connection so we don't confuse the server
// TODO: can we void everything? we might need to forward everything to server except
// for some blacklisted ones
trace!(target: "lazymc::lobby", "Voiding remaining incoming lobby client data before relay to real server");
drain_stream(&mut reader).await?;
// Client and server connection ready now, move client to proxy
debug!(target: "lazymc::lobby", "Server connection ready, relaying lobby client to proxy");
route_proxy(inbound, outbound, server_buf);
return Ok(());
}
// TODO: when receiving Login Plugin Request, respond with empty payload
// See: https://wiki.vg/Protocol#Login_Plugin_Request
// Show unhandled packet warning
debug!(target: "lazymc", "Received unhandled packet:");
debug!(target: "lazymc", "- State: {:?}", client_state);
debug!(target: "lazymc", "- Packet ID: 0x{:02X} ({})", packet.id, packet.id);
}
// Gracefully close connection
net::close_tcp_stream(inbound).await.map_err(|_| ())?;
Ok(())
}
/// Respond to client with a set compression packet.
async fn respond_set_compression(
client: &Client,
writer: &mut WriteHalf<'_>,
threshold: i32,
) -> Result<(), ()> {
packet::write_packet(SetCompression { threshold }, client, writer).await
}
/// Respond to client with login success packet
// TODO: support online mode here
async fn respond_login_success(
client: &Client,
writer: &mut WriteHalf<'_>,
login_start: &LoginStart,
) -> Result<(), ()> {
packet::write_packet(
LoginSuccess {
uuid: Uuid::new_v3(
&Uuid::new_v3(&Uuid::nil(), b"OfflinePlayer"),
login_start.name.as_bytes(),
),
username: login_start.name.clone(),
},
client,
writer,
)
.await
}
/// Play lobby ready sound effect if configured.
async fn play_lobby_ready_sound(
client: &Client,
writer: &mut WriteHalf<'_>,
config: &Config,
) -> Result<(), ()> {
if let Some(sound_name) = config.join.lobby.ready_sound.as_ref() {
// Must not be empty string
if sound_name.trim().is_empty() {
warn!(target: "lazymc::lobby", "Lobby ready sound effect is an empty string, you should remove the configuration item instead");
return Ok(());
}
// Play sound effect
send_lobby_player_pos(client, writer).await?;
send_lobby_sound_effect(client, writer, sound_name).await?;
}
Ok(())
}
/// Send packets to client to get workable play state for lobby world.
async fn send_lobby_play_packets(
client: &Client,
writer: &mut WriteHalf<'_>,
server: &Server,
) -> Result<(), ()> {
// See: https://wiki.vg/Protocol_FAQ#What.27s_the_normal_login_sequence_for_a_client.3F
// Send initial game join
send_lobby_join_game(client, writer, server).await?;
// Send server brand
send_lobby_brand(client, writer).await?;
// Send spawn and player position, disables 'download terrain' screen
send_lobby_player_pos(client, writer).await?;
// Notify client of world time, required once before keep-alive packets
send_lobby_time_update(client, writer).await?;
Ok(())
}
/// Send initial join game packet to client for lobby.
async fn send_lobby_join_game(
client: &Client,
writer: &mut WriteHalf<'_>,
server: &Server,
) -> Result<(), ()> {
// Send Minecrafts default states, slightly customised for lobby world
packet::write_packet(
{
let status = server.status().await;
JoinGame {
// Player ID must be unique, if it collides with another server entity ID the player gets
// in a weird state and cannot move
entity_id: 0,
// TODO: use real server value
hardcore: false,
game_mode: 3,
previous_game_mode: -1i8 as u8,
world_names: vec![
"minecraft:overworld".into(),
"minecraft:the_nether".into(),
"minecraft:the_end".into(),
],
dimension_codec: snbt_to_compound_tag(include_str!("../res/dimension_codec.snbt")),
dimension: snbt_to_compound_tag(include_str!("../res/dimension.snbt")),
world_name: "lazymc:lobby".into(),
hashed_seed: 0,
max_players: status.as_ref().map(|s| s.players.max as i32).unwrap_or(20),
// TODO: use real server value
view_distance: 10,
// TODO: use real server value
reduced_debug_info: false,
// TODO: use real server value
enable_respawn_screen: true,
is_debug: true,
is_flat: false,
}
},
client,
writer,
)
.await
}
/// Send lobby brand to client.
async fn send_lobby_brand(client: &Client, writer: &mut WriteHalf<'_>) -> Result<(), ()> {
packet::write_packet(
ClientBoundPluginMessage {
channel: "minecraft:brand".into(),
data: SERVER_BRAND.into(),
},
client,
writer,
)
.await
}
/// Send lobby player position to client.
async fn send_lobby_player_pos(client: &Client, writer: &mut WriteHalf<'_>) -> Result<(), ()> {
// Send player location, disables download terrain screen
packet::write_packet(
PlayerPositionAndLook {
x: 0.0,
y: 0.0,
z: 0.0,
yaw: 0.0,
pitch: 90.0,
flags: 0b00000000,
teleport_id: 0,
dismount_vehicle: true,
},
client,
writer,
)
.await
}
/// Send lobby time update to client.
async fn send_lobby_time_update(client: &Client, writer: &mut WriteHalf<'_>) -> Result<(), ()> {
const MC_TIME_NOON: i64 = 6000;
// Send time update, required once for keep-alive packets
packet::write_packet(
TimeUpdate {
world_age: MC_TIME_NOON,
time_of_day: MC_TIME_NOON,
},
client,
writer,
)
.await
}
/// Send keep alive packet to client.
///
/// Required periodically in play mode to prevent client timeout.
async fn send_keep_alive(client: &Client, writer: &mut WriteHalf<'_>) -> Result<(), ()> {
packet::write_packet(
ClientBoundKeepAlive {
// Keep sending new IDs
id: KEEP_ALIVE_ID.fetch_add(1, Ordering::Relaxed),
},
client,
writer,
)
.await
// TODO: verify we receive keep alive response with same ID from client
}
/// Send lobby title packets to client.
///
/// This will show the given text for two keep-alive periods. Use a newline for the subtitle.
///
/// If an empty string is given, the title times will be reset to default.
async fn send_lobby_title(
client: &Client,
writer: &mut WriteHalf<'_>,
text: &str,
) -> Result<(), ()> {
// Grab title and subtitle bits
let title = text.lines().next().unwrap_or("");
let subtitle = text.lines().skip(1).collect::<Vec<_>>().join("\n");
// Set title
packet::write_packet(
SetTitleText {
text: Message::new(Payload::text(title)),
},
client,
writer,
)
.await?;
// Set subtitle
packet::write_packet(
SetTitleSubtitle {
text: Message::new(Payload::text(&subtitle)),
},
client,
writer,
)
.await?;
// Set title times
packet::write_packet(
if title.is_empty() && subtitle.is_empty() {
// Defaults: https://minecraft.fandom.com/wiki/Commands/title#Detail
SetTitleTimes {
fade_in: 10,
stay: 70,
fade_out: 20,
}
} else {
SetTitleTimes {
fade_in: 0,
stay: KEEP_ALIVE_INTERVAL.as_secs() as i32 * mc::TICKS_PER_SECOND as i32 * 2,
fade_out: 0,
}
},
client,
writer,
)
.await
}
/// Send lobby ready sound effect to client.
async fn send_lobby_sound_effect(
client: &Client,
writer: &mut WriteHalf<'_>,
sound_name: &str,
) -> Result<(), ()> {
packet::write_packet(
NamedSoundEffect {
sound_name: sound_name.into(),
sound_category: 0,
effect_pos_x: 0,
effect_pos_y: 0,
effect_pos_z: 0,
volume: 1.0,
pitch: 1.0,
},
client,
writer,
)
.await
}
/// Send respawn packet to client to jump from lobby into now loaded server.
///
/// The required details will be fetched from the `join_game` packet as provided by the server.
async fn send_respawn_from_join(
client: &Client,
writer: &mut WriteHalf<'_>,
join_game: JoinGame,
) -> Result<(), ()> {
packet::write_packet(
Respawn {
dimension: join_game.dimension,
world_name: join_game.world_name,
hashed_seed: join_game.hashed_seed,
game_mode: join_game.game_mode,
previous_game_mode: join_game.previous_game_mode,
is_debug: join_game.is_debug,
is_flat: join_game.is_flat,
copy_metadata: false,
},
client,
writer,
)
.await
}
/// An infinite keep-alive loop.
///
/// This will keep sending keep-alive and title packets to the client until it is dropped.
async fn keep_alive_loop(
client: &Client,
writer: &mut WriteHalf<'_>,
config: &Config,
) -> Result<(), ()> {
let mut interval = time::interval(KEEP_ALIVE_INTERVAL);
loop {
interval.tick().await;
trace!(target: "lazymc::lobby", "Sending keep-alive sequence to lobby client");
// Send keep alive and title packets
send_keep_alive(client, writer).await?;
send_lobby_title(client, writer, &config.join.lobby.message).await?;
}
}
/// Waiting stage.
///
/// In this stage we wait for the server to come online.
///
/// During this stage we keep sending keep-alive and title packets to the client to keep it active.
async fn stage_wait(
client: &Client,
server: &Server,
config: &Config,
writer: &mut WriteHalf<'_>,
) -> Result<(), ()> {
select! {
a = keep_alive_loop(client, writer, config) => a,
b = wait_for_server(server, config) => b,
}
}
/// Wait for the server to come online.
///
/// Returns `Ok(())` once the server is online, returns `Err(())` if waiting failed.
async fn wait_for_server(server: &Server, config: &Config) -> Result<(), ()> {
debug!(target: "lazymc::lobby", "Waiting on server to come online...");
// A task to wait for suitable server state
// Waits for started state, errors if stopping/stopped state is reached
let task_wait = async {
let mut state = server.state_receiver();
loop {
// Wait for state change
state.changed().await.unwrap();
match state.borrow().deref() {
// Still waiting on server start
State::Starting => {
trace!(target: "lazymc::lobby", "Server not ready, holding client for longer");
continue;
}
// Server started, start relaying and proxy
State::Started => {
break true;
}
// Server stopping, this shouldn't happen, kick
State::Stopping | State::Stopped => {
break false;
}
}
}
};
// Wait for server state with timeout
let timeout = Duration::from_secs(config.join.lobby.timeout as u64);
match time::timeout(timeout, task_wait).await {
// Relay client to proxy
Ok(true) => {
debug!(target: "lazymc::lobby", "Server ready for lobby client");
return Ok(());
}
// Server stopping/stopped, this shouldn't happen, disconnect
Ok(false) => {}
// Timeout reached, disconnect
Err(_) => {
warn!(target: "lazymc::lobby", "Lobby client waiting for server to come online reached timeout of {}s", timeout.as_secs());
}
}
Err(())
}
/// Create connection to the server, with timeout.
///
/// This will initialize the connection to the play state. Client details are used.
async fn connect_to_server(
client_info: ClientInfo,
inbound: &TcpStream,
config: &Config,
) -> Result<(Client, TcpStream, BytesMut), ()> {
time::timeout(
SERVER_CONNECT_TIMEOUT,
connect_to_server_no_timeout(client_info, inbound, config),
)
.await
.map_err(|_| {
error!(target: "lazymc::lobby", "Creating new server connection for lobby client timed out after {}s", SERVER_CONNECT_TIMEOUT.as_secs());
})?
}
/// Create connection to the server, with no timeout.
///
/// This will initialize the connection to the play state. Client details are used.
// TODO: clean this up
async fn connect_to_server_no_timeout(
client_info: ClientInfo,
inbound: &TcpStream,
config: &Config,
) -> Result<(Client, TcpStream, BytesMut), ()> {
// Open connection
// TODO: on connect fail, ping server and redirect to serve_status if offline
let mut outbound = TcpStream::connect(config.server.address)
.await
.map_err(|_| ())?;
// Add proxy header
if config.server.send_proxy_v2 {
trace!(target: "lazymc::lobby", "Sending client proxy header for server connection");
outbound
.write_all(&proxy::stream_proxy_header(inbound).map_err(|_| ())?)
.await
.map_err(|_| ())?;
}
// Construct temporary server client
let tmp_client = match outbound.local_addr() {
Ok(addr) => Client::new(addr),
Err(_) => Client::dummy(),
};
tmp_client.set_state(ClientState::Login);
let (mut reader, mut writer) = outbound.split();
// Handshake packet
packet::write_packet(
Handshake {
protocol_version: client_info.protocol_version.unwrap(),
server_addr: config.server.address.ip().to_string(),
server_port: config.server.address.port(),
next_state: ClientState::Login.to_id(),
},
&tmp_client,
&mut writer,
)
.await?;
// Request login start
packet::write_packet(
LoginStart {
name: client_info.username.ok_or(())?,
},
&tmp_client,
&mut writer,
)
.await?;
// Incoming buffer
let mut buf = BytesMut::new();
loop {
// Read packet from stream
let (packet, _raw) = match packet::read_packet(&tmp_client, &mut buf, &mut reader).await {
Ok(Some(packet)) => packet,
Ok(None) => break,
Err(_) => {
error!(target: "lazymc::lobby", "Closing connection, error occurred");
break;
}
};
// Grab client state
let client_state = tmp_client.state();
// Catch set compression
if client_state == ClientState::Login && packet.id == packets::login::CLIENT_SET_COMPRESSION
{
// Decode compression packet
let set_compression =
SetCompression::decode(&mut packet.data.as_slice()).map_err(|err| {
dbg!(err);
})?;
// Client and server compression threshold should match, show warning if not
if set_compression.threshold != proto::COMPRESSION_THRESHOLD {
error!(
target: "lazymc::lobby",
"Compression threshold sent to lobby client does not match threshold from server, this may cause errors (client: {}, server: {})",
proto::COMPRESSION_THRESHOLD,
set_compression.threshold
);
}
// Set client compression
tmp_client.set_compression(set_compression.threshold);
continue;
}
// Hijack login success
if client_state == ClientState::Login && packet.id == packets::login::CLIENT_LOGIN_SUCCESS {
trace!(target: "lazymc::lobby", "Received login success from server connection, change to play mode");
// TODO: parse this packet to ensure it's fine
// let login_success =
// LoginSuccess::decode(&mut packet.data.as_slice()).map_err(|err| {
// dbg!(err);
// ()
// })?;
// Switch to play state
tmp_client.set_state(ClientState::Play);
// Server must enable compression if enabled for client, show warning otherwise
if tmp_client.is_compressed() != (proto::COMPRESSION_THRESHOLD >= 0) {
error!(target: "lazymc::lobby", "Compression enabled for lobby client while the server did not, this will cause errors");
}
return Ok((tmp_client, outbound, buf));
}
// Show unhandled packet warning
debug!(target: "lazymc::lobby", "Received unhandled packet from server in connect_to_server:");
debug!(target: "lazymc::lobby", "- State: {:?}", client_state);
debug!(target: "lazymc::lobby", "- Packet ID: 0x{:02X} ({})", packet.id, packet.id);
}
// Gracefully close connection
net::close_tcp_stream(outbound).await.map_err(|_| ())?;
Err(())
}
/// Wait for join game packet on server connection, with timeout.
///
/// This parses, consumes and returns the packet.
async fn wait_for_server_join_game(
client: &Client,
outbound: &mut TcpStream,
buf: &mut BytesMut,
) -> Result<JoinGame, ()> {
time::timeout(
SERVER_JOIN_GAME_TIMEOUT,
wait_for_server_join_game_no_timeout(client, outbound, buf),
)
.await
.map_err(|_| {
error!(target: "lazymc::lobby", "Waiting for for game data from server for lobby client timed out after {}s", SERVER_JOIN_GAME_TIMEOUT.as_secs());
})?
}
/// Wait for join game packet on server connection, with no timeout.
///
/// This parses, consumes and returns the packet.
// TODO: clean this up
// TODO: do not drop error here, return Box<dyn Error>
async fn wait_for_server_join_game_no_timeout(
client: &Client,
outbound: &mut TcpStream,
buf: &mut BytesMut,
) -> Result<JoinGame, ()> {
let (mut reader, mut _writer) = outbound.split();
loop {
// Read packet from stream
let (packet, _raw) = match packet::read_packet(client, buf, &mut reader).await {
Ok(Some(packet)) => packet,
Ok(None) => break,
Err(_) => {
error!(target: "lazymc::lobby", "Closing connection, error occurred");
break;
}
};
// Catch join game
if packet.id == packets::play::CLIENT_JOIN_GAME {
let join_game = JoinGame::decode(&mut packet.data.as_slice()).map_err(|err| {
dbg!(err);
})?;
return Ok(join_game);
}
// Show unhandled packet warning
debug!(target: "lazymc::lobby", "Received unhandled packet from server in wait_for_server_join_game:");
debug!(target: "lazymc::lobby", "- Packet ID: 0x{:02X} ({})", packet.id, packet.id);
}
// Gracefully close connection
net::close_tcp_stream_ref(outbound).await.map_err(|_| ())?;
Err(())
}
/// Route our lobby client through the proxy to the real server, spawning a new task.
///
/// `inbound_queue` is used for data already received from the server, that needs to be pushed to
/// the client.
#[inline]
pub fn route_proxy(inbound: TcpStream, outbound: TcpStream, inbound_queue: BytesMut) {
// When server is online, proxy all
let service = async move {
proxy::proxy_inbound_outbound_with_queue(inbound, outbound, &inbound_queue, &[])
.map(|r| {
if let Err(err) = r {
warn!(target: "lazymc", "Failed to proxy: {}", err);
}
})
.await
};
tokio::spawn(service);
}
/// Drain given reader until nothing is left voiding all data.
async fn drain_stream(reader: &mut ReadHalf<'_>) -> Result<(), ()> {
let mut drain_buf = [0; 8 * 1024];
loop {
match reader.try_read(&mut drain_buf) {
Ok(read) if read == 0 => return Ok(()),
Err(err) if err.kind() == ErrorKind::WouldBlock => return Ok(()),
Ok(_) => continue,
Err(err) => {
error!(target: "lazymc::lobby", "Failed to drain lobby client connection before relaying to real server. Maybe already disconnected? Error: {:?}", err);
return Ok(());
}
}
}
}
/// Read NBT CompoundTag from SNBT.
fn snbt_to_compound_tag(data: &str) -> CompoundTag {
use nbt::decode::read_compound_tag;
use quartz_nbt::io::{write_nbt, Flavor};
use quartz_nbt::snbt;
// Parse SNBT data
let compound = snbt::parse(data).expect("failed to parse SNBT");
// Encode to binary
let mut binary = Vec::new();
write_nbt(&mut binary, None, &compound, Flavor::Uncompressed)
.expect("failed to encode NBT CompoundTag as binary");
// Parse binary with usable NBT create
read_compound_tag(&mut &*binary).unwrap()
}

View File

@@ -10,8 +10,12 @@ extern crate log;
pub(crate) mod action;
pub(crate) mod cli;
pub(crate) mod config;
pub(crate) mod join;
#[cfg(feature = "lobby")]
pub(crate) mod lobby;
pub(crate) mod mc;
pub(crate) mod monitor;
pub(crate) mod net;
pub(crate) mod os;
pub(crate) mod proto;
pub(crate) mod proxy;

99
src/mc/ban.rs Normal file
View File

@@ -0,0 +1,99 @@
use std::collections::HashMap;
use std::error::Error;
use std::fs;
use std::net::IpAddr;
use std::path::Path;
use chrono::{DateTime, Utc};
use serde::Deserialize;
/// File name.
pub const FILE: &str = "banned-ips.json";
/// The forever expiry literal.
const EXPIRY_FOREVER: &str = "forever";
/// List of banned IPs.
#[derive(Debug, Default)]
pub struct BannedIps {
/// List of banned IPs.
ips: HashMap<IpAddr, BannedIp>,
}
impl BannedIps {
/// Get ban entry if IP if it exists.
///
/// This uses the latest known `banned-ips.json` contents if known.
/// If this feature is disabled, this will always return false.
pub fn get(&self, ip: &IpAddr) -> Option<BannedIp> {
self.ips.get(ip).cloned()
}
/// Check whether the given IP is banned.
///
/// This uses the latest known `banned-ips.json` contents if known.
/// If this feature is disabled, this will always return false.
pub fn is_banned(&self, ip: &IpAddr) -> bool {
self.ips.get(ip).map(|ip| ip.is_banned()).unwrap_or(false)
}
}
/// A banned IP entry.
#[derive(Debug, Deserialize, Clone)]
pub struct BannedIp {
/// Banned IP.
pub ip: IpAddr,
/// Ban creation time.
pub created: Option<String>,
/// Ban source.
pub source: Option<String>,
/// Ban expiry time.
pub expires: Option<String>,
/// Ban reason.
pub reason: Option<String>,
}
impl BannedIp {
/// Check if this entry is currently banned.
pub fn is_banned(&self) -> bool {
// Get expiry time
let expires = match &self.expires {
Some(expires) => expires,
None => return true,
};
// If expiry is forever, the user is banned
if expires.trim().to_lowercase() == EXPIRY_FOREVER {
return true;
}
// Parse expiry time, check if it has passed
let expiry = match DateTime::parse_from_str(expires, "%Y-%m-%d %H:%M:%S %z") {
Ok(expiry) => expiry,
Err(err) => {
error!(target: "lazymc", "Failed to parse ban expiry '{}', assuming still banned: {}", expires, err);
return true;
}
};
expiry > Utc::now()
}
}
/// Load banned IPs from file.
pub fn load(path: &Path) -> Result<BannedIps, Box<dyn Error>> {
// Load file contents
let contents = fs::read_to_string(path)?;
// Parse contents
let ips: Vec<BannedIp> = serde_json::from_str(&contents)?;
debug!(target: "lazymc", "Loaded {} banned IPs", ips.len());
// Transform into map
let ips = ips.into_iter().map(|ip| (ip.ip, ip)).collect();
Ok(BannedIps { ips })
}

View File

@@ -1,3 +1,8 @@
pub mod ban;
#[cfg(feature = "rcon")]
pub mod rcon;
pub mod server_properties;
/// Minecraft ticks per second.
#[allow(unused)]
pub const TICKS_PER_SECOND: u32 = 20;

View File

@@ -1,4 +1,19 @@
use std::time::Duration;
use async_std::net::TcpStream;
use async_std::prelude::*;
use rust_rcon::{Connection, Error as RconError};
use tokio::time;
use crate::config::Config;
use crate::proxy;
/// Minecraft RCON quirk.
///
/// Wait this time between RCON operations.
/// The Minecraft RCON implementation is very broken and brittle, this is used in the hopes to
/// improve reliability.
const QUIRK_RCON_GRACE_TIME: Duration = Duration::from_millis(200);
/// An RCON client.
pub struct Rcon {
@@ -7,19 +22,52 @@ pub struct Rcon {
impl Rcon {
/// Connect to a host.
pub async fn connect(addr: &str, pass: &str) -> Result<Self, Box<dyn std::error::Error>> {
pub async fn connect(
config: &Config,
addr: &str,
pass: &str,
) -> Result<Self, Box<dyn std::error::Error>> {
// Connect to our TCP stream
let mut stream = TcpStream::connect(addr).await?;
// Add proxy header
if config.rcon.send_proxy_v2 {
trace!(target: "lazymc::rcon", "Sending local proxy header for RCON connection");
stream.write_all(&proxy::local_proxy_header()?).await?;
}
// Start connection
let con = Connection::builder()
.enable_minecraft_quirks(true)
.connect(addr, pass)
.connect_stream(stream, pass)
.await?;
Ok(Self { con })
}
/// Connect to a host from the given configuration.
pub async fn connect_config(config: &Config) -> Result<Self, Box<dyn std::error::Error>> {
// RCON address
let mut addr = config.server.address;
addr.set_port(config.rcon.port);
let addr = addr.to_string();
Self::connect(config, &addr, &config.rcon.password).await
}
/// Send command over RCON.
pub async fn cmd(&mut self, cmd: &str) -> Result<String, RconError> {
// Minecraft quirk
time::sleep(QUIRK_RCON_GRACE_TIME).await;
// Actually send RCON command
debug!(target: "lazymc::rcon", "Sending RCON: {}", cmd);
self.con.cmd(cmd).await
}
/// Close connection.
pub async fn close(self) {
// Minecraft quirk
time::sleep(QUIRK_RCON_GRACE_TIME).await;
}
}

View File

@@ -1,5 +1,3 @@
// TODO: remove all unwraps/expects here!
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
@@ -7,23 +5,26 @@ use std::time::Duration;
use bytes::BytesMut;
use minecraft_protocol::data::server_status::ServerStatus;
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, StatusResponse};
use minecraft_protocol::version::v1_14_4::status::{
PingRequest, PingResponse, StatusRequest, StatusResponse,
};
use rand::Rng;
use tokio::io::AsyncWriteExt;
use tokio::net::TcpStream;
use tokio::time;
use crate::config::Config;
use crate::proto::{self, ClientState, RawPacket};
use crate::proto::client::{Client, ClientState};
use crate::proto::{packet, packets};
use crate::proxy;
use crate::server::{Server, State};
/// Monitor ping inverval in seconds.
const MONITOR_POLL_INTERVAL: Duration = Duration::from_secs(2);
/// Status request timeout in seconds.
const STATUS_TIMEOUT: u64 = 8;
const STATUS_TIMEOUT: u64 = 20;
/// Ping request timeout in seconds.
const PING_TIMEOUT: u64 = 10;
@@ -36,15 +37,17 @@ pub async fn monitor_server(config: Arc<Config>, server: Arc<Server>) {
let mut poll_interval = time::interval(MONITOR_POLL_INTERVAL);
loop {
poll_interval.tick().await;
// Poll server state and update internal status
trace!(target: "lazymc::monitor", "Fetching status for {} ... ", addr);
let status = poll_server(&config, &server, addr).await;
match status {
// Got status, update
Ok(Some(status)) => server.update_status(&config, Some(status)),
Ok(Some(status)) => server.update_status(&config, Some(status)).await,
// Error, reset status
Err(_) => server.update_status(&config, None),
Err(_) => server.update_status(&config, None).await,
// Didn't get status, but ping fallback worked, leave as-is, show warning
Ok(None) => {
@@ -53,22 +56,18 @@ pub async fn monitor_server(config: Arc<Config>, server: Arc<Server>) {
}
// Sleep server when it's bedtime
if server.should_sleep(&config) {
if server.should_sleep(&config).await {
info!(target: "lazymc::montior", "Server has been idle, sleeping...");
if !server.stop(&config).await {
warn!(target: "lazymc", "Failed to stop server");
}
server.stop(&config).await;
}
// Check whether we should force kill server
if server.should_kill() {
if server.should_kill().await {
error!(target: "lazymc::montior", "Force killing server, took too long to start or stop");
if !server.force_kill().await {
warn!(target: "lazymc", "Failed to force kill server");
}
}
poll_interval.tick().await;
}
}
@@ -88,6 +87,7 @@ pub async fn poll_server(
// Try ping fallback if server is currently started
if server.state() == State::Started {
debug!(target: "lazymc::monitor", "Failed to get status from started server, trying ping...");
do_ping(config, addr).await?;
}
@@ -98,84 +98,92 @@ pub async fn poll_server(
async fn fetch_status(config: &Config, addr: SocketAddr) -> Result<ServerStatus, ()> {
let mut stream = TcpStream::connect(addr).await.map_err(|_| ())?;
send_handshake(&mut stream, config, addr).await?;
request_status(&mut stream).await?;
wait_for_status_timeout(&mut stream).await
// Add proxy header
if config.server.send_proxy_v2 {
trace!(target: "lazymc::monitor", "Sending local proxy header for server connection");
stream
.write_all(&proxy::local_proxy_header().map_err(|_| ())?)
.await
.map_err(|_| ())?;
}
// Dummy client
let client = Client::dummy();
send_handshake(&client, &mut stream, config, addr).await?;
request_status(&client, &mut stream).await?;
wait_for_status_timeout(&client, &mut stream).await
}
/// Attemp to ping server.
async fn do_ping(config: &Config, addr: SocketAddr) -> Result<(), ()> {
let mut stream = TcpStream::connect(addr).await.map_err(|_| ())?;
send_handshake(&mut stream, config, addr).await?;
let token = send_ping(&mut stream).await?;
wait_for_ping_timeout(&mut stream, token).await
// Add proxy header
if config.server.send_proxy_v2 {
trace!(target: "lazymc::monitor", "Sending local proxy header for server connection");
stream
.write_all(&proxy::local_proxy_header().map_err(|_| ())?)
.await
.map_err(|_| ())?;
}
// Dummy client
let client = Client::dummy();
send_handshake(&client, &mut stream, config, addr).await?;
let token = send_ping(&client, &mut stream).await?;
wait_for_ping_timeout(&client, &mut stream, token).await
}
/// Send handshake.
async fn send_handshake(
client: &Client,
stream: &mut TcpStream,
config: &Config,
addr: SocketAddr,
) -> Result<(), ()> {
let handshake = Handshake {
protocol_version: config.public.protocol as i32,
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(proto::HANDSHAKE_PACKET_ID_HANDSHAKE, packet)
.encode()
.map_err(|_| ())?;
stream.write_all(&raw).await.map_err(|_| ())?;
Ok(())
packet::write_packet(
Handshake {
protocol_version: config.public.protocol as i32,
server_addr: addr.ip().to_string(),
server_port: addr.port(),
next_state: ClientState::Status.to_id(),
},
client,
&mut stream.split().1,
)
.await
}
/// Send status request.
async fn request_status(stream: &mut TcpStream) -> Result<(), ()> {
let raw = RawPacket::new(proto::STATUS_PACKET_ID_STATUS, vec![])
.encode()
.map_err(|_| ())?;
stream.write_all(&raw).await.map_err(|_| ())?;
Ok(())
async fn request_status(client: &Client, stream: &mut TcpStream) -> Result<(), ()> {
packet::write_packet(StatusRequest {}, client, &mut stream.split().1).await
}
/// Send status request.
async fn send_ping(stream: &mut TcpStream) -> Result<u64, ()> {
async fn send_ping(client: &Client, stream: &mut TcpStream) -> Result<u64, ()> {
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(proto::STATUS_PACKET_ID_PING, packet)
.encode()
.map_err(|_| ())?;
stream.write_all(&raw).await.map_err(|_| ())?;
packet::write_packet(PingRequest { time: token }, client, &mut stream.split().1).await?;
Ok(token)
}
/// Wait for a status response.
async fn wait_for_status(stream: &mut TcpStream) -> Result<ServerStatus, ()> {
async fn wait_for_status(client: &Client, stream: &mut TcpStream) -> Result<ServerStatus, ()> {
// 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 proto::read_packet(&mut buf, &mut reader).await {
let (packet, _raw) = match packet::read_packet(client, &mut buf, &mut reader).await {
Ok(Some(packet)) => packet,
Ok(None) => break,
Err(_) => continue,
};
// Catch status response
if packet.id == proto::STATUS_PACKET_ID_STATUS {
if packet.id == packets::status::CLIENT_STATUS {
let status = StatusResponse::decode(&mut packet.data.as_slice()).map_err(|_| ())?;
return Ok(status.server_status);
}
@@ -186,29 +194,32 @@ async fn wait_for_status(stream: &mut TcpStream) -> Result<ServerStatus, ()> {
}
/// Wait for a status response.
async fn wait_for_status_timeout(stream: &mut TcpStream) -> Result<ServerStatus, ()> {
let status = wait_for_status(stream);
async fn wait_for_status_timeout(
client: &Client,
stream: &mut TcpStream,
) -> Result<ServerStatus, ()> {
let status = wait_for_status(client, stream);
tokio::time::timeout(Duration::from_secs(STATUS_TIMEOUT), status)
.await
.map_err(|_| ())?
}
/// Wait for a status response.
async fn wait_for_ping(stream: &mut TcpStream, token: u64) -> Result<(), ()> {
async fn wait_for_ping(client: &Client, 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 proto::read_packet(&mut buf, &mut reader).await {
let (packet, _raw) = match packet::read_packet(client, &mut buf, &mut reader).await {
Ok(Some(packet)) => packet,
Ok(None) => break,
Err(_) => continue,
};
// Catch ping response
if packet.id == proto::STATUS_PACKET_ID_PING {
if packet.id == packets::status::CLIENT_PING {
let ping = PingResponse::decode(&mut packet.data.as_slice()).map_err(|_| ())?;
// Ping token must match
@@ -225,8 +236,12 @@ async fn wait_for_ping(stream: &mut TcpStream, token: u64) -> Result<(), ()> {
}
/// Wait for a status response.
async fn wait_for_ping_timeout(stream: &mut TcpStream, token: u64) -> Result<(), ()> {
let status = wait_for_ping(stream, token);
async fn wait_for_ping_timeout(
client: &Client,
stream: &mut TcpStream,
token: u64,
) -> Result<(), ()> {
let status = wait_for_ping(client, stream, token);
tokio::time::timeout(Duration::from_secs(PING_TIMEOUT), status)
.await
.map_err(|_| ())?

22
src/net.rs Normal file
View File

@@ -0,0 +1,22 @@
use std::error::Error;
use std::io;
use tokio::io::AsyncWriteExt;
use tokio::net::TcpStream;
/// Gracefully close given TCP stream.
///
/// Intended as helper to make code less messy. This also succeeds if already closed.
pub async fn close_tcp_stream(mut stream: TcpStream) -> Result<(), Box<dyn Error>> {
close_tcp_stream_ref(&mut stream).await
}
/// Gracefully close given TCP stream.
///
/// Intended as helper to make code less messy. This also succeeds if already closed.
pub async fn close_tcp_stream_ref(stream: &mut TcpStream) -> Result<(), Box<dyn Error>> {
match stream.shutdown().await {
Ok(_) => Ok(()),
Err(err) if err.kind() == io::ErrorKind::NotConnected => Ok(()),
Err(err) => Err(err.into()),
}
}

View File

@@ -20,7 +20,7 @@ pub unsafe fn kill_gracefully(pid: u32) -> bool {
let result = libc::kill(pid as i32, libc::SIGTERM);
if result != 0 {
trace!(target: "lazymc", "SIGTERM failed: {}", result);
warn!(target: "lazymc", "Sending SIGTERM signal to server failed: {}", result);
}
result == 0

View File

@@ -1,203 +0,0 @@
use std::sync::Mutex;
use bytes::BytesMut;
use tokio::io;
use tokio::io::AsyncReadExt;
use tokio::net::tcp::ReadHalf;
use crate::types;
/// Default minecraft protocol version name.
///
/// Send to clients when the server is sleeping when the real server version is not known yet.
pub const PROTO_DEFAULT_VERSION: &str = "1.17.1";
/// Default minecraft protocol version.
///
/// Send to clients when the server is sleeping when the real server version is not known yet, and
/// with server status polling requests.
pub const PROTO_DEFAULT_PROTOCOL: u32 = 756;
/// Handshake state, handshake packet ID.
pub const HANDSHAKE_PACKET_ID_HANDSHAKE: i32 = 0;
/// Status state, status packet ID.
pub const STATUS_PACKET_ID_STATUS: i32 = 0;
/// Status state, ping packet ID.
pub const STATUS_PACKET_ID_PING: i32 = 1;
/// Login state, login start packet ID.
pub const LOGIN_PACKET_ID_LOGIN_START: i32 = 0;
/// Client state.
///
/// Note: this does not keep track of compression/encryption states because packets are never
/// inspected when these modes are enabled.
#[derive(Debug, Default)]
pub struct Client {
/// Current client state.
pub state: Mutex<ClientState>,
}
impl Client {
/// Get client state.
pub fn state(&self) -> ClientState {
*self.state.lock().unwrap()
}
/// Set client state.
pub fn set_state(&self, state: ClientState) {
*self.state.lock().unwrap() = state;
}
}
/// Protocol state a client may be in.
///
/// Note: this does not include the `play` state, because this is never used anymore when a client
/// reaches this state.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum ClientState {
/// Initial client state.
Handshake,
/// State to query server status.
Status,
/// State to login to server.
Login,
}
impl ClientState {
/// From state ID.
pub fn from_id(id: i32) -> Option<Self> {
match id {
0 => Some(Self::Handshake),
1 => Some(Self::Status),
2 => Some(Self::Login),
_ => None,
}
}
/// Get state ID.
pub fn to_id(self) -> i32 {
match self {
Self::Handshake => 0,
Self::Status => 1,
Self::Login => 2,
}
}
}
impl Default for ClientState {
fn default() -> Self {
Self::Handshake
}
}
/// Raw Minecraft packet.
///
/// Having a packet ID and a raw data byte array.
pub struct RawPacket {
/// Packet ID.
pub id: i32,
/// Packet data.
pub data: Vec<u8>,
}
impl RawPacket {
/// Construct new raw packet.
pub fn new(id: i32, data: Vec<u8>) -> Self {
Self { id, data }
}
/// Decode packet from raw buffer.
pub fn decode(mut buf: &[u8]) -> Result<Self, ()> {
// Read length
let (read, len) = types::read_var_int(buf)?;
buf = &buf[read..][..len as usize];
// Read packet ID, select buf
let (read, packet_id) = types::read_var_int(buf)?;
buf = &buf[read..];
Ok(Self::new(packet_id, buf.to_vec()))
}
/// Encode packet to raw buffer.
pub fn encode(&self) -> Result<Vec<u8>, ()> {
let mut data = types::encode_var_int(self.id)?;
data.extend_from_slice(&self.data);
let len = data.len() as i32;
let mut packet = types::encode_var_int(len)?;
packet.append(&mut data);
Ok(packet)
}
}
/// Read raw packet from stream.
///
/// Note: this does not support reading compressed/encrypted packets.
/// We should never need this though, as we're done reading user packets before any of this is
/// enabled. See: https://wiki.vg/Protocol#Packet_format
pub async fn read_packet(
buf: &mut BytesMut,
stream: &mut ReadHalf<'_>,
) -> Result<Option<(RawPacket, Vec<u8>)>, ()> {
// Keep reading until we have at least 2 bytes
while buf.len() < 2 {
// Read packet from socket
let mut tmp = Vec::with_capacity(64);
match stream.read_buf(&mut tmp).await {
Ok(_) => {}
Err(err) if err.kind() == io::ErrorKind::ConnectionReset => return Ok(None),
Err(err) => {
dbg!(err);
return Err(());
}
}
if tmp.is_empty() {
return Ok(None);
}
buf.extend(tmp);
}
// Attempt to read packet length
let (consumed, len) = match types::read_var_int(buf) {
Ok(result) => result,
Err(err) => {
error!(target: "lazymc", "Malformed packet, could not read packet length");
return Err(err);
}
};
// Keep reading until we have all packet bytes
while buf.len() < consumed + len as usize {
// Read packet from socket
let mut tmp = Vec::with_capacity(64);
match stream.read_buf(&mut tmp).await {
Ok(_) => {}
Err(err) if err.kind() == io::ErrorKind::ConnectionReset => return Ok(None),
Err(err) => {
dbg!(err);
return Err(());
}
}
if tmp.is_empty() {
return Ok(None);
}
buf.extend(tmp);
}
// Parse packet
let raw = buf.split_to(consumed + len as usize);
let packet = RawPacket::decode(&raw)?;
Ok(Some((packet, raw.to_vec())))
}

36
src/proto/action.rs Normal file
View File

@@ -0,0 +1,36 @@
use minecraft_protocol::data::chat::{Message, Payload};
use minecraft_protocol::version::v1_14_4::game::GameDisconnect;
use minecraft_protocol::version::v1_14_4::login::LoginDisconnect;
use tokio::net::tcp::WriteHalf;
use crate::proto::client::{Client, ClientState};
use crate::proto::packet;
/// Kick client with a message.
///
/// Should close connection afterwards.
pub async fn kick(client: &Client, msg: &str, writer: &mut WriteHalf<'_>) -> Result<(), ()> {
match client.state() {
ClientState::Login => {
packet::write_packet(
LoginDisconnect {
reason: Message::new(Payload::text(msg)),
},
client,
writer,
)
.await
}
ClientState::Play => {
packet::write_packet(
GameDisconnect {
reason: Message::new(Payload::text(msg)),
},
client,
writer,
)
.await
}
_ => Err(()),
}
}

127
src/proto/client.rs Normal file
View File

@@ -0,0 +1,127 @@
use std::net::SocketAddr;
use std::sync::atomic::{AtomicI32, Ordering};
use std::sync::Mutex;
/// Client state.
///
/// Note: this does not keep track of encryption states.
#[derive(Debug)]
pub struct Client {
/// Client peer address.
pub peer: SocketAddr,
/// Current client state.
pub state: Mutex<ClientState>,
/// Compression state.
///
/// 0 or positive if enabled, negative if disabled.
pub compression: AtomicI32,
}
impl Client {
/// Construct new client with given peer address.
pub fn new(peer: SocketAddr) -> Self {
Self {
peer,
state: Default::default(),
compression: AtomicI32::new(-1),
}
}
/// Construct dummy client.
pub fn dummy() -> Self {
Self::new("0.0.0.0:0".parse().unwrap())
}
/// Get client state.
pub fn state(&self) -> ClientState {
*self.state.lock().unwrap()
}
/// Set client state.
pub fn set_state(&self, state: ClientState) {
*self.state.lock().unwrap() = state;
}
/// Get compression threshold.
pub fn compressed(&self) -> i32 {
self.compression.load(Ordering::Relaxed)
}
/// Whether compression is used.
pub fn is_compressed(&self) -> bool {
self.compressed() >= 0
}
/// Set compression value.
#[allow(unused)]
pub fn set_compression(&self, threshold: i32) {
trace!(target: "lazymc", "Client now uses compression threshold of {}", threshold);
self.compression.store(threshold, Ordering::Relaxed);
}
}
/// Protocol state a client may be in.
///
/// Note: this does not include the `play` state, because this is never used anymore when a client
/// reaches this state.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum ClientState {
/// Initial client state.
Handshake,
/// State to query server status.
Status,
/// State to login to server.
Login,
/// State to play on the server.
#[allow(unused)]
Play,
}
impl ClientState {
/// From state ID.
pub fn from_id(id: i32) -> Option<Self> {
match id {
0 => Some(Self::Handshake),
1 => Some(Self::Status),
2 => Some(Self::Login),
_ => None,
}
}
/// Get state ID.
pub fn to_id(self) -> i32 {
match self {
Self::Handshake => 0,
Self::Status => 1,
Self::Login => 2,
Self::Play => -1,
}
}
}
impl Default for ClientState {
fn default() -> Self {
Self::Handshake
}
}
/// Client info, useful during connection handling.
#[derive(Debug, Clone, Default)]
pub struct ClientInfo {
/// Client protocol version.
pub protocol_version: Option<i32>,
/// Client username.
pub username: Option<String>,
}
impl ClientInfo {
pub fn empty() -> Self {
Self::default()
}
}

27
src/proto/mod.rs Normal file
View File

@@ -0,0 +1,27 @@
pub mod action;
pub mod client;
pub mod packet;
pub mod packets;
/// Default minecraft protocol version name.
///
/// Just something to default to when real server version isn't known or when no hint is specified
/// in the configuration.
///
/// Should be kept up-to-date with latest supported Minecraft version by lazymc.
pub const PROTO_DEFAULT_VERSION: &str = "1.17.1";
/// Default minecraft protocol version.
///
/// Just something to default to when real server version isn't known or when no hint is specified
/// in the configuration.
///
/// Should be kept up-to-date with latest supported Minecraft version by lazymc.
pub const PROTO_DEFAULT_PROTOCOL: u32 = 756;
/// Compression threshold to use.
// TODO: read this from server.properties instead
pub const COMPRESSION_THRESHOLD: i32 = 256;
/// Default buffer size when reading packets.
pub(super) const BUF_SIZE: usize = 8 * 1024;

216
src/proto/packet.rs Normal file
View File

@@ -0,0 +1,216 @@
use std::io::prelude::*;
use bytes::BytesMut;
use flate2::read::ZlibDecoder;
use flate2::write::ZlibEncoder;
use flate2::Compression;
use minecraft_protocol::encoder::Encoder;
use minecraft_protocol::version::PacketId;
use tokio::io;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::tcp::{ReadHalf, WriteHalf};
use crate::proto::client::Client;
use crate::proto::BUF_SIZE;
use crate::types;
/// Raw Minecraft packet.
///
/// Having a packet ID and a raw data byte array.
pub struct RawPacket {
/// Packet ID.
pub id: u8,
/// Packet data.
pub data: Vec<u8>,
}
impl RawPacket {
/// Construct new raw packet.
pub fn new(id: u8, data: Vec<u8>) -> Self {
Self { id, data }
}
/// Read packet ID from buffer, use remaining buffer as data.
fn read_packet_id_data(mut buf: &[u8]) -> Result<Self, ()> {
// Read packet ID, select buf
let (read, packet_id) = types::read_var_int(buf)?;
buf = &buf[read..];
Ok(Self::new(packet_id as u8, buf.to_vec()))
}
/// Decode packet from raw buffer.
///
/// This decodes both compressed and uncompressed packets based on the client threshold
/// preference.
pub fn decode(client: &Client, mut buf: &[u8]) -> Result<Self, ()> {
// Read length
let (read, len) = types::read_var_int(buf)?;
buf = &buf[read..][..len as usize];
// If no compression is used, read remaining packet ID and data
if !client.is_compressed() {
// Read packet ID and data
return Self::read_packet_id_data(buf);
}
// Read data length
let (read, data_len) = types::read_var_int(buf)?;
buf = &buf[read..];
// If data length is zero, the rest is not compressed
if data_len == 0 {
return Self::read_packet_id_data(buf);
}
// Decompress packet ID and data section
let mut decompressed = Vec::with_capacity(data_len as usize);
ZlibDecoder::new(buf)
.read_to_end(&mut decompressed)
.map_err(|err| {
error!(target: "lazymc", "Packet decompression error: {}", err);
})?;
// Decompressed data must match length
if decompressed.len() != data_len as usize {
error!(target: "lazymc", "Decompressed packet has different length than expected ({}b != {}b)", decompressed.len(), data_len);
return Err(());
}
// Read decompressed packet ID
Self::read_packet_id_data(&decompressed)
}
/// Encode packet to raw buffer.
///
/// This compresses packets based on the client threshold preference.
pub fn encode(&self, client: &Client) -> Result<Vec<u8>, ()> {
let threshold = client.compressed();
if threshold >= 0 {
self.encode_compressed(threshold)
} else {
self.encode_uncompressed()
}
}
/// Encode compressed packet to raw buffer.
fn encode_compressed(&self, threshold: i32) -> Result<Vec<u8>, ()> {
// Packet payload: packet ID and data buffer
let mut payload = types::encode_var_int(self.id as i32)?;
payload.extend_from_slice(&self.data);
// Determine whether to compress, encode data length bytes
let data_len = payload.len() as i32;
let compress = data_len > threshold;
let mut data_len_bytes =
types::encode_var_int(if compress { data_len } else { 0 }).unwrap();
// Compress payload
if compress {
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
encoder.write_all(&payload).map_err(|err| {
error!(target: "lazymc", "Failed to compress packet: {}", err);
})?;
payload = encoder.finish().map_err(|err| {
error!(target: "lazymc", "Failed to compress packet: {}", err);
})?;
}
// Encapsulate payload with packet and data length
let len = data_len_bytes.len() as i32 + payload.len() as i32;
let mut packet = types::encode_var_int(len)?;
packet.append(&mut data_len_bytes);
packet.append(&mut payload);
Ok(packet)
}
/// Encode uncompressed packet to raw buffer.
fn encode_uncompressed(&self) -> Result<Vec<u8>, ()> {
let mut data = types::encode_var_int(self.id as i32)?;
data.extend_from_slice(&self.data);
let len = data.len() as i32;
let mut packet = types::encode_var_int(len)?;
packet.append(&mut data);
Ok(packet)
}
}
/// Read raw packet from stream.
pub async fn read_packet(
client: &Client,
buf: &mut BytesMut,
stream: &mut ReadHalf<'_>,
) -> Result<Option<(RawPacket, Vec<u8>)>, ()> {
// Keep reading until we have at least 2 bytes
while buf.len() < 2 {
// Read packet from socket
let mut tmp = Vec::with_capacity(BUF_SIZE);
match stream.read_buf(&mut tmp).await {
Ok(_) => {}
Err(err) if err.kind() == io::ErrorKind::ConnectionReset => return Ok(None),
Err(err) => {
dbg!(err);
return Err(());
}
}
if tmp.is_empty() {
return Ok(None);
}
buf.extend(tmp);
}
// Attempt to read packet length
let (consumed, len) = match types::read_var_int(buf) {
Ok(result) => result,
Err(err) => {
error!(target: "lazymc", "Malformed packet, could not read packet length");
return Err(err);
}
};
// Keep reading until we have all packet bytes
while buf.len() < consumed + len as usize {
// Read packet from socket
let mut tmp = Vec::with_capacity(BUF_SIZE);
match stream.read_buf(&mut tmp).await {
Ok(_) => {}
Err(err) if err.kind() == io::ErrorKind::ConnectionReset => return Ok(None),
Err(err) => {
dbg!(err);
return Err(());
}
}
if tmp.is_empty() {
return Ok(None);
}
buf.extend(tmp);
}
// Parse packet, use full buffer since we'll read the packet length again
let raw = buf.split_to(consumed + len as usize);
let packet = RawPacket::decode(client, &raw)?;
Ok(Some((packet, raw.to_vec())))
}
/// Write packet to stream writer.
pub async fn write_packet(
packet: impl PacketId + Encoder,
client: &Client,
writer: &mut WriteHalf<'_>,
) -> Result<(), ()> {
let mut data = Vec::new();
packet.encode(&mut data).map_err(|_| ())?;
let response = RawPacket::new(packet.packet_id(), data).encode(client)?;
writer.write_all(&response).await.map_err(|_| ())?;
Ok(())
}

41
src/proto/packets.rs Normal file
View File

@@ -0,0 +1,41 @@
//! Minecraft protocol packet IDs.
#![allow(unused)]
pub mod handshake {
pub const SERVER_HANDSHAKE: u8 = 0x00;
}
pub mod status {
pub const CLIENT_STATUS: u8 = 0x0;
pub const CLIENT_PING: u8 = 0x01;
pub const SERVER_STATUS: u8 = 0x00;
pub const SERVER_PING: u8 = 0x01;
}
pub mod login {
pub const CLIENT_DISCONNECT: u8 = 0x00;
pub const CLIENT_LOGIN_SUCCESS: u8 = 0x02;
pub const CLIENT_SET_COMPRESSION: u8 = 0x03;
pub const SERVER_LOGIN_START: u8 = 0x00;
}
pub mod play {
pub const CLIENT_CHAT_MSG: u8 = 0x0F;
pub const CLIENT_PLUGIN_MESSAGE: u8 = 0x18;
pub const CLIENT_NAMED_SOUND_EFFECT: u8 = 0x19;
pub const CLIENT_DISCONNECT: u8 = 0x1A;
pub const CLIENT_KEEP_ALIVE: u8 = 0x21;
pub const CLIENT_JOIN_GAME: u8 = 0x26;
pub const CLIENT_PLAYER_POS_LOOK: u8 = 0x38;
pub const CLIENT_RESPAWN: u8 = 0x3D;
pub const CLIENT_SPAWN_POS: u8 = 0x4B;
pub const CLIENT_SET_TITLE_SUBTITLE: u8 = 0x57;
pub const CLIENT_TIME_UPDATE: u8 = 0x58;
pub const CLIENT_SET_TITLE_TEXT: u8 = 0x59;
pub const CLIENT_SET_TITLE_TIMES: u8 = 0x5A;
pub const SERVER_CLIENT_SETTINGS: u8 = 0x05;
pub const SERVER_PLUGIN_MESSAGE: u8 = 0x0A;
pub const SERVER_PLAYER_POS: u8 = 0x11;
pub const SERVER_PLAYER_POS_ROT: u8 = 0x12;
}

View File

@@ -1,20 +1,30 @@
use std::error::Error;
use std::net::SocketAddr;
use bytes::BytesMut;
use proxy_protocol::version2::{ProxyAddresses, ProxyCommand, ProxyTransportProtocol};
use proxy_protocol::EncodeError;
use tokio::io;
use tokio::io::AsyncWriteExt;
use tokio::net::TcpStream;
use crate::net;
/// Proxy the inbound stream to a target address.
pub async fn proxy(inbound: TcpStream, addr_target: SocketAddr) -> Result<(), Box<dyn Error>> {
proxy_with_queue(inbound, addr_target, &[]).await
pub async fn proxy(
inbound: TcpStream,
proxy_header: ProxyHeader,
addr_target: SocketAddr,
) -> Result<(), Box<dyn Error>> {
proxy_with_queue(inbound, proxy_header, addr_target, &[]).await
}
/// Proxy the inbound stream to a target address.
///
/// Send the queue to the target server before proxying.
pub async fn proxy_with_queue(
mut inbound: TcpStream,
inbound: TcpStream,
proxy_header: ProxyHeader,
addr_target: SocketAddr,
queue: &[u8],
) -> Result<(), Box<dyn Error>> {
@@ -22,14 +32,48 @@ pub async fn proxy_with_queue(
// TODO: on connect fail, ping server and redirect to serve_status if offline
let mut outbound = TcpStream::connect(addr_target).await?;
// Add proxy header
match proxy_header {
ProxyHeader::None => {}
ProxyHeader::Local => {
let header = local_proxy_header()?;
outbound.write_all(&header).await?;
}
ProxyHeader::Proxy => {
let header = stream_proxy_header(&inbound)?;
outbound.write_all(&header).await?;
}
}
// Start proxy on both streams
proxy_inbound_outbound_with_queue(inbound, outbound, &[], queue).await
}
/// Proxy the inbound stream to a target address.
///
/// Send the queue to the target server before proxying.
// TODO: find better name for this
pub async fn proxy_inbound_outbound_with_queue(
mut inbound: TcpStream,
mut outbound: TcpStream,
inbound_queue: &[u8],
outbound_queue: &[u8],
) -> Result<(), Box<dyn Error>> {
let (mut ri, mut wi) = inbound.split();
let (mut ro, mut wo) = outbound.split();
// Forward queued bytes to client once writable
if !inbound_queue.is_empty() {
wi.writable().await?;
trace!(target: "lazymc", "Relaying {} queued bytes to client", inbound_queue.len());
wi.write_all(inbound_queue).await?;
}
// Forward queued bytes to server once writable
if !queue.is_empty() {
if !outbound_queue.is_empty() {
wo.writable().await?;
trace!(target: "lazymc", "Relaying {} queued bytes to server", queue.len());
wo.write_all(queue).await?;
trace!(target: "lazymc", "Relaying {} queued bytes to server", outbound_queue.len());
wo.write_all(outbound_queue).await?;
}
let client_to_server = async {
@@ -43,5 +87,80 @@ pub async fn proxy_with_queue(
tokio::try_join!(client_to_server, server_to_client)?;
// Gracefully close connection if not done already
net::close_tcp_stream(inbound).await?;
Ok(())
}
/// Proxy header.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum ProxyHeader {
/// Do not add proxy header.
None,
/// Header for locally initiated connection.
Local,
/// Header for proxied connection.
Proxy,
}
impl ProxyHeader {
/// Changes to `None` if `false` if given.
///
/// `None` stays `None`.
pub fn not_none(self, not_none: bool) -> Self {
if not_none {
self
} else {
Self::None
}
}
}
/// Get the proxy header for a locally initiated connection.
///
/// This header may be sent over the outbound stream to signal client information.
pub fn local_proxy_header() -> Result<BytesMut, EncodeError> {
// Build proxy header
let header = proxy_protocol::ProxyHeader::Version2 {
command: ProxyCommand::Local,
transport_protocol: ProxyTransportProtocol::Stream,
addresses: ProxyAddresses::Unspec,
};
proxy_protocol::encode(header)
}
/// Get the proxy header for the given inbound stream.
///
/// This header may be sent over the outbound stream to signal client information.
pub fn stream_proxy_header(inbound: &TcpStream) -> Result<BytesMut, EncodeError> {
// Get peer and local address
let peer = inbound
.peer_addr()
.expect("Peer address not known for TCP stream");
let local = inbound
.local_addr()
.expect("Local address not known for TCP stream");
// Build proxy header
let header = proxy_protocol::ProxyHeader::Version2 {
command: ProxyCommand::Proxy,
transport_protocol: ProxyTransportProtocol::Stream,
addresses: match (peer, local) {
(SocketAddr::V4(source), SocketAddr::V4(destination)) => ProxyAddresses::Ipv4 {
source,
destination,
},
(SocketAddr::V6(source), SocketAddr::V6(destination)) => ProxyAddresses::Ipv6 {
source,
destination,
},
(_, _) => unreachable!(),
},
};
proxy_protocol::encode(header)
}

View File

@@ -1,14 +1,371 @@
use std::net::IpAddr;
use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::{Arc, Mutex, RwLock, RwLockReadGuard};
use std::sync::Arc;
use std::time::{Duration, Instant};
use futures::FutureExt;
use minecraft_protocol::data::server_status::ServerStatus;
use tokio::process::Command;
use tokio::sync::watch;
#[cfg(feature = "rcon")]
use tokio::sync::Semaphore;
use tokio::sync::{Mutex, RwLock, RwLockReadGuard};
use tokio::time;
use crate::config::Config;
use crate::mc::ban::{BannedIp, BannedIps};
use crate::os;
/// Server cooldown after the process quit.
/// Used to give it some more time to quit forgotten threads, such as for RCON.
const SERVER_QUIT_COOLDOWN: Duration = Duration::from_millis(2500);
/// RCON cooldown. Required period between RCON invocations.
///
/// The Minecraft RCON implementation is very broken and brittle, this is used in the hopes to
/// improve reliability.
#[cfg(feature = "rcon")]
const RCON_COOLDOWN: Duration = Duration::from_secs(15);
/// Shared server state.
#[derive(Debug)]
pub struct Server {
/// Server state.
///
/// Matches `State`, utilzes AtomicU8 for better performance.
state: AtomicU8,
/// State watch sender, broadcast state changes.
state_watch_sender: watch::Sender<State>,
/// State watch receiver, subscribe to state changes.
state_watch_receiver: watch::Receiver<State>,
/// Server process PID.
///
/// Set if a server process is running.
pid: Mutex<Option<u32>>,
/// Last known server status.
///
/// Will remain set once known, not cleared if server goes offline.
status: RwLock<Option<ServerStatus>>,
/// Last active time.
///
/// The last time there was activity on the server. Also set at the moment the server comes
/// online.
last_active: RwLock<Option<Instant>>,
/// Force server to stay online until.
keep_online_until: RwLock<Option<Instant>>,
/// Time to force kill the server process at.
///
/// Used as starting/stopping timeout.
kill_at: RwLock<Option<Instant>>,
/// List of banned IPs.
banned_ips: RwLock<BannedIps>,
/// Lock for exclusive RCON operations.
#[cfg(feature = "rcon")]
rcon_lock: Semaphore,
/// Last time server was stopped over RCON.
#[cfg(feature = "rcon")]
rcon_last_stop: Mutex<Option<Instant>>,
}
impl Server {
/// Get current state.
pub fn state(&self) -> State {
State::from_u8(self.state.load(Ordering::Relaxed))
}
/// Get state receiver to subscribe on server state changes.
pub fn state_receiver(&self) -> watch::Receiver<State> {
self.state_watch_receiver.clone()
}
/// Set a new state.
///
/// This updates various other internal things depending on how the state changes.
///
/// Returns false if the state didn't change, in which case nothing happens.
async fn update_state(&self, state: State, config: &Config) -> bool {
self.update_state_from(None, state, config).await
}
/// Set new state, from a current state.
///
/// This updates various other internal things depending on how the state changes.
///
/// Returns false if current state didn't match `from` or if nothing changed.
async fn update_state_from(&self, from: Option<State>, new: State, config: &Config) -> bool {
// Atomically swap state to new, return if from doesn't match
let old = State::from_u8(match from {
Some(from) => match self.state.compare_exchange(
from.to_u8(),
new.to_u8(),
Ordering::Relaxed,
Ordering::Relaxed,
) {
Ok(old) => old,
Err(_) => return false,
},
None => self.state.swap(new.to_u8(), Ordering::Relaxed),
});
// State must be changed
if old == new {
return false;
}
trace!("Change server state from {:?} to {:?}", old, new);
// Broadcast change
let _ = self.state_watch_sender.send(new);
// Update kill at time for starting/stopping state
*self.kill_at.write().await = match new {
State::Starting if config.server.start_timeout > 0 => {
Some(Instant::now() + Duration::from_secs(config.server.start_timeout as u64))
}
State::Stopping if config.server.stop_timeout > 0 => {
Some(Instant::now() + Duration::from_secs(config.server.stop_timeout as u64))
}
_ => None,
};
// Online/offline messages
match new {
State::Started => info!(target: "lazymc::monitor", "Server is now online"),
State::Stopped => info!(target: "lazymc::monitor", "Server is now sleeping"),
_ => {}
}
// If Starting -> Started, update active time and keep it online for configured time
if old == State::Starting && new == State::Started {
self.update_last_active().await;
self.keep_online_for(Some(config.time.min_online_time))
.await;
}
true
}
/// Update status as obtained from the server.
///
/// This updates various other internal things depending on the current state and the given
/// status.
pub async fn update_status(&self, config: &Config, status: Option<ServerStatus>) {
// Update state based on curren
match (self.state(), &status) {
(State::Stopped | State::Starting, Some(_)) => {
self.update_state(State::Started, config).await;
}
(State::Started, None) => {
self.update_state(State::Stopped, config).await;
}
_ => {}
}
// Update last status if known
if let Some(status) = status {
// Update last active time if there are online players
if status.players.online > 0 {
self.update_last_active().await;
}
self.status.write().await.replace(status);
}
}
/// Try to start the server.
///
/// Does nothing if currently not in stopped state.
pub async fn start(config: Arc<Config>, server: Arc<Server>, username: Option<String>) -> bool {
// Must set state from stopped to starting
if !server
.update_state_from(Some(State::Stopped), State::Starting, &config)
.await
{
return false;
}
// Log starting message
match username {
Some(username) => info!(target: "lazymc", "Starting server for '{}'...", username),
None => info!(target: "lazymc", "Starting server..."),
}
// Spawn server in new task
Self::spawn_server_task(config, server);
true
}
/// Spawn the server task.
///
/// This should not be called directly.
fn spawn_server_task(config: Arc<Config>, server: Arc<Server>) {
tokio::spawn(invoke_server_cmd(config, server).map(|_| ()));
}
/// Stop running server.
///
/// This will attempt to stop the server with all available methods.
#[allow(unused_variables)]
pub async fn stop(&self, config: &Config) -> bool {
// Try to stop through RCON if started
#[cfg(feature = "rcon")]
if self.state() == State::Started && stop_server_rcon(config, self).await {
return true;
}
// Try to stop through signal
#[cfg(unix)]
if stop_server_signal(config, self).await {
return true;
}
warn!(target: "lazymc", "Failed to stop server, no more suitable stopping method to use");
false
}
/// Force kill running server.
///
/// This requires the server PID to be known.
pub async fn force_kill(&self) -> bool {
if let Some(pid) = *self.pid.lock().await {
return os::force_kill(pid);
}
false
}
/// Decide whether the server should sleep.
///
/// Always returns false if it is currently not online.
pub async fn should_sleep(&self, config: &Config) -> bool {
// Server must be online
if self.state() != State::Started {
return false;
}
// Never sleep if players are online
let players_online = self
.status
.read()
.await
.as_ref()
.map(|status| status.players.online > 0)
.unwrap_or(false);
if players_online {
trace!(target: "lazymc", "Not sleeping because players are online");
return false;
}
// Don't sleep when keep online until isn't expired
let keep_online = self
.keep_online_until
.read()
.await
.map(|i| i >= Instant::now())
.unwrap_or(false);
if keep_online {
trace!(target: "lazymc", "Not sleeping because of keep online");
return false;
}
// Last active time must have passed sleep threshold
if let Some(last_idle) = self.last_active.read().await.as_ref() {
return last_idle.elapsed() >= Duration::from_secs(config.time.sleep_after as u64);
}
false
}
/// Decide whether to force kill the server process.
pub async fn should_kill(&self) -> bool {
self.kill_at
.read()
.await
.map(|t| t <= Instant::now())
.unwrap_or(false)
}
/// Read last known server status.
pub async fn status(&self) -> RwLockReadGuard<'_, Option<ServerStatus>> {
self.status.read().await
}
/// Update the last active time.
async fn update_last_active(&self) {
self.last_active.write().await.replace(Instant::now());
}
/// Force the server to be online for the given number of seconds.
async fn keep_online_for(&self, duration: Option<u32>) {
*self.keep_online_until.write().await = duration
.filter(|d| *d > 0)
.map(|d| Instant::now() + Duration::from_secs(d as u64));
}
/// Check whether the given IP is banned.
///
/// This uses the latest known `banned-ips.json` contents if known.
/// If this feature is disabled, this will always return false.
pub async fn is_banned_ip(&self, ip: &IpAddr) -> bool {
self.banned_ips.read().await.is_banned(ip)
}
/// Get user ban entry.
pub async fn ban_entry(&self, ip: &IpAddr) -> Option<BannedIp> {
self.banned_ips.read().await.get(ip)
}
/// Check whether the given IP is banned.
///
/// This uses the latest known `banned-ips.json` contents if known.
/// If this feature is disabled, this will always return false.
pub fn is_banned_ip_blocking(&self, ip: &IpAddr) -> bool {
futures::executor::block_on(async { self.is_banned_ip(ip).await })
}
/// Update the list of banned IPs.
pub async fn set_banned_ips(&self, ips: BannedIps) {
*self.banned_ips.write().await = ips;
}
/// Update the list of banned IPs.
pub fn set_banned_ips_blocking(&self, ips: BannedIps) {
futures::executor::block_on(async { self.set_banned_ips(ips).await })
}
}
impl Default for Server {
fn default() -> Self {
let (state_watch_sender, state_watch_receiver) = watch::channel(State::Stopped);
Self {
state: AtomicU8::new(State::Stopped.to_u8()),
state_watch_sender,
state_watch_receiver,
pid: Default::default(),
status: Default::default(),
last_active: Default::default(),
keep_online_until: Default::default(),
kill_at: Default::default(),
banned_ips: Default::default(),
#[cfg(feature = "rcon")]
rcon_lock: Semaphore::new(1),
#[cfg(feature = "rcon")]
rcon_last_stop: Default::default(),
}
}
}
/// Server state.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum State {
@@ -48,279 +405,12 @@ impl State {
}
}
/// Shared server state.
#[derive(Debug)]
pub struct Server {
/// Server state.
///
/// Matches `State`, utilzes AtomicU8 for better performance.
state: AtomicU8,
/// Server process PID.
///
/// Set if a server process is running.
pid: Mutex<Option<u32>>,
/// Last known server status.
///
/// Will remain set once known, not cleared if server goes offline.
status: RwLock<Option<ServerStatus>>,
/// Last active time.
///
/// The last time there was activity on the server. Also set at the moment the server comes
/// online.
last_active: RwLock<Option<Instant>>,
/// Force server to stay online until.
keep_online_until: RwLock<Option<Instant>>,
/// Time to force kill the server process at.
///
/// Used as starting/stopping timeout.
kill_at: RwLock<Option<Instant>>,
}
impl Server {
/// Get current state.
pub fn state(&self) -> State {
State::from_u8(self.state.load(Ordering::Relaxed))
}
/// Set a new state.
///
/// This updates various other internal things depending on how the state changes.
///
/// Returns false if the state didn't change, in which case nothing happens.
fn update_state(&self, state: State, config: &Config) -> bool {
self.update_state_from(None, state, config)
}
/// Set new state, from a current state.
///
/// This updates various other internal things depending on how the state changes.
///
/// Returns false if current state didn't match `from` or if nothing changed.
fn update_state_from(&self, from: Option<State>, new: State, config: &Config) -> bool {
// Atomically swap state to new, return if from doesn't match
let old = State::from_u8(match from {
Some(from) => match self.state.compare_exchange(
from.to_u8(),
new.to_u8(),
Ordering::Relaxed,
Ordering::Relaxed,
) {
Ok(old) => old,
Err(_) => return false,
},
None => self.state.swap(new.to_u8(), Ordering::Relaxed),
});
// State must be changed
if old == new {
return false;
}
trace!("Change server state from {:?} to {:?}", old, new);
// Update kill at time for starting/stopping state
*self.kill_at.write().unwrap() = match new {
State::Starting if config.time.start_timeout > 0 => {
Some(Instant::now() + Duration::from_secs(config.time.start_timeout as u64))
}
State::Stopping if config.time.stop_timeout > 0 => {
Some(Instant::now() + Duration::from_secs(config.time.stop_timeout as u64))
}
_ => None,
};
// Online/offline messages
match new {
State::Started => info!(target: "lazymc::monitor", "Server is now online"),
State::Stopped => info!(target: "lazymc::monitor", "Server is now sleeping"),
_ => {}
}
// If Starting -> Started, update active time and keep it online for configured time
if old == State::Starting && new == State::Started {
self.update_last_active();
self.keep_online_for(Some(config.time.min_online_time));
}
true
}
/// Update status as polled from the server.
///
/// This updates various other internal things depending on the current state and the given
/// status.
pub fn update_status(&self, config: &Config, status: Option<ServerStatus>) {
// Update state based on curren
match (self.state(), &status) {
(State::Stopped | State::Starting, Some(_)) => {
self.update_state(State::Started, config);
}
(State::Started, None) => {
self.update_state(State::Stopped, config);
}
_ => {}
}
// Update last status if known
if let Some(status) = status {
// Update last active time if there are online players
if status.players.online > 0 {
self.update_last_active();
}
self.status.write().unwrap().replace(status);
}
}
/// Try to start the server.
///
/// Does nothing if currently not in stopped state.
pub fn start(config: Arc<Config>, server: Arc<Server>, username: Option<String>) -> bool {
// Must set state from stopped to starting
if !server.update_state_from(Some(State::Stopped), State::Starting, &config) {
return false;
}
// Log starting message
match username {
Some(username) => info!(target: "lazymc", "Starting server for '{}'...", username),
None => info!(target: "lazymc", "Starting server..."),
}
// Invoke server command in separate task
tokio::spawn(invoke_server_cmd(config, server).map(|_| ()));
true
}
/// Stop running server.
///
/// This requires the server PID to be known.
#[allow(unused_variables)]
pub async fn stop(&self, config: &Config) -> bool {
// We must have a running process
let has_process = self.pid.lock().unwrap().is_some();
if !has_process {
return false;
}
// Try to stop through RCON if started
#[cfg(feature = "rcon")]
if self.state() == State::Started && stop_server_rcon(config, self).await {
return true;
}
// Try to stop through signal
#[cfg(unix)]
if stop_server_signal(config, self) {
return true;
}
false
}
/// Force kill running server.
///
/// This requires the server PID to be known.
pub async fn force_kill(&self) -> bool {
if let Some(pid) = *self.pid.lock().unwrap() {
return os::force_kill(pid);
}
false
}
/// Decide whether the server should sleep.
///
/// Always returns false if it is currently not online.
pub fn should_sleep(&self, config: &Config) -> bool {
// Server must be online
if self.state() != State::Started {
return false;
}
// Never sleep if players are online
let players_online = self
.status
.read()
.unwrap()
.as_ref()
.map(|status| status.players.online > 0)
.unwrap_or(false);
if players_online {
trace!(target: "lazymc", "Not sleeping because players are online");
return false;
}
// Don't sleep when keep online until isn't expired
let keep_online = self
.keep_online_until
.read()
.unwrap()
.map(|i| i >= Instant::now())
.unwrap_or(false);
if keep_online {
trace!(target: "lazymc", "Not sleeping because of keep online");
return false;
}
// Last active time must have passed sleep threshold
if let Some(last_idle) = self.last_active.read().unwrap().as_ref() {
return last_idle.elapsed() >= Duration::from_secs(config.time.sleep_after as u64);
}
false
}
/// Decide whether to force kill the server process.
pub fn should_kill(&self) -> bool {
self.kill_at
.read()
.unwrap()
.map(|t| t <= Instant::now())
.unwrap_or(false)
}
/// Read last known server status.
pub fn status(&self) -> RwLockReadGuard<Option<ServerStatus>> {
self.status.read().unwrap()
}
/// Update the last active time.
fn update_last_active(&self) {
self.last_active.write().unwrap().replace(Instant::now());
}
/// Force the server to be online for the given number of seconds.
fn keep_online_for(&self, duration: Option<u32>) {
*self.keep_online_until.write().unwrap() = duration
.filter(|d| *d > 0)
.map(|d| Instant::now() + Duration::from_secs(d as u64));
}
}
impl Default for Server {
fn default() -> Self {
Self {
state: AtomicU8::new(State::Stopped.to_u8()),
pid: Default::default(),
status: Default::default(),
last_active: Default::default(),
keep_online_until: Default::default(),
kill_at: Default::default(),
}
}
}
/// Invoke server command, store PID and wait for it to quit.
pub async fn invoke_server_cmd(
config: Arc<Config>,
state: Arc<Server>,
) -> Result<(), Box<dyn std::error::Error>> {
// Build command
// Configure command
let args = shlex::split(&config.server.command).expect("invalid server command");
let mut cmd = Command::new(&args[0]);
cmd.args(args.iter().skip(1));
@@ -344,7 +434,7 @@ pub async fn invoke_server_cmd(
state
.pid
.lock()
.unwrap()
.await
.replace(child.id().expect("unknown server PID"));
// Wait for process to exit, handle status
@@ -364,14 +454,19 @@ pub async fn invoke_server_cmd(
}
};
// Set state to stopped, update server PID
state.pid.lock().unwrap().take();
state.update_state(State::Stopped, &config);
// Forget server PID
state.pid.lock().await.take();
// Give server a little more time to quit forgotten threads
time::sleep(SERVER_QUIT_COOLDOWN).await;
// Set server state to stopped
state.update_state(State::Stopped, &config).await;
// Restart on crash
if crashed && config.server.wake_on_crash {
warn!(target: "lazymc", "Server crashed, restarting...");
Server::start(config, state, None);
Server::start(config, state, None).await;
}
Ok(())
@@ -384,16 +479,27 @@ async fn stop_server_rcon(config: &Config, server: &Server) -> bool {
// RCON must be enabled
if !config.rcon.enabled {
trace!(target: "lazymc", "Not using RCON to stop server, disabled in config");
return false;
}
// RCON address
let mut addr = config.server.address;
addr.set_port(config.rcon.port);
let addr = addr.to_string();
// Grab RCON lock
let rcon_lock = server.rcon_lock.acquire().await.unwrap();
// Ensure RCON has cooled down
let rcon_cooled_down = server
.rcon_last_stop
.lock()
.await
.map(|t| t.elapsed() >= RCON_COOLDOWN)
.unwrap_or(true);
if !rcon_cooled_down {
debug!(target: "lazymc", "Not using RCON to stop server, in cooldown, used too recently");
return false;
}
// Create RCON client
let mut rcon = match Rcon::connect(&addr, &config.rcon.password).await {
let mut rcon = match Rcon::connect_config(&config).await {
Ok(rcon) => rcon,
Err(err) => {
error!(target: "lazymc", "Failed to RCON server to sleep: {}", err);
@@ -404,11 +510,17 @@ async fn stop_server_rcon(config: &Config, server: &Server) -> bool {
// Invoke stop
if let Err(err) = rcon.cmd("stop").await {
error!(target: "lazymc", "Failed to invoke stop through RCON: {}", err);
return false;
}
// Set server to stopping state
// TODO: set before stop command, revert state on failure
server.update_state(State::Stopping, config);
// Set server to stopping state, update last RCON time
server.rcon_last_stop.lock().await.replace(Instant::now());
server.update_state(State::Stopping, config).await;
// Gracefully close connection
rcon.close().await;
drop(rcon_lock);
true
}
@@ -417,17 +529,29 @@ async fn stop_server_rcon(config: &Config, server: &Server) -> bool {
///
/// Only available on Unix.
#[cfg(unix)]
fn stop_server_signal(config: &Config, server: &Server) -> bool {
async fn stop_server_signal(config: &Config, server: &Server) -> bool {
// Grab PID
let pid = match *server.pid.lock().unwrap() {
let pid = match *server.pid.lock().await {
Some(pid) => pid,
None => return false,
None => {
debug!(target: "lazymc", "Could not send stop signal to server process, PID unknown");
return false;
}
};
// Set stopping state, send kill signal
// TODO: revert state on failure
server.update_state(State::Stopping, config);
crate::os::kill_gracefully(pid);
// Send kill signal
if !crate::os::kill_gracefully(pid) {
error!(target: "lazymc", "Failed to send stop signal to server process");
return false;
}
// Update from starting/started to stopping
server
.update_state_from(Some(State::Starting), State::Stopping, config)
.await;
server
.update_state_from(Some(State::Started), State::Stopping, config)
.await;
true
}

119
src/service/ban_reload.rs Normal file
View File

@@ -0,0 +1,119 @@
use std::path::Path;
use std::sync::mpsc::channel;
use std::sync::Arc;
use std::thread;
use std::time::Duration;
use notify::{watcher, DebouncedEvent, RecursiveMode, Watcher};
use crate::config::Config;
use crate::mc::ban;
use crate::server::Server;
/// File debounce time.
const WATCH_DEBOUNCE: Duration = Duration::from_secs(2);
/// Service to reload banned IPs when its file changes.
pub fn service(config: Arc<Config>, server: Arc<Server>) {
// TODO: check what happens when file doesn't exist at first?
// Ensure we need to reload banned IPs
if !config.server.block_banned_ips && !config.server.drop_banned_ips {
return;
}
// Ensure server directory is set, it must exist
let dir = match &config.server.directory {
Some(dir) => dir,
None => {
warn!(target: "lazymc", "Not blocking banned IPs, server directory not configured, unable to find {} file", ban::FILE);
return;
}
};
// Determine file path, ensure it exists
let path = dir.join(crate::mc::ban::FILE);
if !path.is_file() {
warn!(target: "lazymc", "Not blocking banned IPs, {} file does not exist", ban::FILE);
return;
}
// Load banned IPs once
match ban::load(&path) {
Ok(ips) => server.set_banned_ips_blocking(ips),
Err(err) => {
error!(target: "lazymc", "Failed to load banned IPs from {}: {}", ban::FILE, err);
}
}
// Show warning if 127.0.0.1 is banned
if server.is_banned_ip_blocking(&("127.0.0.1".parse().unwrap())) {
warn!(target: "lazymc", "Local address 127.0.0.1 IP banned, probably not what you want");
warn!(target: "lazymc", "Use '/pardon-ip 127.0.0.1' on the server to unban");
}
// Keep watching
while watch(&server, &path) {}
}
/// Watch the given file.
fn watch(server: &Server, path: &Path) -> bool {
// The file must exist
if !path.is_file() {
warn!(target: "lazymc", "File {} does not exist, not watching changes", ban::FILE);
return false;
}
// Create watcher for banned IPs file
let (tx, rx) = channel();
let mut watcher =
watcher(tx, WATCH_DEBOUNCE).expect("failed to create watcher for banned-ips.json");
if let Err(err) = watcher.watch(path, RecursiveMode::NonRecursive) {
error!(target: "lazymc", "An error occured while creating watcher for {}: {}", ban::FILE, err);
return true;
}
loop {
// Take next event
let event = rx.recv().unwrap();
// Decide whether to reload and rewatch
let (reload, rewatch) = match event {
// Reload on write
DebouncedEvent::NoticeWrite(_) | DebouncedEvent::Write(_) => (true, false),
// Reload and rewatch on rename/remove
DebouncedEvent::NoticeRemove(_)
| DebouncedEvent::Remove(_)
| DebouncedEvent::Rename(_, _)
| DebouncedEvent::Rescan
| DebouncedEvent::Create(_) => {
trace!(target: "lazymc", "File banned-ips.json removed, trying to rewatch after 1 second");
thread::sleep(WATCH_DEBOUNCE);
(true, true)
}
// Ignore chmod changes
DebouncedEvent::Chmod(_) => (false, false),
// Rewatch on error
DebouncedEvent::Error(_, _) => (false, true),
};
// Reload banned IPs
if reload {
info!(target: "lazymc", "Reloading list of banned IPs...");
match ban::load(path) {
Ok(ips) => server.set_banned_ips_blocking(ips),
Err(err) => {
error!(target: "lazymc", "Failed reload list of banned IPs from {}: {}", ban::FILE, err);
}
}
}
// Rewatch
if rewatch {
return true;
}
}
}

View File

@@ -1,3 +1,4 @@
pub mod ban_reload;
pub mod monitor;
pub mod server;
pub mod signal;

View File

@@ -1,3 +1,4 @@
use std::net::SocketAddr;
use std::sync::Arc;
use bytes::BytesMut;
@@ -5,8 +6,8 @@ use futures::FutureExt;
use tokio::net::{TcpListener, TcpStream};
use crate::config::Config;
use crate::proto::Client;
use crate::proxy;
use crate::proto::client::Client;
use crate::proxy::{self, ProxyHeader};
use crate::server::{self, Server};
use crate::service;
use crate::status;
@@ -38,13 +39,24 @@ pub async fn service(config: Arc<Config>) -> Result<(), ()> {
config.public.address, config.server.address,
);
if config.lockout.enabled {
warn!(
target: "lazymc",
"Lockout mode is enabled, nobody will be able to connect through the proxy",
);
}
// Spawn server monitor and signal handler services
tokio::spawn(service::monitor::service(config.clone(), server.clone()));
tokio::spawn(service::signal::service(config.clone(), server.clone()));
tokio::task::spawn_blocking({
let (config, server) = (config.clone(), server.clone());
|| service::ban_reload::service(config, server)
});
// Initiate server start
if config.server.wake_on_start {
Server::start(config.clone(), server.clone(), None);
Server::start(config.clone(), server.clone(), None).await;
}
// Route all incomming connections
@@ -58,18 +70,37 @@ pub async fn service(config: Arc<Config>) -> Result<(), ()> {
/// Route inbound TCP stream to correct service, spawning a new task.
#[inline]
fn route(inbound: TcpStream, config: Arc<Config>, server: Arc<Server>) {
if server.state() == server::State::Started {
// Get user peer address
let peer = match inbound.peer_addr() {
Ok(peer) => peer,
Err(err) => {
warn!(target: "lazymc", "Connection from unknown peer address, disconnecting: {}", err);
return;
}
};
// Check ban state, just drop connection if enabled
let banned = server.is_banned_ip_blocking(&peer.ip());
if config.server.drop_banned_ips {
info!(target: "lazymc", "Connection from banned IP {}, dropping", peer.ip());
return;
}
// Route connection through proper channel
let should_proxy =
!banned && server.state() == server::State::Started && !config.lockout.enabled;
if should_proxy {
route_proxy(inbound, config)
} else {
route_status(inbound, config, server)
route_status(inbound, config, server, peer)
}
}
/// Route inbound TCP stream to status server, spawning a new task.
#[inline]
fn route_status(inbound: TcpStream, config: Arc<Config>, server: Arc<Server>) {
fn route_status(inbound: TcpStream, config: Arc<Config>, server: Arc<Server>, peer: SocketAddr) {
// When server is not online, spawn a status server
let client = Client::default();
let client = Client::new(peer);
let service = status::serve(client, inbound, config, server).map(|r| {
if let Err(err) = r {
warn!(target: "lazymc", "Failed to serve status: {:?}", err);
@@ -83,7 +114,12 @@ fn route_status(inbound: TcpStream, config: Arc<Config>, server: Arc<Server>) {
#[inline]
fn route_proxy(inbound: TcpStream, config: Arc<Config>) {
// When server is online, proxy all
let service = proxy::proxy(inbound, config.server.address).map(|r| {
let service = proxy::proxy(
inbound,
ProxyHeader::Proxy.not_none(config.server.send_proxy_v2),
config.server.address,
)
.map(|r| {
if let Err(err) = r {
warn!(target: "lazymc", "Failed to proxy: {}", err);
}
@@ -95,9 +131,25 @@ fn route_proxy(inbound: TcpStream, config: Arc<Config>) {
/// Route inbound TCP stream to proxy with queued data, spawning a new task.
#[inline]
pub fn route_proxy_queue(inbound: TcpStream, config: Arc<Config>, queue: BytesMut) {
route_proxy_address_queue(
inbound,
ProxyHeader::Proxy.not_none(config.server.send_proxy_v2),
config.server.address,
queue,
);
}
/// Route inbound TCP stream to proxy with given address and queued data, spawning a new task.
#[inline]
pub fn route_proxy_address_queue(
inbound: TcpStream,
proxy_header: ProxyHeader,
addr: SocketAddr,
queue: BytesMut,
) {
// When server is online, proxy all
let service = async move {
proxy::proxy_with_queue(inbound, config.server.address, &queue)
proxy::proxy_with_queue(inbound, proxy_header, addr, &queue)
.map(|r| {
if let Err(err) = r {
warn!(target: "lazymc", "Failed to proxy: {}", err);

View File

@@ -1,27 +1,33 @@
use std::sync::Arc;
use std::time::{Duration, Instant};
use crate::server::State;
use bytes::BytesMut;
use minecraft_protocol::data::chat::{Message, Payload};
use minecraft_protocol::data::server_status::*;
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::login::{LoginDisconnect, LoginStart};
use minecraft_protocol::version::v1_14_4::login::LoginStart;
use minecraft_protocol::version::v1_14_4::status::StatusResponse;
use tokio::io::{self, AsyncWriteExt};
use tokio::net::tcp::WriteHalf;
use tokio::fs;
use tokio::io::AsyncWriteExt;
use tokio::net::TcpStream;
use tokio::time;
use crate::config::*;
use crate::proto::{self, Client, ClientState, RawPacket};
use crate::join;
use crate::proto::action;
use crate::proto::client::{Client, ClientInfo, ClientState};
use crate::proto::packet::{self, RawPacket};
use crate::proto::packets;
use crate::server::{self, Server};
use crate::service;
/// Client holding server state poll interval.
const HOLD_POLL_INTERVAL: Duration = Duration::from_millis(500);
/// The ban message prefix.
const BAN_MESSAGE_PREFIX: &str = "Your IP address is banned from this server.\nReason: ";
/// Default ban reason if unknown.
const DEFAULT_BAN_REASON: &str = "Banned by an operator.";
/// Server icon file path.
const SERVER_ICON_FILE: &str = "server-icon.png";
/// Proxy the given inbound stream to a target address.
// TODO: do not drop error here, return Box<dyn Error>
@@ -35,11 +41,14 @@ pub async fn serve(
// Incoming buffer and packet holding queue
let mut buf = BytesMut::new();
let mut hold_queue = BytesMut::new();
// Remember inbound packets, track client info
let mut inbound_history = BytesMut::new();
let mut client_info = ClientInfo::empty();
loop {
// Read packet from stream
let (packet, raw) = match proto::read_packet(&mut buf, &mut reader).await {
let (packet, raw) = match packet::read_packet(&client, &mut buf, &mut reader).await {
Ok(Some(packet)) => packet,
Ok(None) => break,
Err(_) => {
@@ -52,84 +61,129 @@ pub async fn serve(
let client_state = client.state();
// Hijack handshake
if client_state == ClientState::Handshake && packet.id == proto::STATUS_PACKET_ID_STATUS {
// Parse handshake, grab new state
let new_state = match Handshake::decode(&mut packet.data.as_slice()) {
Ok(handshake) => match ClientState::from_id(handshake.next_state) {
Some(state) => state,
None => {
error!(target: "lazymc", "Client tried to switch into unknown protcol state ({}), disconnecting", handshake.next_state);
break;
}
},
if client_state == ClientState::Handshake
&& packet.id == packets::handshake::SERVER_HANDSHAKE
{
// Parse handshake
let handshake = match Handshake::decode(&mut packet.data.as_slice()) {
Ok(handshake) => handshake,
Err(_) => {
debug!(target: "lazymc", "Got malformed handshake from client, disconnecting");
break;
}
};
// Update client state
// Parse new state
let new_state = match ClientState::from_id(handshake.next_state) {
Some(state) => state,
None => {
error!(target: "lazymc", "Client tried to switch into unknown protcol state ({}), disconnecting", handshake.next_state);
break;
}
};
// Update client info and client state
client_info
.protocol_version
.replace(handshake.protocol_version);
client.set_state(new_state);
// If login handshake and holding is enabled, hold packets
if new_state == ClientState::Login && config.time.hold() {
hold_queue.extend(raw);
// If loggin in with handshake, remember inbound
if new_state == ClientState::Login {
inbound_history.extend(raw);
}
continue;
}
// Hijack server status packet
if client_state == ClientState::Status && packet.id == proto::STATUS_PACKET_ID_STATUS {
let server_status = server_status(&config, &server);
if client_state == ClientState::Status && packet.id == packets::status::SERVER_STATUS {
let server_status = server_status(&config, &server).await;
let packet = StatusResponse { server_status };
let mut data = Vec::new();
packet.encode(&mut data).map_err(|_| ())?;
let response = RawPacket::new(0, data).encode()?;
let response = RawPacket::new(0, data).encode(&client)?;
writer.write_all(&response).await.map_err(|_| ())?;
continue;
}
// Hijack ping packet
if client_state == ClientState::Status && packet.id == proto::STATUS_PACKET_ID_PING {
if client_state == ClientState::Status && packet.id == packets::status::SERVER_PING {
writer.write_all(&raw).await.map_err(|_| ())?;
continue;
}
// Hijack login start
if client_state == ClientState::Login && packet.id == proto::LOGIN_PACKET_ID_LOGIN_START {
// Try to get login username
if client_state == ClientState::Login && packet.id == packets::login::SERVER_LOGIN_START {
// Try to get login username, update client info
// TODO: we should always parse this packet successfully
let username = LoginStart::decode(&mut packet.data.as_slice())
.ok()
.map(|p| p.name);
client_info.username = username.clone();
// Start server if not starting yet
Server::start(config.clone(), server.clone(), username);
// Hold client if enabled and starting
if config.time.hold() && server.state() == State::Starting {
// Hold login packet and remaining read bytes
hold_queue.extend(raw);
hold_queue.extend(buf.split_off(0));
// Start holding
hold(inbound, config, server, hold_queue).await?;
return Ok(());
// Kick if lockout is enabled
if config.lockout.enabled {
match username {
Some(username) => {
info!(target: "lazymc", "Kicked '{}' because lockout is enabled", username)
}
None => info!(target: "lazymc", "Kicked player because lockout is enabled"),
}
action::kick(&client, &config.lockout.message, &mut writer).await?;
break;
}
// Select message and kick
let msg = match server.state() {
server::State::Starting | server::State::Stopped | server::State::Started => {
&config.messages.login_starting
// Kick if client is banned
if let Some(ban) = server.ban_entry(&client.peer.ip()).await {
if ban.is_banned() {
let msg = if let Some(reason) = ban.reason {
info!(target: "lazymc", "Login from banned IP {} ({}), disconnecting", client.peer.ip(), &reason);
reason.to_string()
} else {
info!(target: "lazymc", "Login from banned IP {}, disconnecting", client.peer.ip());
DEFAULT_BAN_REASON.to_string()
};
action::kick(
&client,
&format!("{}{}", BAN_MESSAGE_PREFIX, msg),
&mut writer,
)
.await?;
break;
}
server::State::Stopping => &config.messages.login_stopping,
};
kick(msg, &mut writer).await?;
}
break;
// Start server if not starting yet
Server::start(config.clone(), server.clone(), username).await;
// Remember inbound packets
inbound_history.extend(&raw);
inbound_history.extend(&buf);
// Build inbound packet queue with everything from login start (including this)
let mut login_queue = BytesMut::with_capacity(raw.len() + buf.len());
login_queue.extend(&raw);
login_queue.extend(&buf);
// Buf is fully consumed here
buf.clear();
// Start occupying client
join::occupy(
client,
client_info,
config,
server,
inbound,
inbound_history,
login_queue,
)
.await?;
return Ok(());
}
// Show unhandled packet warning
@@ -138,106 +192,18 @@ pub async fn serve(
debug!(target: "lazymc", "- Packet ID: {}", packet.id);
}
// Gracefully close connection
match writer.shutdown().await {
Ok(_) => {}
Err(err) if err.kind() == io::ErrorKind::NotConnected => {}
Err(_) => return Err(()),
}
Ok(())
}
/// Hold a client while server starts.
///
/// Relays client to proxy once server is ready.
pub async fn hold<'a>(
mut inbound: TcpStream,
config: Arc<Config>,
server: Arc<Server>,
hold_queue: BytesMut,
) -> Result<(), ()> {
trace!(target: "lazymc", "Started holding client");
// Set up polling interval, get timeout
let mut poll_interval = time::interval(HOLD_POLL_INTERVAL);
let since = Instant::now();
let timeout = config.time.hold_client_for as u64;
loop {
// TODO: wait for start signal over channel instead of polling
poll_interval.tick().await;
trace!("Polling server state for holding client...");
match server.state() {
// Still waiting on server start
State::Starting => {
trace!(target: "lazymc", "Server not ready, holding client for longer");
// If hold timeout is reached, kick client
if since.elapsed().as_secs() >= timeout {
warn!(target: "lazymc", "Holding client reached timeout of {}s, disconnecting", timeout);
kick(&config.messages.login_starting, &mut inbound.split().1).await?;
return Ok(());
}
continue;
}
// Server started, start relaying and proxy
State::Started => {
// TODO: drop client if already disconnected
// Relay client to proxy
info!(target: "lazymc", "Server ready for held client, relaying to server");
service::server::route_proxy_queue(inbound, config, hold_queue);
return Ok(());
}
// Server stopping, this shouldn't happen, kick
State::Stopping => {
warn!(target: "lazymc", "Server stopping for held client, disconnecting");
kick(&config.messages.login_stopping, &mut inbound.split().1).await?;
break;
}
// Server stopped, this shouldn't happen, disconnect
State::Stopped => {
error!(target: "lazymc", "Server stopped for held client, disconnecting");
break;
}
}
}
// Gracefully close connection
match inbound.shutdown().await {
Ok(_) => {}
Err(err) if err.kind() == io::ErrorKind::NotConnected => {}
Err(_) => return Err(()),
}
Ok(())
}
/// Kick client with a message.
///
/// Should close connection afterwards.
async fn kick(msg: &str, writer: &mut WriteHalf<'_>) -> Result<(), ()> {
let packet = LoginDisconnect {
reason: Message::new(Payload::text(msg)),
};
let mut data = Vec::new();
packet.encode(&mut data).map_err(|_| ())?;
let response = RawPacket::new(0, data).encode()?;
writer.write_all(&response).await.map_err(|_| ())
}
/// Build server status object to respond to client with.
fn server_status(config: &Config, server: &Server) -> ServerStatus {
let status = server.status();
async fn server_status(config: &Config, server: &Server) -> ServerStatus {
let status = server.status().await;
let server_state = server.state();
// Respond with real server status if started
if server_state == server::State::Started && status.is_some() {
return status.as_ref().unwrap().clone();
}
// Select version and player max from last known server status
let (version, max) = match status.as_ref() {
@@ -253,17 +219,24 @@ fn server_status(config: &Config, server: &Server) -> ServerStatus {
// Select description, use server MOTD if enabled, or use configured
let description = {
if config.messages.use_server_motd && status.is_some() {
if config.motd.from_server && status.is_some() {
status.as_ref().unwrap().description.clone()
} else {
Message::new(Payload::text(match server.state() {
server::State::Stopped | server::State::Started => &config.messages.motd_sleeping,
server::State::Starting => &config.messages.motd_starting,
server::State::Stopping => &config.messages.motd_stopping,
Message::new(Payload::text(match server_state {
server::State::Stopped | server::State::Started => &config.motd.sleeping,
server::State::Starting => &config.motd.starting,
server::State::Stopping => &config.motd.stopping,
}))
}
};
// Get server favicon
let favicon = if config.motd.from_server && status.is_some() {
status.as_ref().unwrap().favicon.clone()
} else {
favicon(&config).await
};
// Build status resposne
ServerStatus {
version,
@@ -273,5 +246,36 @@ fn server_status(config: &Config, server: &Server) -> ServerStatus {
max,
sample: vec![],
},
favicon,
}
}
/// Get server status favicon.
async fn favicon(config: &Config) -> Option<String> {
// Get server dir
let dir = match config.server.directory.as_ref() {
Some(dir) => dir,
None => return None,
};
// Server icon file, ensure it exists
let path = dir.join(SERVER_ICON_FILE);
if !path.is_file() {
return None;
}
// Read icon data
let data = fs::read(path)
.await
.map_err(|err| {
error!(target: "lazymc", "Failed to read favicon from {}: {}", SERVER_ICON_FILE, err);
})
.ok()?;
// Format and return favicon
Some(format!(
"{}{}",
"data:image/png;base64,",
base64::encode(data)
))
}

View File

@@ -1,5 +1,6 @@
pub mod cli;
pub mod error;
pub mod serde;
pub mod style;
use std::env;

30
src/util/serde.rs Normal file
View File

@@ -0,0 +1,30 @@
use std::net::{SocketAddr, ToSocketAddrs};
use serde::de::{Error, Unexpected};
use serde::{Deserialize, Deserializer};
/// Deserialize a `Vec` into a `HashMap` by key.
pub fn to_socket_addrs<'de, D>(d: D) -> Result<SocketAddr, D::Error>
where
D: Deserializer<'de>,
{
// Deserialize string
let addr = String::deserialize(d)?;
// Try to socket address to resolve
match addr.to_socket_addrs() {
Ok(mut addr) => {
if let Some(addr) = addr.next() {
return Ok(addr);
}
}
Err(err) => {
dbg!(err);
}
}
// Parse raw IP address
addr.parse().map_err(|_| {
Error::invalid_value(Unexpected::Str(&addr), &"IP or resolvable host and port")
})
}