From 6f74284f88b1f6772301bf178965e884277532e7 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Thu, 15 Jan 2026 16:50:03 +0700 Subject: [PATCH 001/115] refactor: migrate from Java/Gradle to Rust/Cargo for server implementation feat: add Rust implementation with multi-stage Docker build feat: add new GitHub workflows for Rust builds chore: remove Java/Gradle files and dependencies docs: update Dockerfile for Rust deployment --- .github/workflows/gradle.yml | 25 -- .github/workflows/release.yml | 26 ++ .github/workflows/rust-release.yml | 26 ++ .gitignore | 5 + Cargo.lock | 7 + Cargo.toml | 6 + Dockerfile | 32 +- README.md | 69 ---- build.gradle | 64 --- gradle.properties | 13 - gradle/wrapper/gradle-wrapper.jar | Bin 43764 -> 0 bytes gradle/wrapper/gradle-wrapper.properties | 7 - gradlew | 251 ------------ gradlew.bat | 94 ----- libs/PlayerConnectShared.jar | Bin 16091 -> 0 bytes settings.gradle | 1 - src/main.rs | 3 + src/playerconnect/Configs.java | 8 - src/playerconnect/HttpServer.java | 283 ------------- src/playerconnect/NetworkRelay.java | 455 --------------------- src/playerconnect/PlayerConnect.java | 45 -- src/playerconnect/PlayerConnectEvents.java | 102 ----- src/playerconnect/ServerRoom.java | 238 ----------- src/playerconnect/Utils.java | 15 - 24 files changed, 97 insertions(+), 1678 deletions(-) delete mode 100644 .github/workflows/gradle.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/rust-release.yml create mode 100644 Cargo.lock create mode 100644 Cargo.toml delete mode 100644 README.md delete mode 100644 build.gradle delete mode 100644 gradle.properties delete mode 100644 gradle/wrapper/gradle-wrapper.jar delete mode 100644 gradle/wrapper/gradle-wrapper.properties delete mode 100644 gradlew delete mode 100644 gradlew.bat delete mode 100644 libs/PlayerConnectShared.jar delete mode 100644 settings.gradle create mode 100644 src/main.rs delete mode 100644 src/playerconnect/Configs.java delete mode 100644 src/playerconnect/HttpServer.java delete mode 100644 src/playerconnect/NetworkRelay.java delete mode 100644 src/playerconnect/PlayerConnect.java delete mode 100644 src/playerconnect/PlayerConnectEvents.java delete mode 100644 src/playerconnect/ServerRoom.java delete mode 100644 src/playerconnect/Utils.java diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml deleted file mode 100644 index ec12b62..0000000 --- a/.github/workflows/gradle.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Build and push to DockerHub -on: - push: - branches: - - main -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Login to DockerHub - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Build and push Docker image - uses: docker/build-push-action@v5 - with: - context: ./ - push: true - tags: ghcr.io/mindustrytool/player-connect-server:latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..8edb990 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,26 @@ +name: Build and push to DockerHub +on: + push: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: ./ + push: true + tags: ghcr.io/mindustrytool/player-connect-server:latest diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml new file mode 100644 index 0000000..958c865 --- /dev/null +++ b/.github/workflows/rust-release.yml @@ -0,0 +1,26 @@ +name: Build and push to DockerHub +on: + push: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: ./ + push: true + tags: ghcr.io/mindustrytool/player-connect-server:rust diff --git a/.gitignore b/.gitignore index a31a6f7..da4e6cd 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,8 @@ .project build/ bin/ + + +# Added by cargo + +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..f1b1e97 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "server" +version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..ff06670 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "server" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/Dockerfile b/Dockerfile index 2e88686..2c226b3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,30 @@ -FROM gradle:8.9.0-jdk17 AS build -COPY --chown=gradle:gradle . /home/gradle/src +# ---------- Build stage ---------- +FROM rust:1.85-slim AS build -WORKDIR /home/gradle/src +# Create app user (optional but good practice) +RUN useradd -m rustuser -RUN chmod +x gradlew +WORKDIR /app -RUN ./gradlew jar --no-daemon +# Cache dependencies first +COPY Cargo.toml Cargo.lock ./ +RUN mkdir src && echo "fn main() {}" > src/main.rs +RUN cargo build --release +RUN rm -rf src -FROM eclipse-temurin:17-jre-alpine +# Copy real source +COPY . . +RUN cargo build --release -COPY --from=build /home/gradle/src/build/libs/*.jar /app/server.jar +# ---------- Runtime stage ---------- +FROM gcr.io/distroless/cc-debian12 -ENTRYPOINT ["java","-Xmx384m","-jar", "/app/server.jar"] +WORKDIR /app + +# Copy compiled binary +COPY --from=build /app/target/release/server /app/server + +# Run as non-root +USER nonroot + +ENTRYPOINT ["/app/server"] diff --git a/README.md b/README.md deleted file mode 100644 index c006319..0000000 --- a/README.md +++ /dev/null @@ -1,69 +0,0 @@ -# Copy Link and Join (CLaJ) -This system allow you to play with your friends just by creating a room, copy the link and send it to your friends.
-In fact it's pretty much the same thing as Hamachi, but in a Mindustry mod. - -This is a bundled, reworked and optimized version of the [CLaJ server](https://github.com/xzxADIxzx/Copy-Link-and-Join) and the [xzxADIxzx's Scheme-Size mod](https://github.com/xzxADIxzx/Scheme-Size) (with only the CLaJ feature). - -> [!IMPORTANT] -> This CLaJ version is not compatible with the [xzxADIxzx's](https://github.com/xzxADIxzx) one.
-> The protocol has been reworked and optimized and CLaJ links have also been changed to a more standard version. - - -## Mindustry v8 note -Mindustry v8 has been released, and many changes have been made. Mods must now make changes to be compatible with this version.
-The mod is not officially updated to this version, at this time, but it remains compatible with it. - -To install the mod for mindustry v8, just go to the mod browser, search for **'claj'**, then click ``View Releases`` -and install the latest version named **'CLaJ for Mindustry v8'**.
-Or you can download the mod file from the [releases section](https://github.com/Xpdustry/claj/releases) of pre-releases versions and place it into the mod folder of your game. - - -## How to use -### Client -**First, if you don't have the mod yet, you can find it in the mod browser by searching for 'claj' and then installing it.** - -Start and host a map as normal (or your campaign): ``Host Multiplayer Game`` **>** ``Host``.
-Then go to ``Manage CLaJ Room``, select a server (or add your own), now ``Create Room`` and wait for the room to be created, click ``Copy Link`` and send the copied link to your friends. - -To join, it's simple, copy the link your friend sent you, open your game, go to ``Play`` **>** ``Join Game`` **>** ``Join via CLaJ``, paste the link and ``OK``. - -Now, if all goods, you can play with your friends, so enjoy =). - - -### Server -To host a server, just run the command ``java -jar claj-server.jar ``, where ```` is the port for the server.
-Also don't forget to open the port in TCP and UDP mode on your end-point and redirect it to the host machine. - -A CLaJ server doesn't need much memory and cpu, 256MB of memory and one core are enough, even at high traffic.
-To change the memory allocated to the server, change the command to ``java -Xms -Xmx -jar claj-server.jar ``, where ```` is the memory allocated to the server *(e.g. 256m for 256 MB of ram)*. - -> [!IMPORTANT] -> Please note that if you plan to make a public server, CLaJ servers are high bandwidth consumers, as they act as a relay. For an average server, around 1TB up/down of consumption per month and around 1MB/s of constant network usage. -> -> Also, you can create a Pull-Request to add your server to the public server list (in [public-servers.hjson](https://github.com/xpdustry/claj/blob/main/public-servers.hjson)). - - -## How it works -CLaJ is a system like [Hamachi](https://vpn.net/), that allows you to create a room and share the link to your friends. This way, they can connect to you as if they were on your private network. - -The only differences are that Hamachi requires account creation and therefore the collection of personal information, etc., while CLaJ does not. And CLaJ is directly integrated into Mindustry and optimized for it, which makes it easier to use compared to Hamachi, which needs to stay in parallel of the game. - -On the host player's side, it's server never receives packets from people connected via CLaJ, the work is done by the CLaJ Proxy which simply run the server's callbacks. - - -## How to build -Pre-build releases can be found in the [releases section](https://github.com/Xpdustry/claj/releases), but if you want to build the project yourself, follow the steps above. - -To build the client version, simply run the command ``./gradlew client:build``. The jar file will be located in the root directory and named ``claj-client.jar``. - -To build the server version, simply run the command ``./gradlew server:build``. The jar file will be located in the root directory and named ``claj-server.jar``. - -You can also run a test server by using the command ``./gradlew server:run``. It will be hosted on port ``7000``. - - -## Modding -The CLaJ server can be modded using plugins that are located in the ``plugins/`` directory.
-They work the same way as [Mindustry mods](https://mindustrygame.github.io/wiki/modding/2-plugins/), but only handles Java ones (not json and js) and doesn't handles sprites, icon, bundles, and others things designed for client-side. - -The descriptor file can therefore only be ``plugin.json`` or ``plugin.hjson``, and some properties are removed, such as ``java`` because these can only be Java plugins, or ``texturescale`` because there is no texture handling on servers.
-Supported plugin properties: ``name``, ``internalName``, ``displayName``, ``author``, ``description``, ``version``, ``repo``, ``main``, ``dependencies`` and ``softDependencies``. diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 07e110f..0000000 --- a/build.gradle +++ /dev/null @@ -1,64 +0,0 @@ -apply plugin: "java" - -version '1.0' - -java { - targetCompatibility = 8 - sourceCompatibility = JavaVersion.VERSION_17 -} - -allprojects{ - tasks.withType(JavaCompile){ - options.annotationProcessorPath = configurations.annotationProcessor - options.compilerArgs.addAll(['--release', '8']) - } -} - -sourceSets.main.java.srcDirs = ["src"] - -compileJava.options.encoding = "UTF-8" -compileTestJava.options.encoding = "UTF-8" - - - -repositories{ - mavenCentral() - maven{ url "https://raw.githubusercontent.com/Zelaux/MindustryRepo/master/repository" } - maven{ url 'https://jitpack.io' } -} - -ext{ - //the build number that this plugin is made for - mindustryVersion = 'v153' - jabelVersion = "93fde537c7" -} - -dependencies{ - implementation files("libs/PlayerConnectShared.jar") - implementation 'io.javalin:javalin:6.7.0' - - implementation 'com.fasterxml.jackson.core:jackson-databind:2.16.2' - - implementation "com.github.Anuken.Arc:arc-core:$mindustryVersion" - implementation "com.github.Anuken.Mindustry:core:$mindustryVersion" - - compileOnly 'org.projectlombok:lombok:1.18.32' - - annotationProcessor 'org.projectlombok:lombok:1.18.32' - - annotationProcessor "com.github.Anuken:jabel:$jabelVersion" -} - -jar{ - duplicatesStrategy(DuplicatesStrategy.EXCLUDE) - archiveFileName = "${project.archivesBaseName}.jar" - from{ - configurations.runtimeClasspath.collect{it.isDirectory() ? it : zipTree(it)} - } - - manifest { - attributes( - 'Main-Class': 'playerconnect.PlayerConnect' - ) - } -} diff --git a/gradle.properties b/gradle.properties deleted file mode 100644 index 051052c..0000000 --- a/gradle.properties +++ /dev/null @@ -1,13 +0,0 @@ -org.gradle.jvmargs=--illegal-access=permit \ ---add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \ ---add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED \ ---add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED \ ---add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED \ ---add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \ ---add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED \ ---add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \ ---add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \ ---add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED \ ---add-exports=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED \ ---add-exports=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED \ ---add-exports=java.base/sun.reflect.annotation=ALL-UNNAMED diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 1b33c55baabb587c669f562ae36f953de2481846..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43764 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 '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -# This is normally unused -# shellcheck disable=SC2034 -APP_BASE_NAME=${0##*/} -# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH="\\\"\\\"" - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - if ! command -v java >/dev/null 2>&1 - then - die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, -# and any embedded shellness will be escaped. -# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be -# treated as '${Hostname}' itself on the command line. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ - "$@" - -# Stop when "xargs" is not available. -if ! command -v xargs >/dev/null 2>&1 -then - die "xargs is not available" -fi - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat deleted file mode 100644 index db3a6ac..0000000 --- a/gradlew.bat +++ /dev/null @@ -1,94 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem -@rem SPDX-License-Identifier: Apache-2.0 -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH= - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/libs/PlayerConnectShared.jar b/libs/PlayerConnectShared.jar deleted file mode 100644 index e206f59520c8dcc852dad62a0ec796ef910accae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16091 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 diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index ddb8a08..0000000 --- a/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'PlayerConnectServer' diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/src/playerconnect/Configs.java b/src/playerconnect/Configs.java deleted file mode 100644 index 2ebf35b..0000000 --- a/src/playerconnect/Configs.java +++ /dev/null @@ -1,8 +0,0 @@ -package playerconnect; - -import arc.struct.Seq; - -public class Configs { - public static final Seq IP_BLACK_LIST = new Seq<>(); - public static final int SPAM_LIMIT = 300; -} diff --git a/src/playerconnect/HttpServer.java b/src/playerconnect/HttpServer.java deleted file mode 100644 index 7123bcd..0000000 --- a/src/playerconnect/HttpServer.java +++ /dev/null @@ -1,283 +0,0 @@ -package playerconnect; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; - -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.SerializationFeature; - -import arc.Events; -import arc.net.DcReason; -import arc.struct.Seq; -import arc.util.Log; -import io.javalin.Javalin; -import io.javalin.json.JavalinJackson; -import io.javalin.plugin.bundled.RouteOverviewPlugin; -import mindustry.game.Gamemode; -import playerconnect.PlayerConnectEvents.RoomClosedEvent; -import playerconnect.shared.Packets; -import playerconnect.shared.Packets.RoomStats; -import io.javalin.http.Context; -import io.javalin.http.sse.SseClient; - -import lombok.Data; - -public class HttpServer { - - private Javalin app; - - private final Queue statsConsumers = new ConcurrentLinkedQueue<>(); - private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); - - private final String magicWord = "ditmemay"; - - public HttpServer() { - app = Javalin.create(config -> { - config.showJavalinBanner = false; - config.jsonMapper(new JavalinJackson().updateMapper(mapper -> { - mapper// - - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)// - .configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true)// - .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) - .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); - - })); - - config.http.asyncTimeout = 5_000; - config.useVirtualThreads = true; - - config.registerPlugin(new RouteOverviewPlugin()); - - config.requestLogger.http((ctx, ms) -> { - if (!ctx.fullUrl().contains("stats")) { - Log.info("[" + ctx.method().name() + "] " + Math.round(ms) + "ms " + ctx.fullUrl()); - } - }); - - config.bundledPlugins.enableCors(cors -> { - cors.addRule(rules -> { - rules.anyHost(); - }); - }); - }); - - app.sse("rooms", client -> { - client.keepAlive(); - - client.onClose(() -> { - Log.info("Client removed: <" + client + ">"); - statsConsumers.remove(client); - }); - - ArrayList data = PlayerConnect.relay.rooms - .values() - .toSeq() - .map(room -> toLiveData(room, room.stats)) - .list(); - - Log.info("Client connected <" + client + "> sending " + data.size() + " rooms"); - - client.sendEvent("update", data); - - statsConsumers.add(client); - }); - - app.get("ping", ctx -> ctx.result("pong")); - - app.get("connections", ctx -> { - if (!auth(ctx)) { - return; - } - - var result = Seq.with(PlayerConnect.relay.getConnections()) - .map(connection -> { - - ConnectionDto dto = new ConnectionDto(); - dto.id = Utils.toString(connection); - dto.ip = Utils.getIP(connection); - - return dto; - }).list(); - - ctx.json(result); - }); - - app.post("ban", ctx -> { - if (!auth(ctx)) { - return; - } - - var ip = ctx.body(); - - for (var connection : PlayerConnect.relay.getConnections()) { - if (Utils.getIP(connection).equals(ip)) { - connection.close(DcReason.closed); - Log.info("Kicked client <" + Utils.toString(connection) + "> for IP ban"); - Events.fire(new PlayerConnectEvents.ClientKickedEvent(connection)); - } - } - - Configs.IP_BLACK_LIST.add(ip); - - ctx.json(Configs.IP_BLACK_LIST.list()); - }); - - app.post("unban", ctx -> { - if (!auth(ctx)) { - return; - } - - var ip = ctx.body(); - Configs.IP_BLACK_LIST.remove(ip); - - ctx.json(Configs.IP_BLACK_LIST.list()); - }); - - app.start(Integer.parseInt(System.getenv("PLAYER_CONNECT_HTTP_PORT"))); - - Events.on(PlayerConnectEvents.RoomCreatedEvent.class, event -> { - try { - sendUpdateEvent(toLiveData(event.room, event.room.stats)); - } catch (Exception error) { - Log.err("Failed to send initial room stats", error); - } - }); - - Events.on(Packets.StatsPacket.class, event -> { - try { - ServerRoom room = PlayerConnect.relay.rooms.get(event.roomId); - - if (room != null) { - sendUpdateEvent(toLiveData(room, event.data)); - } else { - Log.warn("Update stats for non-existent room"); - } - } catch (Exception error) { - Log.err("Failed to send room stats", error); - } - }); - - Events.on(RoomClosedEvent.class, event -> { - sendRemoveEvent(event.room.id); - }); - - scheduler.scheduleWithFixedDelay(() -> { - try { - statsConsumers.forEach(client -> client.sendComment("Kept alive")); - } catch (Exception error) { - Log.err("Failed to send keep alive comment", error); - } - }, 0, 1, TimeUnit.SECONDS); - } - - private boolean auth(Context context) { - var word = context.header("Authorization"); - - if (!word.equals(magicWord)) { - context.status(403); - context.result("Forbidden"); - return false; - } - - return true; - } - - private void sendUpdateEvent(StatsLiveEvent stat) { - try { - ArrayList response = new ArrayList<>(); - - response.add(stat); - - for (SseClient client : statsConsumers) { - client.sendEvent("update", response); - } - } catch (Exception error) { - Log.err("Failed to send update event", error); - } - } - - private void sendRemoveEvent(String roomId) { - try { - - Log.info("Sent remove event for " + roomId); - - HashMap response = new HashMap<>(); - - response.put("roomId", roomId); - - for (SseClient client : statsConsumers) { - client.sendEvent("remove", response); - } - } catch (Exception error) { - Log.err("Failed to send remove event", error); - } - } - - private StatsLiveEvent toLiveData(ServerRoom room, RoomStats stats) { - StatsLiveEvent response = new StatsLiveEvent(); - - StatsLiveEventData data = new StatsLiveEventData(); - - data.mapName = stats.mapName; - data.name = stats.name; - data.gamemode = stats.gamemode; - data.mods = stats.mods.list(); - data.isSecured = room.password != null && !room.password.isEmpty(); - data.locale = stats.locale; - data.version = stats.version; - data.createdAt = room.createdAt; - data.ping = room.ping; - - for (Packets.RoomPlayer playerData : stats.players) { - StatsLiveEventPlayerData player = new StatsLiveEventPlayerData(); - player.name = playerData.name; - player.locale = playerData.locale; - data.players.add(player); - } - - response.roomId = room.id; - response.data = data; - - return response; - } - - @Data - private static class StatsLiveEvent { - public String roomId = null; - public StatsLiveEventData data; - } - - @Data - private static class StatsLiveEventData { - public String name = ""; - public String status = "UP"; - public boolean isPrivate = false; - public boolean isSecured = false; - public ArrayList players = new ArrayList<>(); - public String mapName = "unknown"; - public String gamemode = Gamemode.survival.name(); - public ArrayList mods = new ArrayList<>(); - public String locale; - public String version; - public Long createdAt; - public Long ping = 0L; - } - - @Data - private static class StatsLiveEventPlayerData { - public String name = ""; - public String locale = "en"; - } - - @Data - private static class ConnectionDto { - public String id = ""; - public String ip = ""; - } -} diff --git a/src/playerconnect/NetworkRelay.java b/src/playerconnect/NetworkRelay.java deleted file mode 100644 index c144ce5..0000000 --- a/src/playerconnect/NetworkRelay.java +++ /dev/null @@ -1,455 +0,0 @@ -package playerconnect; - -import java.nio.ByteBuffer; - -import arc.Events; -import arc.net.Connection; -import arc.net.DcReason; -import arc.net.FrameworkMessage; -import arc.net.FrameworkMessage.*; -import arc.net.NetListener; -import arc.net.NetSerializer; -import arc.net.Server; -import arc.struct.IntMap; -import arc.struct.IntSet; -import arc.struct.ObjectMap; -import arc.util.Log; -import arc.util.Ratekeeper; -import arc.util.io.ByteBufferInput; -import arc.util.io.ByteBufferOutput; -import playerconnect.shared.Packets; - -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; - -public class NetworkRelay extends Server implements NetListener { - protected boolean isClosed; - - /** - * Keeps a cache of packets received from connections that are not yet in a - * room. (queue of 3 packets)
- * Because sometimes {@link Packets.RoomJoinPacket} comes after - * {@link Packets.ConnectPacket}, - * when the client connection is slow, so the server will ignore this essential - * packet and the client - * will waits until the timeout. - */ - protected final IntMap packetQueue = new IntMap<>(); - /** Size of the packet queue */ - protected final int packetQueueSize = 6; - /** - * Keeps a cache of already notified idling connection, to avoid packet - * spamming. - */ - protected final IntSet notifiedIdle = new IntSet(); - /** List of created rooms */ - public final ObjectMap rooms = new ObjectMap<>(); - - public final int ROOM_IDLE_TIMEOUT = 10 * 60 * 1000; - - private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); - - public NetworkRelay() { - super(32768, 16384, new Serializer()); - addListener(this); - - scheduler.scheduleWithFixedDelay(() -> { - var delete = rooms.values().toSeq() - .select(r -> r.updatedAt < System.currentTimeMillis() - ROOM_IDLE_TIMEOUT); - for (ServerRoom room : delete) { - rooms.remove(room.id); - room.close(Packets.RoomClosedPacket.CloseReason.closed); - Events.fire(new PlayerConnectEvents.RoomClosedEvent(room)); - } - }, 0, 60, TimeUnit.SECONDS); - } - - @Override - public void run() { - isClosed = false; - super.run(); - } - - @Override - public void stop() { - isClosed = true; - Events.fire(new PlayerConnectEvents.ServerStoppingEvent()); - closeRooms(); - super.stop(); - } - - public boolean isClosed() { - return isClosed; - } - - public void closeRooms() { - try { - rooms.values().forEach(r -> { - r.close(Packets.RoomClosedPacket.CloseReason.serverClosed); - Events.fire(new PlayerConnectEvents.RoomClosedEvent(r)); - }); - } catch (Throwable ignored) { - } - rooms.clear(); - } - - @Override - public void connected(Connection connection) { - if (isClosed()) { - Log.info("Connection @ denied, server closed.", Utils.toString(connection)); - connection.close(DcReason.closed); - return; - } - - if (Configs.IP_BLACK_LIST.contains(Utils.getIP(connection))) { - Log.info("Connection @ denied, ip banned: @", Utils.toString(connection), Utils.getIP(connection)); - connection.close(DcReason.closed); - return; - } - - Log.info("Connection @ received, ip: @", Utils.toString(connection), Utils.getIP(connection)); - connection.setArbitraryData(new Ratekeeper()); - connection.setName("Connection" + Utils.toString(connection)); // fix id format in stacktraces - Events.fire(new PlayerConnectEvents.ClientConnectedEvent(connection)); - } - - @Override - public void disconnected(Connection connection, DcReason reason) { - if (reason != DcReason.error) { - Log.info("Connection @ lost: @.", Utils.toString(connection), reason); - } - - if (connection.getLastProtocolError() != null) { - Log.err(connection.getLastProtocolError()); - } - - notifiedIdle.remove(connection.getID()); - packetQueue.remove(connection.getID()); - - // Avoid searching for a room if it was an invalid connection or just a ping - if (!(connection.getArbitraryData() instanceof Ratekeeper)) - return; - - ServerRoom room = find(connection); - - if (room != null) { - room.disconnected(connection, reason); - // Remove the room if it was the host - - if (connection == room.host) { - rooms.remove(room.id); - Log.info("Room @ closed because connection @ (the host) has disconnected.", room.id, - Utils.toString(connection)); - Events.fire(new PlayerConnectEvents.RoomClosedEvent(room)); - } else - Log.info("Connection @ left the room @.", Utils.toString(connection), room.id); - } - - Events.fire(new PlayerConnectEvents.ClientDisconnectedEvent(connection, reason, room)); - } - - @Override - public void received(Connection connection, Object object) { - try { - if (!(connection.getArbitraryData() instanceof Ratekeeper) || (object instanceof FrameworkMessage)) - return; - - notifiedIdle.remove(connection.getID()); - - Ratekeeper rate = (Ratekeeper) connection.getArbitraryData(); - ServerRoom room = find(connection); - - // Simple packet spam protection, ignored for room hosts - if ((room == null || room.host != connection) && Configs.SPAM_LIMIT > 0 - && !rate.allow(3000L, Configs.SPAM_LIMIT)) { - - if (room != null) { - room.message(Packets.Message2Packet.MessageType.packetSpamming); - room.disconnected(connection, DcReason.closed); - } - - connection.close(DcReason.closed); - Log.warn("Connection @ disconnected for packet spamming.", Utils.toString(connection)); - Events.fire(new PlayerConnectEvents.ClientKickedEvent(connection)); - - } else if (object instanceof Packets.StatsPacket) { - Packets.StatsPacket statsPacket = (Packets.StatsPacket) object; - if (room != null) { - room.stats = statsPacket.data; - room.updatedAt = System.currentTimeMillis(); - room.ping = System.currentTimeMillis() - statsPacket.data.createdAt; - Events.fire(statsPacket); - } - } else if (object instanceof Packets.RoomJoinPacket) { - Packets.RoomJoinPacket joinPacket = (Packets.RoomJoinPacket) object; - // Disconnect from a potential another room. - if (room != null) { - // Ignore if it's the host of another room - if (room.host == connection) { - room.message(Packets.Message2Packet.MessageType.alreadyHosting); - Log.warn("Connection @ tried to join the room @ but is already hosting the room @.", - Utils.toString(connection), joinPacket.roomId, room.id); - Events.fire(new PlayerConnectEvents.ActionDeniedEvent(connection, - Packets.Message2Packet.MessageType.alreadyHosting)); - return; - } - // Disconnect from the room - Log.info("Connection @ left the room @ when trying to join", Utils.toString(connection), room.id); - room.disconnected(connection, DcReason.closed); - } - - room = get(((Packets.RoomJoinPacket) object).roomId); - if (room != null) { - if (!room.password.equals(joinPacket.password)) { - mindustry.net.Packets.Disconnect p = new mindustry.net.Packets.Disconnect(); - p.reason = "Wrong password"; - connection.sendTCP(p); - Log.info("Connection @ tried to join the room @ with wrong password.", - Utils.toString(connection), room.id); - return; - } - - room.connected(connection); - Log.info("Connection @ joined the room @.", Utils.toString(connection), room.id); - - // Send the queued packets of connections to room host - ByteBuffer[] queue = packetQueue.remove(connection.getID()); - if (queue != null) { - Log.debug("Sending queued packets of connection @ to room host.", - Utils.toString(connection)); - for (int i = 0; i < queue.length; i++) { - if (queue[i] != null) - room.received(connection, queue[i]); - } - } - - Events.fire(new PlayerConnectEvents.ConnectionJoinedEvent(connection, room)); - } else { - connection.close(DcReason.error); - Log.info("Connection @ tried to join a non-existent room @.", - Utils.toString(connection), joinPacket.roomId); - Log.info("Room list: @", rooms.values().toSeq().map(r -> r.id).toString()); - } - - } else if (object instanceof Packets.RoomCreationRequestPacket) { - // Ignore room creation requests when the server is closing - Packets.RoomCreationRequestPacket packet = (Packets.RoomCreationRequestPacket) object; - - if (isClosed()) { - Packets.RoomClosedPacket p = new Packets.RoomClosedPacket(); - p.reason = Packets.RoomClosedPacket.CloseReason.serverClosed; - connection.sendTCP(p); - connection.close(DcReason.error); - Events.fire(new PlayerConnectEvents.RoomCreationRejectedEvent(connection, p.reason)); - Log.info("Ignore room creation, server is closing"); - return; - } - - // Ignore if the connection is already in a room or hold one - if (room != null) { - room.message(Packets.Message2Packet.MessageType.alreadyHosting); - Log.warn("Connection @ tried to create a room but is already hosting the room @.", - Utils.toString(connection), room.id); - Events.fire(new PlayerConnectEvents.ActionDeniedEvent(connection, - Packets.Message2Packet.MessageType.alreadyHosting)); - return; - } - - room = new ServerRoom(connection, packet.password, packet.data); - - rooms.put(room.id, room); - room.create(); - Log.info("Room @ created by connection @.", room.id, Utils.toString(connection)); - Events.fire(new PlayerConnectEvents.RoomCreatedEvent(room)); - - } else if (object instanceof Packets.RoomClosureRequestPacket) { - // Only room host can close the room - if (room == null) - return; - if (room.host != connection) { - room.message(Packets.Message2Packet.MessageType.roomClosureDenied); - Log.warn("Connection @ tried to close the room @ but is not the host.", - Utils.toString(connection), - room.id); - Events.fire(new PlayerConnectEvents.ActionDeniedEvent(connection, - Packets.Message2Packet.MessageType.roomClosureDenied)); - return; - } - - rooms.remove(room.id); - room.close(); - Log.info("Room @ closed by connection @ (the host).", room.id, Utils.toString(connection)); - Events.fire(new PlayerConnectEvents.RoomClosedEvent(room)); - - } else if (object instanceof Packets.ConnectionClosedPacket) { - // Only room host can request a connection closing - if (room == null) - return; - - Packets.ConnectionClosedPacket closePacket = (Packets.ConnectionClosedPacket) object; - - if (room.host != connection) { - - room.message(Packets.Message2Packet.MessageType.conClosureDenied); - Log.warn("Connection @ tried to close the connection @ but is not the host of room @.", - Utils.toString(connection), closePacket.connectionId, room.id); - Events.fire(new PlayerConnectEvents.ActionDeniedEvent(connection, - Packets.Message2Packet.MessageType.conClosureDenied)); - return; - } - - int connectionId = closePacket.connectionId; - Connection con = arc.util.Structs.find(getConnections(), c -> c.getID() == connectionId); - DcReason reason = ((Packets.ConnectionClosedPacket) object).reason; - - // Ignore when trying to close itself or closing one that not in the same room - if (con == connection || !room.contains(con)) { - Log.warn("Connection @ (room @) tried to close a connection from another room.", - Utils.toString(connection), room.id); - return; - } - - if (con != null) { - Log.info("Connection @ (room @) closed the connection @.", Utils.toString(connection), room.id, - Utils.toString(con)); - room.disconnectedQuietly(con, reason); - con.close(reason); - // An event for this is useless, #disconnected() will trigger it - } - - // Ignore if the connection is not in a room - } else if (room != null) { - if (room.host == connection && (object instanceof Packets.ConnectionWrapperPacket)) - notifiedIdle.remove(((Packets.ConnectionWrapperPacket) object).connectionId); - - room.received(connection, object); - - // Puts in queue; if full, future packets will be ignored. - } else if (object instanceof ByteBuffer) { - ByteBuffer[] queue = packetQueue.get(connection.getID(), () -> new ByteBuffer[packetQueueSize]); - ByteBuffer buffer = (ByteBuffer) object; - - for (int i = 0; i < queue.length; i++) { - if (queue[i] == null) { - queue[i] = (ByteBuffer) ByteBuffer.allocate(buffer.remaining()).put(buffer).rewind(); - break; - } - } - } else { - Log.warn("Unhandled packet: @", object); - } - } catch (Exception error) { - Log.err("Failed to handle: " + object, error); - } - } - - /** - * Does nothing if the connection idle state was already notified to the room - * host. - */ - @Override - public void idle(Connection connection) { - if (!(connection.getArbitraryData() instanceof Ratekeeper)) - return; - if (!notifiedIdle.add(connection.getID())) - return; - - ServerRoom room = find(connection); - if (room != null) - room.idle(connection); - } - - public ServerRoom get(String roomId) { - return rooms.get(roomId); - } - - public ServerRoom find(Connection con) { - for (ServerRoom r : rooms.values()) { - if (r.contains(con)) - return r; - } - return null; - } - - public static class Serializer implements NetSerializer { - private final ThreadLocal last = arc.util.Threads.local(() -> ByteBuffer.allocate(32768)); - - @Override - public Object read(ByteBuffer buffer) { - byte id = buffer.get(); - - if (id == -2/* framework id */) - return readFramework(buffer); - - if (id == Packets.id) { - Packets.Packet packet = Packets.newPacket(buffer.get()); - packet.read(new ByteBufferInput(buffer)); - if (packet instanceof Packets.ConnectionPacketWrapPacket) // This one is special - ((Packets.ConnectionPacketWrapPacket) packet).buffer = (ByteBuffer) ((ByteBuffer) last.get() - .clear()).put(buffer).flip(); - return packet; - } - - // Non-claj packets are saved as raw buffer, to avoid re-serialization - return ((ByteBuffer) last.get().clear()).put((ByteBuffer) buffer.position(buffer.position() - 1)).flip(); - } - - @Override - public void write(ByteBuffer buffer, Object object) { - if (object instanceof ByteBuffer) { - buffer.put((ByteBuffer) object); - - } else if (object instanceof FrameworkMessage) { - buffer.put((byte) -2); // framework id - writeFramework(buffer, (FrameworkMessage) object); - - } else if (object instanceof Packets.Packet) { - Packets.Packet packet = (Packets.Packet) object; - buffer.put(Packets.id).put(Packets.getId(packet)); - packet.write(new ByteBufferOutput(buffer)); - if (packet instanceof Packets.ConnectionPacketWrapPacket) // This one is special - buffer.put(((Packets.ConnectionPacketWrapPacket) packet).buffer); - } - } - - public void writeFramework(ByteBuffer buffer, FrameworkMessage message) { - if (message instanceof Ping) { - Ping ping = (Ping) message; - buffer.put((byte) 0).putInt(ping.id).put(ping.isReply ? (byte) 1 : 0); - } else if (message instanceof DiscoverHost) - buffer.put((byte) 1); - else if (message instanceof KeepAlive) - buffer.put((byte) 2); - else if (message instanceof RegisterUDP) - buffer.put((byte) 3).putInt(((RegisterUDP) message).connectionID); - else if (message instanceof RegisterTCP) - buffer.put((byte) 4).putInt(((RegisterTCP) message).connectionID); - } - - public FrameworkMessage readFramework(ByteBuffer buffer) { - byte id = buffer.get(); - - if (id == 0) { - Ping p = new Ping(); - p.id = buffer.getInt(); - p.isReply = buffer.get() == 1; - return p; - } else if (id == 1) { - return FrameworkMessage.discoverHost; - } else if (id == 2) { - return FrameworkMessage.keepAlive; - } else if (id == 3) { - RegisterUDP p = new RegisterUDP(); - p.connectionID = buffer.getInt(); - return p; - } else if (id == 4) { - RegisterTCP p = new RegisterTCP(); - p.connectionID = buffer.getInt(); - return p; - } else { - throw new RuntimeException("Unknown framework message!"); - } - } - } -} diff --git a/src/playerconnect/PlayerConnect.java b/src/playerconnect/PlayerConnect.java deleted file mode 100644 index 70dfc17..0000000 --- a/src/playerconnect/PlayerConnect.java +++ /dev/null @@ -1,45 +0,0 @@ -package playerconnect; - -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -import arc.Events; -import arc.net.ArcNet; -import arc.util.Log; - -public class PlayerConnect { - public static final NetworkRelay relay = new NetworkRelay(); - public static final ExecutorService executor = Executors.newSingleThreadExecutor(); - public static final HttpServer httpServer = new HttpServer(); - - public static void main(String[] args) { - try { - ArcNet.errorHandler = (error) -> Log.err(error); - int port = Integer.parseInt(System.getenv("PLAYER_CONNECT_PORT")); - - if (port < 0 || port > 0xffff) - throw new RuntimeException("Invalid port range"); - // Init event loop - - relay.bind(port, port); - Events.fire(new PlayerConnectEvents.ServerLoadedEvent()); - Log.info("Server loaded and hosted on port @. Type @ for help.", port, "'help'"); - - } catch (Throwable t) { - Log.err("Failed to load server", t); - System.exit(1); - return; - } - - // Start the server - try { - relay.run(); - } catch (Throwable t) { - Log.err(t); - } finally { - relay.close(); - Log.info("Server closed."); - } - } - -} diff --git a/src/playerconnect/PlayerConnectEvents.java b/src/playerconnect/PlayerConnectEvents.java deleted file mode 100644 index f271ec9..0000000 --- a/src/playerconnect/PlayerConnectEvents.java +++ /dev/null @@ -1,102 +0,0 @@ -package playerconnect; - -import arc.net.Connection; -import arc.net.DcReason; -import playerconnect.shared.Packets; - -public class PlayerConnectEvents { - public static class ServerLoadedEvent { - } - - public static class ServerStoppingEvent { - } - - public static class ClientConnectedEvent { - public final Connection connection; - - public ClientConnectedEvent(Connection connection) { - this.connection = connection; - } - } - - /** - * @apiNote this event comes after {@link RoomClosedEvent} if the connection was - * the room host. - */ - public static class ClientDisconnectedEvent { - public final Connection connection; - public final DcReason reason; - /** not {@code null} if the client was in a room */ - public final ServerRoom room; - - public ClientDisconnectedEvent(Connection connection, DcReason reason, ServerRoom room) { - this.connection = connection; - this.reason = reason; - this.room = room; - } - } - - /** Currently the only reason is for packet spam. */ - public static class ClientKickedEvent { - public final Connection connection; - - public ClientKickedEvent(Connection connection) { - this.connection = connection; - } - } - - /** When a connection join a room */ - public static class ConnectionJoinedEvent { - public final Connection connection; - public final ServerRoom room; - - public ConnectionJoinedEvent(Connection connection, ServerRoom room) { - this.connection = connection; - this.room = room; - } - } - - public static class RoomCreatedEvent { - public final ServerRoom room; - - public RoomCreatedEvent(ServerRoom room) { - this.room = room; - } - } - - public static class RoomClosedEvent { - /** @apiNote the room is closed, so it cannot be used anymore. */ - public final ServerRoom room; - - public RoomClosedEvent(ServerRoom room) { - this.room = room; - } - } - - public static class RoomCreationRejectedEvent { - /** the connection that tried to create the room */ - public final Connection connection; - public final Packets.RoomClosedPacket.CloseReason reason; - - public RoomCreationRejectedEvent(Connection connection, Packets.RoomClosedPacket.CloseReason reason) { - this.connection = connection; - this.reason = reason; - } - } - - /** - * Defines an action tried by a connection but was not allowed to do it. - *

- * E.g. a client of the room tried to close it, or the host tried to join - * another room while hosting one. - */ - public static class ActionDeniedEvent { - public final Connection connection; - public final Packets.Message2Packet.MessageType reason; - - public ActionDeniedEvent(Connection connection, Packets.Message2Packet.MessageType reason) { - this.connection = connection; - this.reason = reason; - } - } -} diff --git a/src/playerconnect/ServerRoom.java b/src/playerconnect/ServerRoom.java deleted file mode 100644 index f26f80c..0000000 --- a/src/playerconnect/ServerRoom.java +++ /dev/null @@ -1,238 +0,0 @@ -package playerconnect; - -import java.nio.ByteBuffer; -import java.util.Date; -import java.util.UUID; - -import arc.net.Connection; -import arc.net.DcReason; -import arc.net.NetListener; -import arc.struct.IntMap; -import arc.util.Log; -import playerconnect.shared.Packets; - -public class ServerRoom implements NetListener { - - public final String id; - public final Connection host; - public final String password; - /** Using IntMap instead of Seq for faster search */ - public Packets.RoomStats stats; - public Long ping = -1L; - private boolean isClosed; - public final IntMap clients = new IntMap<>(); - public final Long createdAt = System.currentTimeMillis(); - public Long updatedAt = System.currentTimeMillis(); - - public ServerRoom(Connection host, String password, Packets.RoomStats stats) { - this.id = UUID.randomUUID().toString(); - this.host = host; - this.stats = stats; - this.password = password; - this.ping = new Date().getTime() - stats.createdAt; - } - - @Override - public void connected(Connection connection) { - if (isClosed) - return; - - Packets.ConnectionJoinPacket p = new Packets.ConnectionJoinPacket(); - p.connectionId = connection.getID(); - p.roomId = id; - host.sendTCP(p); - - clients.put(connection.getID(), connection); - } - - /** Alert the host that a client disconnected */ - @Override - public void disconnected(Connection connection, DcReason reason) { - if (isClosed) - return; - - if (connection == host) { - Log.info("Host disconnected, closing room: " + id); - close(); - return; - - } else if (host.isConnected()) { - try { - Packets.ConnectionClosedPacket p = new Packets.ConnectionClosedPacket(); - p.connectionId = connection.getID(); - p.reason = reason; - host.sendTCP(p); - } catch (Exception e) { - Log.err("Error while sending ConnectionClosedPacket to host: " + e.getMessage()); - } - } - - clients.remove(connection.getID()); - } - - /** Doesn't notify the room host about a disconnected client */ - public void disconnectedQuietly(Connection connection, DcReason reason) { - if (isClosed) - return; - - if (connection == host) { - Log.info("Host disconnected quietly, closing room: " + id); - close(); - } else { - clients.remove(connection.getID()); - } - } - - /** - * Wrap and re-send the packet to the host, if it come from a connection, - * else un-wrap and re-send the packet to the specified connection.
- * Only {@link Packets.ConnectionPacketWrapPacket} and {@link ByteBuffer} - * are allowed. - */ - @Override - public void received(Connection connection, Object object) { - if (isClosed) - return; - - if (connection == host) { - // Only claj packets are allowed in the host's connection - // and can only be ConnectionPacketWrapPacket at this point. - if (!(object instanceof Packets.ConnectionPacketWrapPacket)) - return; - - int connectionId = ((Packets.ConnectionPacketWrapPacket) object).connectionId; - Connection con = clients.get(connectionId); - - if (con != null && con.isConnected()) { - boolean tcp = ((Packets.ConnectionPacketWrapPacket) object).isTCP; - Object o = ((Packets.ConnectionPacketWrapPacket) object).buffer; - - if (tcp) - con.sendTCP(o); - else - con.sendUDP(o); - - // Notify that this connection doesn't exist, this case normally never happen - } else if (host.isConnected()) { - Packets.ConnectionClosedPacket p = new Packets.ConnectionClosedPacket(); - p.connectionId = connectionId; - p.reason = DcReason.error; - host.sendTCP(p); - } - - } else if (host.isConnected() && clients.containsKey(connection.getID())) { - // Only raw buffers are allowed here. - // We never send claj packets to anyone other than the room host, framework - // packets are ignored - // and mindustry packets are saved as raw buffer. - if (!(object instanceof ByteBuffer)) - return; - - Packets.ConnectionPacketWrapPacket p = new Packets.ConnectionPacketWrapPacket(); - p.connectionId = connection.getID(); - p.buffer = (ByteBuffer) object; - host.sendTCP(p); - - } - } - - /** Notify the host of an idle connection. */ - @Override - public void idle(Connection connection) { - if (isClosed) - return; - - if (connection == host) { - // Ignore if this is the room host - - } else if (host.isConnected() && clients.containsKey(connection.getID())) { - Packets.ConnectionIdlingPacket p = new Packets.ConnectionIdlingPacket(); - p.connectionId = connection.getID(); - host.sendTCP(p); - } - } - - /** Notify the room id to the host. Must be called once. */ - public void create() { - if (isClosed) - return; - - // Assume the host is still connected - Packets.RoomLinkPacket p = new Packets.RoomLinkPacket(); - p.roomId = id; - host.sendTCP(p); - } - - /** @return whether the room is isClosed or not */ - public boolean isClosed() { - return isClosed; - } - - public void close() { - close(Packets.RoomClosedPacket.CloseReason.closed); - } - - /** - * Closes the room and disconnects the host and all clients. - * The room object cannot be used anymore after this. - */ - public void close(Packets.RoomClosedPacket.CloseReason reason) { - if (isClosed) - return; - isClosed = true; // close before kicking connections, to avoid receiving events - try { - // Alert the close reason to the host - Packets.RoomClosedPacket p = new Packets.RoomClosedPacket(); - p.reason = reason; - host.sendTCP(p); - - host.close(DcReason.closed); - clients.values().forEach(c -> c.close(DcReason.closed)); - clients.clear(); - } catch (Exception e) { - Log.err("Error while closing room @: @", id, e); - } - - Log.info("Room @ closed, reason @", id, reason); - } - - /** Checks if the connection is the room host or one of his client */ - public boolean contains(Connection con) { - if (isClosed || con == null) - return false; - if (con == host) - return true; - return clients.containsKey(con.getID()); - } - - /** Send a message to the host and clients. */ - public void message(String text) { - if (isClosed) - return; - - // Just send to host, it will re-send it properly to all clients - Packets.MessagePacket p = new Packets.MessagePacket(); - p.message = text; - host.sendTCP(p); - } - - /** Send a message the host and clients. Will be translated by the room host. */ - public void message(Packets.Message2Packet.MessageType message) { - if (isClosed) - return; - - Packets.Message2Packet p = new Packets.Message2Packet(); - p.message = message; - host.sendTCP(p); - } - - /** Send a popup to the room host. */ - public void popup(String text) { - if (isClosed) - return; - - Packets.PopupPacket p = new Packets.PopupPacket(); - p.message = text; - host.sendTCP(p); - } -} diff --git a/src/playerconnect/Utils.java b/src/playerconnect/Utils.java deleted file mode 100644 index 864468a..0000000 --- a/src/playerconnect/Utils.java +++ /dev/null @@ -1,15 +0,0 @@ -package playerconnect; - -import arc.net.Connection; - -public class Utils { - - public static String getIP(Connection con) { - java.net.InetSocketAddress a = con.getRemoteAddressTCP(); - return a == null ? null : a.getAddress().getHostAddress(); - } - - public static String toString(Connection con) { - return "0x" + Integer.toHexString(con.getID()); - } -} From fa1f87f38d481b97c6723a1422a8601507ba5ac5 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Thu, 15 Jan 2026 16:57:26 +0700 Subject: [PATCH 002/115] chore: clean up gitignore by removing empty lines and comments Removed unnecessary empty lines and consolidated the /target ignore entry to make the file more concise and readable. --- .gitignore | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index da4e6cd..072f409 100644 --- a/.gitignore +++ b/.gitignore @@ -4,8 +4,4 @@ .project build/ bin/ - - -# Added by cargo - -/target +target From fbe2d73f34f0ab74e53de846e04857e8d032b513 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Thu, 15 Jan 2026 23:48:53 +0700 Subject: [PATCH 003/115] feat: implement PlayerConnect server with HTTP and proxy services Add initial implementation of PlayerConnect server with HTTP API and proxy services for game connections. Includes: - Configuration loading from environment variables - HTTP server with room listing and info endpoints - Proxy server handling TCP/UDP connections - State management for rooms and connections - Packet handling for game protocol - Basic logging and error handling The server provides a foundation for hosting multiplayer game sessions with room management and connection proxying. --- .github/workflows/rust-release.yml | 2 +- Cargo.lock | 1160 ++++++++++++++++++++++++++++ Cargo.toml | 27 +- dev.env | 2 + src/config.rs | 27 + src/http_server.rs | 127 +++ src/main.rs | 48 +- src/models.rs | 60 ++ src/packets.rs | 434 +++++++++++ src/proxy_server.rs | 368 +++++++++ src/state.rs | 42 + structure.md | 170 ++++ 12 files changed, 2463 insertions(+), 4 deletions(-) create mode 100644 dev.env create mode 100644 src/config.rs create mode 100644 src/http_server.rs create mode 100644 src/models.rs create mode 100644 src/packets.rs create mode 100644 src/proxy_server.rs create mode 100644 src/state.rs create mode 100644 structure.md diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index 958c865..f0cf1b1 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -2,7 +2,7 @@ name: Build and push to DockerHub on: push: branches: - - main + - v2 jobs: build: diff --git a/Cargo.lock b/Cargo.lock index f1b1e97..2d9b3ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,1166 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core", + "axum-macros", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "server" version = "0.1.0" +dependencies = [ + "anyhow", + "axum", + "bytes", + "dashmap", + "dotenvy", + "futures", + "serde", + "serde_json", + "serde_repr", + "thiserror", + "tokio", + "tokio-stream", + "tower-http", + "tracing", + "tracing-subscriber", + "uuid", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +dependencies = [ + "bitflags", + "bytes", + "http", + "http-body", + "http-body-util", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "uuid" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" +dependencies = [ + "getrandom", + "js-sys", + "rand", + "serde_core", + "uuid-macro-internal", + "wasm-bindgen", +] + +[[package]] +name = "uuid-macro-internal" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39d11901c36b3650df7acb0f9ebe624f35b5ac4e1922ecd3c57f444648429594" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "zerocopy" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea" diff --git a/Cargo.toml b/Cargo.toml index ff06670..ca12559 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,31 @@ [package] name = "server" version = "0.1.0" -edition = "2024" +edition = "2021" [dependencies] +tokio = { version = "1.0", features = ["full"] } +tokio-stream = { version = "0.1", features = ["sync"] } + +axum = { version = "0.7", features = ["macros"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +serde_repr = "0.1" + +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } + +dotenvy = "0.15" + +bytes = "1.0" +dashmap = "6.0" +uuid = { version = "1.6", features = [ + "v7", + "fast-rng", + "macro-diagnostics", + "serde", +] } +tower-http = { version = "0.5", features = ["cors", "trace"] } +futures = "0.3" +thiserror = "1.0" +anyhow = "1.0" diff --git a/dev.env b/dev.env new file mode 100644 index 0000000..2ecd159 --- /dev/null +++ b/dev.env @@ -0,0 +1,2 @@ +PLAYER_CONNECT_PORT=11010 +PLAYER_CONNECT_HTTP_PORT=11011 diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..d382e4f --- /dev/null +++ b/src/config.rs @@ -0,0 +1,27 @@ +use anyhow::Result; +use dotenvy::dotenv; +use std::env; + +pub struct Config { + pub player_connect_port: u16, + pub player_connect_http_port: u16, +} + +impl Config { + pub fn from_env() -> Result { + dotenv().ok(); + + let player_connect_port = env::var("PLAYER_CONNECT_PORT") + .unwrap_or_else(|_| "11010".to_string()) + .parse()?; + + let player_connect_http_port = env::var("PLAYER_CONNECT_HTTP_PORT") + .unwrap_or_else(|_| "11011".to_string()) + .parse()?; + + Ok(Config { + player_connect_port, + player_connect_http_port, + }) + } +} diff --git a/src/http_server.rs b/src/http_server.rs new file mode 100644 index 0000000..d66790c --- /dev/null +++ b/src/http_server.rs @@ -0,0 +1,127 @@ +use crate::state::{AppState, RoomUpdate}; +use axum::{ + extract::{Path, State}, + response::{sse::{Event, Sse}, Html, IntoResponse}, + routing::{get, post}, + Router, +}; +use futures::stream::Stream; +use std::sync::Arc; +use std::time::Duration; +use tokio_stream::wrappers::BroadcastStream; +use tokio_stream::StreamExt; +use tracing::info; + +pub async fn run(state: Arc, port: u16) -> anyhow::Result<()> { + let app = Router::new() + .route("/api/v1/ping", get(ping)) + .route("/api/v1/rooms", get(rooms_sse)) + .route("/:roomId", get(room_page)) + .route("/:roomId", post(room_port)) + .with_state(state); + + let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", port)).await?; + info!("HTTP Server listening on {}", port); + axum::serve(listener, app).await?; + Ok(()) +} + +async fn ping() -> impl IntoResponse { + "OK" +} + +async fn rooms_sse( + State(state): State>, +) -> Sse>> { + let rx = state.room_updates_tx.subscribe(); + let stream = BroadcastStream::new(rx); + + // Initial state: Send all current rooms + // We need to construct a stream that starts with current rooms and then follows updates + // For simplicity here, we just subscribe to updates. + // Ideally, we should send an initial "snapshot" event or individual add events. + + let stream = stream.map(|msg| { + match msg { + Ok(update) => { + let event = match update { + RoomUpdate::Update(room) => { + Event::default().event("update").json_data(room.stats) + } + RoomUpdate::Remove(id) => Ok({ + Event::default().event("remove").data(id) + }) + }; + event.map_err(|e| axum::BoxError::from(e)) + } + Err(e) => Err(axum::BoxError::from(e)), + } + }); + + Sse::new(stream).keep_alive(axum::response::sse::KeepAlive::new().interval(Duration::from_secs(10))) +} + +async fn room_page(Path(room_id): Path, State(state): State>) -> impl IntoResponse { + if let Some(room) = state.rooms.get(&room_id) { + let stats = &room.stats; + let html = format!(r#" + + + + + Room: {} + + + + + + + + + + + + + + + +

Room: {}

+
    +
  • Map: {}
  • +
  • Gamemode: {}
  • +
  • Players: {}
  • +
  • Locale: {}
  • +
  • Version: {}
  • +
  • Created At: {}
  • +
+ +"#, + stats.name, + stats.name, + stats.map_name, stats.gamemode, stats.players.len(), stats.version, + stats.map_name, + stats.gamemode, + stats.players.len(), + stats.locale, + stats.version, + stats.created_at, + stats.name, + stats.map_name, + stats.gamemode, + stats.players.len(), + stats.locale, + stats.version, + stats.created_at + ); + Html(html) + } else { + Html("

Room not found

".to_string()) + } +} + +async fn room_port(State(_state): State>) -> impl IntoResponse { + // Return the TCP port (which is in config, maybe we should store it in state or config) + // For now hardcode or get from config if we passed it. + // Ideally pass config to state. + "11010" // Using default, should be dynamic +} diff --git a/src/main.rs b/src/main.rs index e7a11a9..b2969bc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,47 @@ -fn main() { - println!("Hello, world!"); +mod config; +mod models; +mod packets; +mod state; +mod proxy_server; +mod http_server; + +use crate::config::Config; +use crate::state::AppState; +use std::sync::Arc; +use tracing::{info, Level}; +use tracing_subscriber::FmtSubscriber; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // Initialize logging + let subscriber = FmtSubscriber::builder() + .with_max_level(Level::INFO) + .finish(); + tracing::subscriber::set_global_default(subscriber) + .expect("setting default subscriber failed"); + + // Load config + let config = Config::from_env()?; + info!("Starting PlayerConnect Server V2..."); + info!("TCP Port: {}", config.player_connect_port); + info!("HTTP Port: {}", config.player_connect_http_port); + + // Initialize state + let state = Arc::new(AppState::new()); + + // Start Proxy Server + let proxy_state = state.clone(); + let proxy_port = config.player_connect_port; + tokio::spawn(async move { + if let Err(e) = proxy_server::run(proxy_state, proxy_port).await { + tracing::error!("Proxy server error: {}", e); + } + }); + + // Start HTTP Server + let http_state = state.clone(); + let http_port = config.player_connect_http_port; + http_server::run(http_state, http_port).await?; + + Ok(()) } diff --git a/src/models.rs b/src/models.rs new file mode 100644 index 0000000..b653efe --- /dev/null +++ b/src/models.rs @@ -0,0 +1,60 @@ +use serde::{Deserialize, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Room { + pub id: String, + #[serde(skip)] + pub host_connection_id: i32, // Internal reference + #[serde(skip_serializing_if = "Option::is_none")] + pub password: Option, + pub ping: i64, + #[serde(rename = "isClosed")] + pub is_closed: bool, + #[serde(rename = "createdAt")] + pub created_at: i64, + #[serde(rename = "updatedAt")] + pub updated_at: i64, + #[serde(skip)] + pub clients: Vec, // List of connection IDs + pub stats: Stats, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Stats { + pub players: Vec, + #[serde(rename = "mapName")] + pub map_name: String, + pub name: String, + pub gamemode: String, + pub mods: Vec, + pub locale: String, + pub version: String, + #[serde(rename = "createdAt")] + pub created_at: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Player { + pub name: String, + pub locale: String, +} + +#[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr, PartialEq)] +#[repr(u8)] +pub enum CloseReason { + Closed = 0, + ObsoleteClient = 1, + OutdatedVersion = 2, + ServerClosed = 3, +} + +#[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr, PartialEq)] +#[repr(u8)] +pub enum MessageType { + ServerClosing = 0, + PacketSpamming = 1, + AlreadyHosting = 2, + RoomClosureDenied = 3, + ConClosureDenied = 4, +} diff --git a/src/packets.rs b/src/packets.rs new file mode 100644 index 0000000..b626952 --- /dev/null +++ b/src/packets.rs @@ -0,0 +1,434 @@ +use crate::models::{CloseReason, MessageType, Stats as RoomStats}; +use anyhow::{anyhow, Result}; +use bytes::{Buf, BufMut, Bytes, BytesMut}; +use std::{io::Cursor, time::Instant}; +use tracing::info; + +// TODO: Verify this ID. +pub const PACKET_GROUP_ID: i8 = -4; + +pub trait Packet: Sized { + fn id(&self) -> u8; + fn read(buf: &mut Cursor<&[u8]>) -> Result; + fn write(&self, buf: &mut BytesMut); +} + +#[derive(Debug)] +pub enum AnyPacket { + Framework(FrameworkMessage), + App(AppPacket), + Raw(Bytes), +} + +#[derive(Debug)] +pub enum FrameworkMessage { + Ping { id: i32, is_reply: bool }, + DiscoverHost, + KeepAlive, + RegisterUDP { connection_id: i32 }, + RegisterTCP { connection_id: i32 }, +} + +#[derive(Debug)] +pub enum AppPacket { + ConnectionPacketWrap(ConnectionPacketWrapPacket), + ConnectionClosed(ConnectionClosedPacket), + ConnectionJoin(ConnectionJoinPacket), + ConnectionIdling(ConnectionIdlingPacket), + RoomLink(RoomLinkPacket), + RoomJoin(RoomJoinPacket), + RoomClosureRequest(RoomClosureRequestPacket), + RoomClosed(RoomClosedPacket), + RoomCreationRequest(RoomCreationRequestPacket), + Message(MessagePacket), + Popup(PopupPacket), + Message2(Message2Packet), + Stats(StatsPacket), +} + +// Packet Struct Definitions with IDs +// Assuming IDs are sequential based on structure.md order: +// 0: ConnectionWrapperPacket (Abstract base, maybe not instantiated directly?) +// 1: ConnectionPacketWrapPacket +// 2: ConnectionClosedPacket +// 3: ConnectionJoinPacket +// 4: ConnectionIdlingPacket +// 5: RoomLinkPacket +// 6: RoomJoinPacket +// 7: RoomClosureRequestPacket +// 8: RoomClosedPacket +// 9: RoomCreationRequestPacket +// 10: MessagePacket +// 11: PopupPacket +// 12: Message2Packet +// 13: StatsPacket + +#[derive(Debug)] +pub struct ConnectionWrapperPacket { + pub connection_id: i32, +} + +#[derive(Debug)] +pub struct ConnectionPacketWrapPacket { + pub connection_id: i32, + pub is_tcp: bool, + pub buffer: Bytes, +} + +#[derive(Debug)] +pub struct ConnectionClosedPacket { + pub connection_id: i32, + pub reason: u8, // Using u8 ordinal directly, or could map to CloseReason if applicable? + // structure.md says "DcReason" which is an arc.net enum. + // Usually: 0=closed, 1=timeout, 2=error. +} + +#[derive(Debug)] +pub struct ConnectionJoinPacket { + pub connection_id: i32, + pub room_id: String, +} + +#[derive(Debug)] +pub struct ConnectionIdlingPacket { + pub connection_id: i32, +} + +#[derive(Debug)] +pub struct RoomLinkPacket { + pub room_id: String, +} + +#[derive(Debug)] +pub struct RoomJoinPacket { + pub room_id: String, + pub password: String, +} + +#[derive(Debug)] +pub struct RoomClosureRequestPacket; + +#[derive(Debug)] +pub struct RoomClosedPacket { + pub reason: CloseReason, +} + +#[derive(Debug)] +pub struct RoomCreationRequestPacket { + pub version: String, + pub password: String, + pub data: RoomStats, +} + +#[derive(Debug)] +pub struct MessagePacket { + pub message: String, +} + +#[derive(Debug)] +pub struct PopupPacket { + pub message: String, +} + +#[derive(Debug)] +pub struct Message2Packet { + pub message: MessageType, +} + +#[derive(Debug)] +pub struct StatsPacket { + pub room_id: String, + pub data: RoomStats, +} + +// Helper to read/write strings +fn read_string(buf: &mut Cursor<&[u8]>) -> Result { + if buf.remaining() < 2 { + return Err(anyhow!("Not enough bytes for string length: {}", buf.remaining())); + } + + let len = buf.get_u16() as usize; + + if buf.remaining() < len { + return Err(anyhow!("Not enough bytes for string content, expected {}, got {}", len, buf.remaining())); + } + + let mut bytes = vec![0u8; len]; + + buf.copy_to_slice(&mut bytes); + + String::from_utf8(bytes).map_err(|e| anyhow!(e)) +} + +fn write_string(buf: &mut BytesMut, s: &str) { + let bytes = s.as_bytes(); + buf.put_u16(bytes.len() as u16); + buf.put_slice(bytes); +} + +// Helper to read/write RoomStats (JSON serialization as string?) +// In Java NetworkRelay: +// room.stats = statsPacket.data; +// It seems it's serialized as an object. +// structure.md says "non-primitive (object)". +// In ArcNet, objects are serialized using generic serialization. +// Since we are rewriting in Rust, and we don't have the full Java serialization context, +// we might assume it's JSON string or specific binary format. +// However, looking at models.rs, Stats has strings and arrays. +// Let's assume for now it's serialized as a JSON string for simplicity in Rust implementation, +// OR we implement binary serialization matching the struct fields. +// Given "non-primitive (object)" usually means Kryo/Java serialization in Mindustry context, +// but for PlayerConnect it might be simpler. +// Let's assume binary serialization of fields in order. + +fn read_stats(buf: &mut Cursor<&[u8]>) -> Result { + // This is a guess. We need to match Java side. + // Stats { players, mapName, name, gamemode, mods, locale, version, createdAt } + + let json = read_string(buf)?; + info!("Stats JSON: {}", json); + + // let data = serde_json::from_str(json.as_str()); + + Ok(RoomStats{ + players: vec![], + map_name: String::new(), + name: String::new(), + gamemode: String::new(), + mods: vec![], + locale: String::new(), + version: String::new(), + created_at: 0, + }) +} + +fn write_stats(buf: &mut BytesMut, stats: &RoomStats) { + buf.put_u8(stats.players.len() as u8); + for p in &stats.players { + write_string(buf, &p.name); + write_string(buf, &p.locale); + } + write_string(buf, &stats.map_name); + write_string(buf, &stats.name); + write_string(buf, &stats.gamemode); + // write_string(buf, &stats.mods); // TODO + write_string(buf, &stats.locale); + write_string(buf, &stats.version); + buf.put_i64(stats.created_at); +} + +impl AnyPacket { + pub fn read(mut buf: &mut Cursor<&[u8]>) -> Result { + if !buf.has_remaining() { + return Err(anyhow!("Empty packet")); + } + + let id = buf.get_i8(); + + if id == -2 { + // Framework Message + let fid = buf.get_u8(); + match fid { + 0 => Ok(AnyPacket::Framework(FrameworkMessage::Ping { + id: buf.get_i32(), + is_reply: buf.get_u8() == 1, + })), + 1 => Ok(AnyPacket::Framework(FrameworkMessage::DiscoverHost)), + 2 => Ok(AnyPacket::Framework(FrameworkMessage::KeepAlive)), + 3 => Ok(AnyPacket::Framework(FrameworkMessage::RegisterUDP { + connection_id: buf.get_i32(), + })), + 4 => Ok(AnyPacket::Framework(FrameworkMessage::RegisterTCP { + connection_id: buf.get_i32(), + })), + _ => Err(anyhow!("Unknown Framework ID: {}", fid)), + } + } else if id == PACKET_GROUP_ID { + // App Packet + let pid = buf.get_u8(); + + match pid { + 0 => Ok(AnyPacket::App(AppPacket::ConnectionPacketWrap( + ConnectionPacketWrapPacket { + connection_id: buf.get_i32(), + is_tcp: buf.get_u8() == 1, + buffer: { + let len = buf.get_i32() as usize; // Buffer length? + // Actually java side: buffer is object. + // If it's just raw bytes, we need length. + // Let's assume remaining or prefixed length. + // Usually `write(buffer, object)` writes generic object. + // If it's ByteBuffer, it writes raw bytes? + // NetworkProxy: ((Packets.ConnectionPacketWrapPacket) packet).buffer = (ByteBuffer) ((ByteBuffer) last.get().clear()).put(buffer).flip(); + // It seems it captures the rest of the buffer? + let mut b = BytesMut::new(); + while buf.has_remaining() { + b.put_u8(buf.get_u8()); + } + b.freeze() + }, + }, + ))), + 1 => Ok(AnyPacket::App(AppPacket::ConnectionClosed( + ConnectionClosedPacket { + connection_id: buf.get_i32(), + reason: buf.get_u8(), // DcReason ordinal + }, + ))), + 2 => Ok(AnyPacket::App(AppPacket::ConnectionJoin( + ConnectionJoinPacket { + connection_id: buf.get_i32(), + room_id: read_string(&mut buf)?, + }, + ))), + 3 => Ok(AnyPacket::App(AppPacket::ConnectionIdling( + ConnectionIdlingPacket { + connection_id: buf.get_i32(), + }, + ))), + 4 => Ok(AnyPacket::App(AppPacket::RoomCreationRequest( + RoomCreationRequestPacket { + version: read_string(&mut buf)?, + password: read_string(&mut buf)?, + data: read_stats(&mut buf)?, + }, + ))), + 5 => Ok(AnyPacket::App(AppPacket::RoomClosureRequest( + RoomClosureRequestPacket, + ))), + 6 => Ok(AnyPacket::App(AppPacket::RoomClosed(RoomClosedPacket { + reason: unsafe { std::mem::transmute(buf.get_u8()) }, // Unsafe or impl TryFrom + }))), + 7 => Ok(AnyPacket::App(AppPacket::RoomLink(RoomLinkPacket { + room_id: read_string(&mut buf)?, + }))), + 8 => Ok(AnyPacket::App(AppPacket::RoomJoin(RoomJoinPacket { + room_id: read_string(&mut buf)?, + password: read_string(&mut buf)?, + }))), + 9 => Ok(AnyPacket::App(AppPacket::Message(MessagePacket { + message: read_string(&mut buf)?, + }))), + 10 => Ok(AnyPacket::App(AppPacket::Message2(Message2Packet { + message: unsafe { std::mem::transmute(buf.get_u8()) }, + }))), + 11 => Ok(AnyPacket::App(AppPacket::Popup(PopupPacket { + message: read_string(&mut buf)?, + }))), + 12 => Ok(AnyPacket::App(AppPacket::Stats(StatsPacket { + room_id: read_string(&mut buf)?, + data: read_stats(&mut buf)?, + }))), + _ => Err(anyhow!("Unknown App Packet ID: {}", pid)), + } + } else { + // Raw Packet + buf.set_position(buf.position() - 1); + let remaining = buf.remaining(); + let mut bytes = BytesMut::with_capacity(remaining); + bytes.put(buf); + Ok(AnyPacket::Raw(bytes.freeze())) + } + } + + pub fn write(&self, out: &mut BytesMut) { + let mut payload = BytesMut::new(); + + match self { + AnyPacket::Framework(msg) => { + payload.put_i8(-2); + + match msg { + FrameworkMessage::Ping { id, is_reply } => { + payload.put_u8(0); + payload.put_i32(*id); + payload.put_u8(if *is_reply { 1 } else { 0 }); + } + FrameworkMessage::DiscoverHost => payload.put_u8(1), + FrameworkMessage::KeepAlive => payload.put_u8(2), + FrameworkMessage::RegisterUDP { connection_id } => { + payload.put_u8(3); + payload.put_i32(*connection_id); + } + FrameworkMessage::RegisterTCP { connection_id } => { + payload.put_u8(4); + payload.put_i32(*connection_id); + } + } + } + AnyPacket::App(pkt) => { + payload.put_i8(PACKET_GROUP_ID as i8); + match pkt { + AppPacket::ConnectionPacketWrap(p) => { + payload.put_u8(0); + payload.put_i32(p.connection_id); + payload.put_u8(if p.is_tcp { 1 } else { 0 }); + // Write payloadfer? + // payload.put_slice(&p.payloadfer); + } + AppPacket::ConnectionClosed(p) => { + payload.put_u8(1); + payload.put_i32(p.connection_id); + payload.put_u8(p.reason); + } + AppPacket::ConnectionJoin(p) => { + payload.put_u8(2); + payload.put_i32(p.connection_id); + write_string(&mut payload, &p.room_id); + } + AppPacket::ConnectionIdling(p) => { + payload.put_u8(3); + payload.put_i32(p.connection_id); + } + AppPacket::RoomCreationRequest(p) => { + payload.put_u8(4); + write_string(&mut payload, &p.version); + write_string(&mut payload, &p.password); + write_stats(&mut payload, &p.data); + } + AppPacket::RoomClosureRequest(_) => { + payload.put_u8(5); + } + AppPacket::RoomClosed(p) => { + payload.put_u8(6); + payload.put_u8(p.reason as u8); + } + AppPacket::RoomLink(p) => { + payload.put_u8(7); + write_string(&mut payload, &p.room_id); + } + AppPacket::RoomJoin(p) => { + payload.put_u8(8); + write_string(&mut payload, &p.room_id); + write_string(&mut payload, &p.password); + } + AppPacket::Message(p) => { + payload.put_u8(9); + write_string(&mut payload, &p.message); + } + AppPacket::Message2(p) => { + payload.put_u8(10); + payload.put_u8(p.message as u8); + } + AppPacket::Popup(p) => { + payload.put_u8(11); + write_string(&mut payload, &p.message); + } + AppPacket::Stats(p) => { + payload.put_u8(12); + write_string(&mut payload, &p.room_id); + write_stats(&mut payload, &p.data); + } + } + } + AnyPacket::Raw(bytes) => { + payload.put_slice(bytes); + } + } + + info!("Sending packet: {:?} with len: {}", self, payload.len()); + + out.put_u16(payload.len() as u16); + out.extend_from_slice(&payload); + } +} diff --git a/src/proxy_server.rs b/src/proxy_server.rs new file mode 100644 index 0000000..380727f --- /dev/null +++ b/src/proxy_server.rs @@ -0,0 +1,368 @@ +use crate::packets::{ + AnyPacket, AppPacket, FrameworkMessage, RoomCreationRequestPacket, RoomLinkPacket, +}; +use crate::state::{AppState, ConnectionState}; +use bytes::{Buf, BytesMut}; +use std::io::Cursor; +use std::net::SocketAddr; +use std::sync::atomic::{AtomicI32, Ordering}; +use std::sync::Arc; +use std::time::{Duration, Instant}; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::{TcpListener, UdpSocket}; +use tokio::sync::mpsc; +use tracing::{error, info, warn}; +use uuid::{uuid, Uuid}; + +static NEXT_CONNECTION_ID: AtomicI32 = AtomicI32::new(1); + +const BUFFER_SIZE: usize = 32768; +const CONNECTION_TIME_OUT: u64 = 10000; // In ms +const KEEP_ALIVE_INTERVAL: u64 = 8000; // In +const PACKET_LENGTH_LENGTH: usize = 2; + +pub async fn run(state: Arc, port: u16) -> anyhow::Result<()> { + let address = format!("0.0.0.0:{}", port); + + let listener = TcpListener::bind(address.clone()).await?; + let udp_socket = Arc::new(UdpSocket::bind(address).await?); + + info!("Proxy Server listening on TCP/UDP {}", port); + + let udp_state = state.clone(); + let udp_socket_clone = udp_socket.clone(); + + // UDP Listener Loop + tokio::spawn(async move { + let mut buf = [0u8; 4096]; + loop { + match udp_socket_clone.recv_from(&mut buf).await { + Ok((len, addr)) => { + let data = &buf[..len]; + if let Err(e) = handle_udp_packet(&udp_state, data, addr).await { + error!("UDP Error from {}: {}", addr, e); + } + } + Err(e) => { + error!("UDP Receive Error: {}", e); + } + } + } + }); + + loop { + let (socket, addr) = listener.accept().await?; + info!("New connection from {}", addr); + let state = state.clone(); + + tokio::spawn(async move { + if let Err(e) = handle_connection(state, socket).await { + error!("Connection error: {}", e); + } + }); + } +} + +async fn handle_udp_packet( + state: &Arc, + data: &[u8], + addr: SocketAddr, +) -> anyhow::Result<()> { + // Basic packet structure check: Length (2 bytes) + Payload + + info!("UDP: Received {} bytes from {}", data.len(), addr); + + // if data.len() < PACKET_LENGTH_LENGTH { + // return Ok(()); + // } + + // let mut cursor = Cursor::new(data); + // let len = cursor.get_u16() as usize; + + // if data.len() < PACKET_LENGTH_LENGTH + len { + // warn!("Invalid packet length {} from {}", len, addr); + // return Ok(()); + // } + + // let payload = &data[PACKET_LENGTH_LENGTH..PACKET_LENGTH_LENGTH ]; + let mut payload_cursor = Cursor::new(data); + let packet = AnyPacket::read(&mut payload_cursor); + + match packet { + Ok(AnyPacket::Framework(FrameworkMessage::RegisterUDP { connection_id })) => { + let tx = if let Some(mut conn) = state.connections.get_mut(&connection_id) { + info!( + "Registering UDP for connection {} at {}", + connection_id, addr + ); + conn.udp_addr = Some(addr); + + Some(conn.tx.clone()) + } else { + info!( + "Received RegisterUDP for unknown connection {}", + connection_id + ); + None + }; + + match tx { + Some(tx) => { + // Send RegisterUDP reply via TCP + let reply = + AnyPacket::Framework(FrameworkMessage::RegisterUDP { connection_id }); + + let mut buf = BytesMut::new(); + + reply.write(&mut buf); + + if let Err(e) = tx.send(buf.freeze()).await { + error!("Failed to send RegisterUDP reply via TCP: {}", e); + } else { + if let Some(mut conn) = state.connections.get_mut(&connection_id) { + conn.last_write_time = Instant::now(); + } + } + } + None => { + warn!( + "Received RegisterUDP for unknown connection {}", + connection_id + ); + } + } + } + Ok(packet) => { + // Handle other UDP packets if necessary (e.g. game data) + // For now, we only care about registration or forwarding + info!("UDP: Unhandled packet {:?}", packet); + } + Err(e) => { + error!("Error parsing UDP packet from {}: {}", addr, e); + } + } + Ok(()) +} + +async fn handle_connection( + state: Arc, + socket: tokio::net::TcpStream, +) -> anyhow::Result<()> { + let connection_id = NEXT_CONNECTION_ID.fetch_add(1, Ordering::Relaxed); + + let (tx, mut rx) = mpsc::channel(100); + + // Register connection + state.connections.insert( + connection_id, + ConnectionState { + id: connection_id, + room_id: None, + is_host: false, + tx: tx.clone(), + udp_addr: None, + last_write_time: Instant::now(), + }, + ); + + info!("Connection {} registered", connection_id); + + // Split socket + let (mut reader, mut writer) = socket.into_split(); + + // Spawn Write Loop + let write_handle = tokio::spawn(async move { + while let Some(bytes) = rx.recv().await { + if let Err(e) = writer.write_all(&bytes).await { + error!("Write error: {}", e); + break; + } + } + }); + + // Send RegisterTCP to client + let mut write_buf: BytesMut = BytesMut::new(); + let register_packet = AnyPacket::Framework(FrameworkMessage::RegisterTCP { connection_id }); + + register_packet.write(&mut write_buf); + + if let Err(e) = tx.send(write_buf.freeze()).await { + error!("Failed to send RegisterTCP: {}", e); + return Ok(()); + } + + // Read Loop + let mut buf = BytesMut::with_capacity(BUFFER_SIZE); + let mut tmp_buf = [0u8; BUFFER_SIZE]; + let mut tick_interval = tokio::time::interval(Duration::from_millis(KEEP_ALIVE_INTERVAL)); + let mut last_read_time = Instant::now(); + + loop { + tokio::select! { + read_result = reader.read(&mut tmp_buf) => { + match read_result { + Ok(n) if n == 0 => { + info!("Connection {} closed by peer", connection_id); + break; + } + Ok(n) => { + last_read_time = Instant::now(); + buf.extend_from_slice(&tmp_buf[..n]); + + loop { + info!("TCP: Received {} bytes from connection {}", n, connection_id); + + if buf.len() < PACKET_LENGTH_LENGTH { + break; + } + + // Peek length + let len = { + let mut cur = Cursor::new(&buf[..]); + cur.get_u16() as usize + }; + + if buf.len() < PACKET_LENGTH_LENGTH + len { + break; + } + + buf.advance(PACKET_LENGTH_LENGTH); + + // Extract payload + let payload = buf.split_to(len); + let mut cursor = Cursor::new(&payload[..]); + + match AnyPacket::read(&mut cursor) { + Ok(packet) => { + info!("TCP: Received packet {:?} from connection {}", packet, connection_id); + + if let Err(e) = handle_packet(&state, connection_id, packet).await { + error!("Error handling packet for connection {}: {}", connection_id, e); + // Depending on severity, we might want to close connection + // For now, log and continue + } + } + Err(e) => { + error!("Error parsing packet for connection {} with size {}: {}", connection_id,len, e); + // Packet is consumed and discarded + } + } + } + } + Err(e) => { + error!("Read error for connection {}: {}", connection_id, e); + break; + } + } + } + _ = tick_interval.tick() => { + if last_read_time.elapsed() > Duration::from_millis(CONNECTION_TIME_OUT) { + info!("Connection {} timed out", connection_id); + break; + } + + let should_send = if let Some(conn) = state.connections.get(&connection_id) { + conn.last_write_time.elapsed() > Duration::from_secs(8) + } else { + break; + }; + + if should_send { + let mut buf = BytesMut::new(); + let packet = AnyPacket::Framework(FrameworkMessage::KeepAlive); + packet.write(&mut buf); + if tx.send(buf.freeze()).await.is_err() { + break; + } + if let Some(mut conn) = state.connections.get_mut(&connection_id) { + conn.last_write_time = Instant::now(); + } + } + } + } + } + + // Cleanup + state.connections.remove(&connection_id); + write_handle.abort(); + info!("Connection {} cleanup complete", connection_id); + Ok(()) +} + +async fn handle_packet( + state: &Arc, + connection_id: i32, + packet: AnyPacket, +) -> anyhow::Result<()> { + match packet { + AnyPacket::Framework(msg) => match msg { + FrameworkMessage::Ping { id, is_reply } => { + if !is_reply { + let pong = AnyPacket::Framework(FrameworkMessage::Ping { id, is_reply: true }); + send_packet(state, connection_id, pong).await; + } + } + FrameworkMessage::KeepAlive => {} + _ => {} + }, + AnyPacket::App(app_msg) => { + handle_app_packet(state, connection_id, app_msg).await?; + } + AnyPacket::Raw(_) => todo!("Handle raw packet {:?}", packet), + } + Ok(()) +} + +async fn handle_app_packet( + state: &Arc, + connection_id: i32, + packet: AppPacket, +) -> anyhow::Result<()> { + match packet { + AppPacket::RoomJoin(pkt) => { + info!("Connection {} joining room {}", connection_id, pkt.room_id); + if let Some(mut conn) = state.connections.get_mut(&connection_id) { + conn.room_id = Some(pkt.room_id.clone()); + } + } + AppPacket::Message(pkt) => { + info!("Connection {} sent message: {}", connection_id, pkt.message); + } + AppPacket::RoomCreationRequest(RoomCreationRequestPacket { + version, + password, + data, + }) => { + send_packet( + state, + connection_id, + AnyPacket::App(AppPacket::RoomLink(RoomLinkPacket { + room_id: Uuid::now_v7().to_string(), + })), + ) + .await; + } + _ => todo!("Handle app packet {:?}", packet), + } + Ok(()) +} + +async fn send_packet(state: &Arc, connection_id: i32, packet: AnyPacket) { + let tx = if let Some(conn) = state.connections.get(&connection_id) { + Some(conn.tx.clone()) + } else { + None + }; + + if let Some(tx) = tx { + let mut buf = BytesMut::new(); + packet.write(&mut buf); + if let Ok(_) = tx.send(buf.freeze()).await { + if let Some(mut conn) = state.connections.get_mut(&connection_id) { + conn.last_write_time = Instant::now(); + } + } + } else { + error!("No connection found for ID {}", connection_id); + } +} diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..50848c9 --- /dev/null +++ b/src/state.rs @@ -0,0 +1,42 @@ +use crate::models::Room; +use bytes::Bytes; +use dashmap::DashMap; +use std::net::SocketAddr; +use std::time::Instant; +use tokio::sync::mpsc; + +pub struct AppState { + pub rooms: DashMap, + pub connections: DashMap, + // Maps connection ID to its queue of packets waiting for room join + pub packet_queue: DashMap>, + // Channel to broadcast room updates to SSE (sender) + pub room_updates_tx: tokio::sync::broadcast::Sender, +} + +#[derive(Clone, Debug)] +pub enum RoomUpdate { + Update(Room), + Remove(String), +} + +pub struct ConnectionState { + pub id: i32, + pub room_id: Option, + pub is_host: bool, + pub tx: mpsc::Sender, // To send data back to the TCP connection + pub udp_addr: Option, + pub last_write_time: Instant, +} + +impl AppState { + pub fn new() -> Self { + let (tx, _) = tokio::sync::broadcast::channel(100); + Self { + rooms: DashMap::new(), + connections: DashMap::new(), + packet_queue: DashMap::new(), + room_updates_tx: tx, + } + } +} diff --git a/structure.md b/structure.md new file mode 100644 index 0000000..867c5bc --- /dev/null +++ b/structure.md @@ -0,0 +1,170 @@ +# This is a complete rewrite of player connect protocol (a protocol base on claj) in Rust + +## Purpose + +- Improve the performance of the player connect server +- Make the player connect server more readable, extendable +- Easier to moderate + +## Structure + +### App + +Handle main loop and stuff + +### Http Server + +Handle all related http stuff + +### Proxy Server + +Handle proxy all game packet + +### Event Bus + +Boardcast event between all components in system + +## Data Model + +Room { + id + host_connection + password (optional) + ping: number + isClosed: boolean + createdAt: number + updatedAt: number + clients: list of Connection + stats: Stats +} + +Stats { + players: list of Player + mapName: string + name: string + gamemode:string + mods: string[] + locale: string + version: string + createdAt: number +} + +Player { + name: string + locale: string +} + +ConnectionWrapperPacket { + connectionId: int +} + +ConnectionPacketWrapPacket (extends ConnectionWrapperPacket) +ConnectionPacketWrapPacket { + object: Object // non-primitive (generic object payload) + buffer: ByteBuffer // non-primitive (java.nio buffer) + isTCP: boolean +} + +ConnectionClosedPacket (extends ConnectionWrapperPacket) +ConnectionClosedPacket { + reason: DcReason // non-primitive (enum) +} + +ConnectionJoinPacket (extends ConnectionWrapperPacket) +ConnectionJoinPacket { + roomId: String // non-primitive +} + +ConnectionIdlingPacket (extends ConnectionWrapperPacket) +ConnectionIdlingPacket { + // no additional fields +} + +RoomLinkPacket (extends Packet) +RoomLinkPacket { + roomId: String // non-primitive +} + +RoomJoinPacket (extends RoomLinkPacket) +RoomJoinPacket { + password: String // non-primitive +} + +RoomClosureRequestPacket (extends Packet) +RoomClosureRequestPacket { + // no fields +} + +RoomClosedPacket (extends Packet) +RoomClosedPacket { + reason: CloseReason // non-primitive (enum) +} + +RoomCreationRequestPacket (extends Packet) +RoomCreationRequestPacket { + version: String // non-primitive + password: String // non-primitive + data: RoomStats // non-primitive (object) +} + +MessagePacket (extends Packet) +MessagePacket { + message: String // non-primitive +} + +PopupPacket (extends MessagePacket) +PopupPacket { + // inherits message +} + +Message2Packet (extends Packet) +Message2Packet { + message: MessageType // non-primitive (enum) +} + +StatsPacket (extends Packet) +StatsPacket { + roomId: String // non-primitive + data: Stats // non-primitive (object) +} + +Enums +enum MessageType { + serverClosing + packetSpamming + alreadyHosting + roomClosureDenied + conClosureDenied +} + +enum CloseReason { + closed + obsoleteClient + outdatedVersion + serverClosed +} + +## Flow + +### App + +- Read env PLAYER_CONNECT_PORT, PLAYER_CONNECT_HTTP_PORT from env, validate +- Do initiation, logging, create HTTP server, bind to PLAYER_CONNECT_HTTP_PORT, create proxy server +- Start main loop + +### Http Server + +- Setup server +- Create routes: ++ GET /api/v1/rooms is a server sent event route that: + - On connected: send all rooms to client as and "update" server sent event + - Listen to any room changes broadcast by proxy server and send as "update" server sent event + - Listen to room deletion by proxy server and send an "remove" server sent event + ++ GET /api/v1/ping: return 200 OK ++ GET /{roomId}: + - Return a html page that show room info with metadata and opengraph ++ POST /{roomId}: + - Return PLAYER_CONNECT_PORT + +### Proxy Server From 143695032956f4f0fb39ec2230a3f680496843b4 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Fri, 16 Jan 2026 13:53:19 +0700 Subject: [PATCH 004/115] feat(packets): implement packet serialization and add ArcCloseReason Add ArcCloseReason enum to models and implement packet serialization logic in packets.rs. The changes include reading/writing packet data, handling different packet types, and adding helper functions for string and stats serialization. --- src/models.rs | 19 ++ src/packets.rs | 535 +++++++++++++++++++++++-------------------------- 2 files changed, 272 insertions(+), 282 deletions(-) diff --git a/src/models.rs b/src/models.rs index b653efe..c340746 100644 --- a/src/models.rs +++ b/src/models.rs @@ -49,6 +49,25 @@ pub enum CloseReason { ServerClosed = 3, } +#[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr, PartialEq)] +#[repr(u8)] +pub enum ArcCloseReason { + Closed = 0, + Timeout = 1, + Error = 2, +} + +impl ArcCloseReason { + pub fn from(value: u8) -> Self { + match value { + 0 => ArcCloseReason::Closed, + 1 => ArcCloseReason::Timeout, + 2 => ArcCloseReason::Error, + _ => panic!("Invalid ArcCloseReason number"), + } + } +} + #[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr, PartialEq)] #[repr(u8)] pub enum MessageType { diff --git a/src/packets.rs b/src/packets.rs index b626952..86bacdd 100644 --- a/src/packets.rs +++ b/src/packets.rs @@ -1,17 +1,11 @@ -use crate::models::{CloseReason, MessageType, Stats as RoomStats}; +use crate::models::{ArcCloseReason, CloseReason, MessageType, Stats}; use anyhow::{anyhow, Result}; use bytes::{Buf, BufMut, Bytes, BytesMut}; -use std::{io::Cursor, time::Instant}; +use std::io::Cursor; use tracing::info; -// TODO: Verify this ID. -pub const PACKET_GROUP_ID: i8 = -4; - -pub trait Packet: Sized { - fn id(&self) -> u8; - fn read(buf: &mut Cursor<&[u8]>) -> Result; - fn write(&self, buf: &mut BytesMut); -} +pub const APP_PACKET_ID: i8 = -4; +pub const FRAMEWORK_PACKET_ID: i8 = -2; #[derive(Debug)] pub enum AnyPacket { @@ -46,28 +40,6 @@ pub enum AppPacket { Stats(StatsPacket), } -// Packet Struct Definitions with IDs -// Assuming IDs are sequential based on structure.md order: -// 0: ConnectionWrapperPacket (Abstract base, maybe not instantiated directly?) -// 1: ConnectionPacketWrapPacket -// 2: ConnectionClosedPacket -// 3: ConnectionJoinPacket -// 4: ConnectionIdlingPacket -// 5: RoomLinkPacket -// 6: RoomJoinPacket -// 7: RoomClosureRequestPacket -// 8: RoomClosedPacket -// 9: RoomCreationRequestPacket -// 10: MessagePacket -// 11: PopupPacket -// 12: Message2Packet -// 13: StatsPacket - -#[derive(Debug)] -pub struct ConnectionWrapperPacket { - pub connection_id: i32, -} - #[derive(Debug)] pub struct ConnectionPacketWrapPacket { pub connection_id: i32, @@ -78,9 +50,7 @@ pub struct ConnectionPacketWrapPacket { #[derive(Debug)] pub struct ConnectionClosedPacket { pub connection_id: i32, - pub reason: u8, // Using u8 ordinal directly, or could map to CloseReason if applicable? - // structure.md says "DcReason" which is an arc.net enum. - // Usually: 0=closed, 1=timeout, 2=error. + pub reason: ArcCloseReason, } #[derive(Debug)] @@ -117,7 +87,7 @@ pub struct RoomClosedPacket { pub struct RoomCreationRequestPacket { pub version: String, pub password: String, - pub data: RoomStats, + pub data: Stats, } #[derive(Debug)] @@ -138,59 +108,276 @@ pub struct Message2Packet { #[derive(Debug)] pub struct StatsPacket { pub room_id: String, - pub data: RoomStats, + pub data: Stats, +} + +impl AnyPacket { + pub fn read(buf: &mut Cursor<&[u8]>) -> Result { + if !buf.has_remaining() { + return Err(anyhow!("Empty packet")); + } + + let id = buf.get_i8(); + + match id { + FRAMEWORK_PACKET_ID => Ok(AnyPacket::Framework(FrameworkMessage::read(buf)?)), + APP_PACKET_ID => Ok(AnyPacket::App(AppPacket::read(buf)?)), + _ => { + buf.set_position(buf.position() - 1); + let remaining = buf.remaining(); + let mut bytes = BytesMut::with_capacity(remaining); + bytes.put(buf); + + Ok(AnyPacket::Raw(bytes.freeze())) + } + } + } + + pub fn write(&self, out: &mut BytesMut) { + let mut payload = BytesMut::new(); + + match self { + AnyPacket::Framework(package) => { + package.write(&mut payload); + } + AnyPacket::App(package) => { + package.write(&mut payload); + } + AnyPacket::Raw(bytes) => { + payload.put_slice(bytes); + } + } + + info!("Sending packet: {:?} with len: {}", self, payload.len()); + + out.put_u16(payload.len() as u16); + out.extend_from_slice(&payload); + } +} + +impl FrameworkMessage { + pub fn read(buf: &mut Cursor<&[u8]>) -> Result { + let fid = buf.get_u8(); + + match fid { + 0 => Ok(FrameworkMessage::Ping { + id: buf.get_i32(), + is_reply: buf.get_u8() == 1, + }), + 1 => Ok(FrameworkMessage::DiscoverHost), + 2 => Ok(FrameworkMessage::KeepAlive), + 3 => Ok(FrameworkMessage::RegisterUDP { + connection_id: buf.get_i32(), + }), + 4 => Ok(FrameworkMessage::RegisterTCP { + connection_id: buf.get_i32(), + }), + _ => Err(anyhow!("Unknown Framework ID: {}", fid)), + } + } + + pub fn write(&self, buf: &mut BytesMut) { + buf.put_i8(FRAMEWORK_PACKET_ID); + + match self { + FrameworkMessage::Ping { id, is_reply } => { + buf.put_u8(0); + buf.put_i32(*id); + buf.put_u8(if *is_reply { 1 } else { 0 }); + } + FrameworkMessage::DiscoverHost => buf.put_u8(1), + FrameworkMessage::KeepAlive => buf.put_u8(2), + FrameworkMessage::RegisterUDP { connection_id } => { + buf.put_u8(3); + buf.put_i32(*connection_id); + } + FrameworkMessage::RegisterTCP { connection_id } => { + buf.put_u8(4); + buf.put_i32(*connection_id); + } + } + } +} + + +impl AppPacket { + pub fn read(buf: &mut Cursor<&[u8]>) -> Result { + let pid = buf.get_u8(); + + match pid { + 0 => Ok(AppPacket::ConnectionPacketWrap( + ConnectionPacketWrapPacket { + connection_id: buf.get_i32(), + is_tcp: buf.get_u8() == 1, + buffer: { + let len = buf.get_i32() as usize; // Buffer length? + // Actually java side: buffer is object. + // If it's just raw bytes, we need length. + // Let's assume remaining or prefixed length. + // Usually `write(buffer, object)` writes generic object. + // If it's ByteBuffer, it writes raw bytes? + // NetworkProxy: ((Packets.ConnectionPacketWrapPacket) packet).buffer = (ByteBuffer) ((ByteBuffer) last.get().clear()).put(buffer).flip(); + // It seems it captures the rest of the buffer? + let mut b = BytesMut::new(); + while buf.has_remaining() { + b.put_u8(buf.get_u8()); + } + b.freeze() + }, + }, + )), + 1 => Ok(AppPacket::ConnectionClosed(ConnectionClosedPacket { + connection_id: buf.get_i32(), + reason: ArcCloseReason::from(buf.get_u8()), // DcReason ordinal + })), + 2 => Ok(AppPacket::ConnectionJoin(ConnectionJoinPacket { + connection_id: buf.get_i32(), + room_id: read_string(buf)?, + })), + 3 => Ok(AppPacket::ConnectionIdling(ConnectionIdlingPacket { + connection_id: buf.get_i32(), + })), + 4 => Ok(AppPacket::RoomCreationRequest(RoomCreationRequestPacket { + version: read_string(buf)?, + password: read_string(buf)?, + data: read_stats(buf)?, + })), + 5 => Ok(AppPacket::RoomClosureRequest(RoomClosureRequestPacket)), + 6 => Ok(AppPacket::RoomClosed(RoomClosedPacket { + reason: unsafe { std::mem::transmute(buf.get_u8()) }, // Unsafe or impl TryFrom + })), + 7 => Ok(AppPacket::RoomLink(RoomLinkPacket { + room_id: read_string(buf)?, + })), + 8 => Ok(AppPacket::RoomJoin(RoomJoinPacket { + room_id: read_string(buf)?, + password: read_string(buf)?, + })), + 9 => Ok(AppPacket::Message(MessagePacket { + message: read_string(buf)?, + })), + 10 => Ok(AppPacket::Message2(Message2Packet { + message: unsafe { std::mem::transmute(buf.get_u8()) }, + })), + 11 => Ok(AppPacket::Popup(PopupPacket { + message: read_string(buf)?, + })), + 12 => Ok(AppPacket::Stats(StatsPacket { + room_id: read_string(buf)?, + data: read_stats(buf)?, + })), + _ => Err(anyhow!("Unknown App Packet ID: {}", pid)), + } + } + + pub fn write(&self, buf: &mut BytesMut) { + buf.put_i8(APP_PACKET_ID as i8); + + match self { + AppPacket::ConnectionPacketWrap(p) => { + buf.put_u8(0); + buf.put_i32(p.connection_id); + buf.put_u8(if p.is_tcp { 1 } else { 0 }); + // Write buffer? + // buf.put_slice(&p.buffer); + } + AppPacket::ConnectionClosed(p) => { + buf.put_u8(1); + buf.put_i32(p.connection_id); + buf.put_u8(p.reason as u8); + } + AppPacket::ConnectionJoin(p) => { + buf.put_u8(2); + buf.put_i32(p.connection_id); + write_string(buf, &p.room_id); + } + AppPacket::ConnectionIdling(p) => { + buf.put_u8(3); + buf.put_i32(p.connection_id); + } + AppPacket::RoomCreationRequest(p) => { + buf.put_u8(4); + write_string(buf, &p.version); + write_string(buf, &p.password); + write_stats(buf, &p.data); + } + AppPacket::RoomClosureRequest(_) => { + buf.put_u8(5); + } + AppPacket::RoomClosed(p) => { + buf.put_u8(6); + buf.put_u8(p.reason as u8); + } + AppPacket::RoomLink(p) => { + buf.put_u8(7); + write_string(buf, &p.room_id); + } + AppPacket::RoomJoin(p) => { + buf.put_u8(8); + write_string(buf, &p.room_id); + write_string(buf, &p.password); + } + AppPacket::Message(p) => { + buf.put_u8(9); + write_string(buf, &p.message); + } + AppPacket::Message2(p) => { + buf.put_u8(10); + buf.put_u8(p.message as u8); + } + AppPacket::Popup(p) => { + buf.put_u8(11); + write_string(buf, &p.message); + } + AppPacket::Stats(p) => { + buf.put_u8(12); + write_string(buf, &p.room_id); + write_stats(buf, &p.data); + } + } + } } + // Helper to read/write strings -fn read_string(buf: &mut Cursor<&[u8]>) -> Result { +pub fn read_string(buf: &mut Cursor<&[u8]>) -> Result { if buf.remaining() < 2 { - return Err(anyhow!("Not enough bytes for string length: {}", buf.remaining())); + return Err(anyhow!( + "Not enough bytes for string length: {}", + buf.remaining() + )); } - + let len = buf.get_u16() as usize; - + if buf.remaining() < len { - return Err(anyhow!("Not enough bytes for string content, expected {}, got {}", len, buf.remaining())); + return Err(anyhow!( + "Not enough bytes for string content, expected {}, got {}", + len, + buf.remaining() + )); } - + let mut bytes = vec![0u8; len]; - + buf.copy_to_slice(&mut bytes); String::from_utf8(bytes).map_err(|e| anyhow!(e)) } -fn write_string(buf: &mut BytesMut, s: &str) { +pub fn write_string(buf: &mut BytesMut, s: &str) { let bytes = s.as_bytes(); buf.put_u16(bytes.len() as u16); buf.put_slice(bytes); } -// Helper to read/write RoomStats (JSON serialization as string?) -// In Java NetworkRelay: -// room.stats = statsPacket.data; -// It seems it's serialized as an object. -// structure.md says "non-primitive (object)". -// In ArcNet, objects are serialized using generic serialization. -// Since we are rewriting in Rust, and we don't have the full Java serialization context, -// we might assume it's JSON string or specific binary format. -// However, looking at models.rs, Stats has strings and arrays. -// Let's assume for now it's serialized as a JSON string for simplicity in Rust implementation, -// OR we implement binary serialization matching the struct fields. -// Given "non-primitive (object)" usually means Kryo/Java serialization in Mindustry context, -// but for PlayerConnect it might be simpler. -// Let's assume binary serialization of fields in order. - -fn read_stats(buf: &mut Cursor<&[u8]>) -> Result { - // This is a guess. We need to match Java side. - // Stats { players, mapName, name, gamemode, mods, locale, version, createdAt } - +pub fn read_stats(buf: &mut Cursor<&[u8]>) -> Result { let json = read_string(buf)?; info!("Stats JSON: {}", json); // let data = serde_json::from_str(json.as_str()); - Ok(RoomStats{ + Ok(Stats { players: vec![], map_name: String::new(), name: String::new(), @@ -202,7 +389,7 @@ fn read_stats(buf: &mut Cursor<&[u8]>) -> Result { }) } -fn write_stats(buf: &mut BytesMut, stats: &RoomStats) { +pub fn write_stats(buf: &mut BytesMut, stats: &Stats) { buf.put_u8(stats.players.len() as u8); for p in &stats.players { write_string(buf, &p.name); @@ -216,219 +403,3 @@ fn write_stats(buf: &mut BytesMut, stats: &RoomStats) { write_string(buf, &stats.version); buf.put_i64(stats.created_at); } - -impl AnyPacket { - pub fn read(mut buf: &mut Cursor<&[u8]>) -> Result { - if !buf.has_remaining() { - return Err(anyhow!("Empty packet")); - } - - let id = buf.get_i8(); - - if id == -2 { - // Framework Message - let fid = buf.get_u8(); - match fid { - 0 => Ok(AnyPacket::Framework(FrameworkMessage::Ping { - id: buf.get_i32(), - is_reply: buf.get_u8() == 1, - })), - 1 => Ok(AnyPacket::Framework(FrameworkMessage::DiscoverHost)), - 2 => Ok(AnyPacket::Framework(FrameworkMessage::KeepAlive)), - 3 => Ok(AnyPacket::Framework(FrameworkMessage::RegisterUDP { - connection_id: buf.get_i32(), - })), - 4 => Ok(AnyPacket::Framework(FrameworkMessage::RegisterTCP { - connection_id: buf.get_i32(), - })), - _ => Err(anyhow!("Unknown Framework ID: {}", fid)), - } - } else if id == PACKET_GROUP_ID { - // App Packet - let pid = buf.get_u8(); - - match pid { - 0 => Ok(AnyPacket::App(AppPacket::ConnectionPacketWrap( - ConnectionPacketWrapPacket { - connection_id: buf.get_i32(), - is_tcp: buf.get_u8() == 1, - buffer: { - let len = buf.get_i32() as usize; // Buffer length? - // Actually java side: buffer is object. - // If it's just raw bytes, we need length. - // Let's assume remaining or prefixed length. - // Usually `write(buffer, object)` writes generic object. - // If it's ByteBuffer, it writes raw bytes? - // NetworkProxy: ((Packets.ConnectionPacketWrapPacket) packet).buffer = (ByteBuffer) ((ByteBuffer) last.get().clear()).put(buffer).flip(); - // It seems it captures the rest of the buffer? - let mut b = BytesMut::new(); - while buf.has_remaining() { - b.put_u8(buf.get_u8()); - } - b.freeze() - }, - }, - ))), - 1 => Ok(AnyPacket::App(AppPacket::ConnectionClosed( - ConnectionClosedPacket { - connection_id: buf.get_i32(), - reason: buf.get_u8(), // DcReason ordinal - }, - ))), - 2 => Ok(AnyPacket::App(AppPacket::ConnectionJoin( - ConnectionJoinPacket { - connection_id: buf.get_i32(), - room_id: read_string(&mut buf)?, - }, - ))), - 3 => Ok(AnyPacket::App(AppPacket::ConnectionIdling( - ConnectionIdlingPacket { - connection_id: buf.get_i32(), - }, - ))), - 4 => Ok(AnyPacket::App(AppPacket::RoomCreationRequest( - RoomCreationRequestPacket { - version: read_string(&mut buf)?, - password: read_string(&mut buf)?, - data: read_stats(&mut buf)?, - }, - ))), - 5 => Ok(AnyPacket::App(AppPacket::RoomClosureRequest( - RoomClosureRequestPacket, - ))), - 6 => Ok(AnyPacket::App(AppPacket::RoomClosed(RoomClosedPacket { - reason: unsafe { std::mem::transmute(buf.get_u8()) }, // Unsafe or impl TryFrom - }))), - 7 => Ok(AnyPacket::App(AppPacket::RoomLink(RoomLinkPacket { - room_id: read_string(&mut buf)?, - }))), - 8 => Ok(AnyPacket::App(AppPacket::RoomJoin(RoomJoinPacket { - room_id: read_string(&mut buf)?, - password: read_string(&mut buf)?, - }))), - 9 => Ok(AnyPacket::App(AppPacket::Message(MessagePacket { - message: read_string(&mut buf)?, - }))), - 10 => Ok(AnyPacket::App(AppPacket::Message2(Message2Packet { - message: unsafe { std::mem::transmute(buf.get_u8()) }, - }))), - 11 => Ok(AnyPacket::App(AppPacket::Popup(PopupPacket { - message: read_string(&mut buf)?, - }))), - 12 => Ok(AnyPacket::App(AppPacket::Stats(StatsPacket { - room_id: read_string(&mut buf)?, - data: read_stats(&mut buf)?, - }))), - _ => Err(anyhow!("Unknown App Packet ID: {}", pid)), - } - } else { - // Raw Packet - buf.set_position(buf.position() - 1); - let remaining = buf.remaining(); - let mut bytes = BytesMut::with_capacity(remaining); - bytes.put(buf); - Ok(AnyPacket::Raw(bytes.freeze())) - } - } - - pub fn write(&self, out: &mut BytesMut) { - let mut payload = BytesMut::new(); - - match self { - AnyPacket::Framework(msg) => { - payload.put_i8(-2); - - match msg { - FrameworkMessage::Ping { id, is_reply } => { - payload.put_u8(0); - payload.put_i32(*id); - payload.put_u8(if *is_reply { 1 } else { 0 }); - } - FrameworkMessage::DiscoverHost => payload.put_u8(1), - FrameworkMessage::KeepAlive => payload.put_u8(2), - FrameworkMessage::RegisterUDP { connection_id } => { - payload.put_u8(3); - payload.put_i32(*connection_id); - } - FrameworkMessage::RegisterTCP { connection_id } => { - payload.put_u8(4); - payload.put_i32(*connection_id); - } - } - } - AnyPacket::App(pkt) => { - payload.put_i8(PACKET_GROUP_ID as i8); - match pkt { - AppPacket::ConnectionPacketWrap(p) => { - payload.put_u8(0); - payload.put_i32(p.connection_id); - payload.put_u8(if p.is_tcp { 1 } else { 0 }); - // Write payloadfer? - // payload.put_slice(&p.payloadfer); - } - AppPacket::ConnectionClosed(p) => { - payload.put_u8(1); - payload.put_i32(p.connection_id); - payload.put_u8(p.reason); - } - AppPacket::ConnectionJoin(p) => { - payload.put_u8(2); - payload.put_i32(p.connection_id); - write_string(&mut payload, &p.room_id); - } - AppPacket::ConnectionIdling(p) => { - payload.put_u8(3); - payload.put_i32(p.connection_id); - } - AppPacket::RoomCreationRequest(p) => { - payload.put_u8(4); - write_string(&mut payload, &p.version); - write_string(&mut payload, &p.password); - write_stats(&mut payload, &p.data); - } - AppPacket::RoomClosureRequest(_) => { - payload.put_u8(5); - } - AppPacket::RoomClosed(p) => { - payload.put_u8(6); - payload.put_u8(p.reason as u8); - } - AppPacket::RoomLink(p) => { - payload.put_u8(7); - write_string(&mut payload, &p.room_id); - } - AppPacket::RoomJoin(p) => { - payload.put_u8(8); - write_string(&mut payload, &p.room_id); - write_string(&mut payload, &p.password); - } - AppPacket::Message(p) => { - payload.put_u8(9); - write_string(&mut payload, &p.message); - } - AppPacket::Message2(p) => { - payload.put_u8(10); - payload.put_u8(p.message as u8); - } - AppPacket::Popup(p) => { - payload.put_u8(11); - write_string(&mut payload, &p.message); - } - AppPacket::Stats(p) => { - payload.put_u8(12); - write_string(&mut payload, &p.room_id); - write_stats(&mut payload, &p.data); - } - } - } - AnyPacket::Raw(bytes) => { - payload.put_slice(bytes); - } - } - - info!("Sending packet: {:?} with len: {}", self, payload.len()); - - out.put_u16(payload.len() as u16); - out.extend_from_slice(&payload); - } -} From 3d1a79b6012fa5584634851fee2654ae76d814cc Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Fri, 16 Jan 2026 19:28:22 +0700 Subject: [PATCH 005/115] refactor(server): restructure connection and room management - Extract room management logic into separate Rooms struct - Add rate limiting for connections - Improve packet handling and error management - Split TCP/UDP handling into separate functions - Add pending connection state for better connection lifecycle - Update models to simplify serialization - Add utility functions for time management --- src/http_server.rs | 81 ++++---- src/main.rs | 2 + src/models.rs | 13 +- src/packets.rs | 12 +- src/proxy_server.rs | 489 ++++++++++++++++++++++++-------------------- src/rate.rs | 39 ++++ src/state.rs | 183 ++++++++++++++++- src/utils.rs | 8 + 8 files changed, 538 insertions(+), 289 deletions(-) create mode 100644 src/rate.rs create mode 100644 src/utils.rs diff --git a/src/http_server.rs b/src/http_server.rs index d66790c..1747808 100644 --- a/src/http_server.rs +++ b/src/http_server.rs @@ -1,7 +1,10 @@ use crate::state::{AppState, RoomUpdate}; use axum::{ extract::{Path, State}, - response::{sse::{Event, Sse}, Html, IntoResponse}, + response::{ + sse::{Event, Sse}, + Html, IntoResponse, + }, routing::{get, post}, Router, }; @@ -33,38 +36,37 @@ async fn ping() -> impl IntoResponse { async fn rooms_sse( State(state): State>, ) -> Sse>> { - let rx = state.room_updates_tx.subscribe(); + let rx = state.rooms.subscribe(); let stream = BroadcastStream::new(rx); // Initial state: Send all current rooms // We need to construct a stream that starts with current rooms and then follows updates - // For simplicity here, we just subscribe to updates. + // For simplicity here, we just subscribe to updates. // Ideally, we should send an initial "snapshot" event or individual add events. - - let stream = stream.map(|msg| { - match msg { - Ok(update) => { - let event = match update { - RoomUpdate::Update(room) => { - Event::default().event("update").json_data(room.stats) - } - RoomUpdate::Remove(id) => Ok({ - Event::default().event("remove").data(id) - }) - }; - event.map_err(|e| axum::BoxError::from(e)) - } - Err(e) => Err(axum::BoxError::from(e)), + + let stream = stream.map(|msg| match msg { + Ok(update) => { + let event = match update { + RoomUpdate::Update(room) => Event::default().event("update").json_data(room.stats), + RoomUpdate::Remove(id) => Ok({ Event::default().event("remove").data(id) }), + }; + event.map_err(|e| axum::BoxError::from(e)) } + Err(e) => Err(axum::BoxError::from(e)), }); - Sse::new(stream).keep_alive(axum::response::sse::KeepAlive::new().interval(Duration::from_secs(10))) + Sse::new(stream) + .keep_alive(axum::response::sse::KeepAlive::new().interval(Duration::from_secs(10))) } -async fn room_page(Path(room_id): Path, State(state): State>) -> impl IntoResponse { +async fn room_page( + Path(room_id): Path, + State(state): State>, +) -> impl IntoResponse { if let Some(room) = state.rooms.get(&room_id) { let stats = &room.stats; - let html = format!(r#" + let html = format!( + r#" @@ -95,23 +97,26 @@ async fn room_page(Path(room_id): Path, State(state): StateCreated At: {} -"#, - stats.name, - stats.name, - stats.map_name, stats.gamemode, stats.players.len(), stats.version, - stats.map_name, - stats.gamemode, - stats.players.len(), - stats.locale, - stats.version, - stats.created_at, - stats.name, - stats.map_name, - stats.gamemode, - stats.players.len(), - stats.locale, - stats.version, - stats.created_at +"#, + stats.name, + stats.name, + stats.map_name, + stats.gamemode, + stats.players.len(), + stats.version, + stats.map_name, + stats.gamemode, + stats.players.len(), + stats.locale, + stats.version, + stats.created_at, + stats.name, + stats.map_name, + stats.gamemode, + stats.players.len(), + stats.locale, + stats.version, + stats.created_at ); Html(html) } else { diff --git a/src/main.rs b/src/main.rs index b2969bc..e1a20f8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,8 @@ mod packets; mod state; mod proxy_server; mod http_server; +mod utils; +mod rate; use crate::config::Config; use crate::state::AppState; diff --git a/src/models.rs b/src/models.rs index c340746..c4731bc 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,22 +1,15 @@ use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub struct Room { pub id: String, - #[serde(skip)] pub host_connection_id: i32, // Internal reference - #[serde(skip_serializing_if = "Option::is_none")] pub password: Option, pub ping: i64, - #[serde(rename = "isClosed")] pub is_closed: bool, - #[serde(rename = "createdAt")] - pub created_at: i64, - #[serde(rename = "updatedAt")] - pub updated_at: i64, - #[serde(skip)] - pub clients: Vec, // List of connection IDs + pub created_at: u128, + pub updated_at: u128, pub stats: Stats, } diff --git a/src/packets.rs b/src/packets.rs index 86bacdd..3dfc21b 100644 --- a/src/packets.rs +++ b/src/packets.rs @@ -133,7 +133,7 @@ impl AnyPacket { } } - pub fn write(&self, out: &mut BytesMut) { + pub fn bytes(&self) -> Bytes { let mut payload = BytesMut::new(); match self { @@ -143,15 +143,17 @@ impl AnyPacket { AnyPacket::App(package) => { package.write(&mut payload); } - AnyPacket::Raw(bytes) => { - payload.put_slice(bytes); + AnyPacket::Raw(data) => { + payload.put_slice(data); } } - info!("Sending packet: {:?} with len: {}", self, payload.len()); + let mut out: BytesMut = BytesMut::new(); out.put_u16(payload.len() as u16); out.extend_from_slice(&payload); + + out.freeze() } } @@ -199,7 +201,6 @@ impl FrameworkMessage { } } - impl AppPacket { pub fn read(buf: &mut Cursor<&[u8]>) -> Result { let pid = buf.get_u8(); @@ -338,7 +339,6 @@ impl AppPacket { } } - // Helper to read/write strings pub fn read_string(buf: &mut Cursor<&[u8]>) -> Result { if buf.remaining() < 2 { diff --git a/src/proxy_server.rs b/src/proxy_server.rs index 380727f..59d3dbb 100644 --- a/src/proxy_server.rs +++ b/src/proxy_server.rs @@ -1,7 +1,10 @@ use crate::packets::{ AnyPacket, AppPacket, FrameworkMessage, RoomCreationRequestPacket, RoomLinkPacket, }; -use crate::state::{AppState, ConnectionState}; +use crate::rate::RateLimiter; +use crate::state::{AppState, ConnectionState, PendingConnectionState, RoomInit}; + +use anyhow::anyhow; use bytes::{Buf, BytesMut}; use std::io::Cursor; use std::net::SocketAddr; @@ -9,50 +12,59 @@ use std::sync::atomic::{AtomicI32, Ordering}; use std::sync::Arc; use std::time::{Duration, Instant}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; -use tokio::net::{TcpListener, UdpSocket}; +use tokio::net::{TcpListener, TcpStream, UdpSocket}; use tokio::sync::mpsc; -use tracing::{error, info, warn}; -use uuid::{uuid, Uuid}; +use tracing::{error, info}; static NEXT_CONNECTION_ID: AtomicI32 = AtomicI32::new(1); -const BUFFER_SIZE: usize = 32768; -const CONNECTION_TIME_OUT: u64 = 10000; // In ms -const KEEP_ALIVE_INTERVAL: u64 = 8000; // In +const UDP_BUFFER_SIZE: usize = 4096; +const TCP_BUFFER_SIZE: usize = 32768; +const CHANNEL_CAPACITY: usize = 100; +const CONNECTION_TIME_OUT_MS: u64 = 10000; +const KEEP_ALIVE_INTERVAL_MS: u64 = 2000; const PACKET_LENGTH_LENGTH: usize = 2; +const TICK_INTERVAL_SECS: u64 = 1; + +const PACKET_RATE_LIMIT_WINDOW: Duration = Duration::from_millis(3000); +const PACKET_RATE_LIMIT: usize = 300; pub async fn run(state: Arc, port: u16) -> anyhow::Result<()> { let address = format!("0.0.0.0:{}", port); - let listener = TcpListener::bind(address.clone()).await?; + let tcp_listener = TcpListener::bind(address.clone()).await?; let udp_socket = Arc::new(UdpSocket::bind(address).await?); info!("Proxy Server listening on TCP/UDP {}", port); - let udp_state = state.clone(); - let udp_socket_clone = udp_socket.clone(); + spawn_udp_listener(state.clone(), udp_socket); + accept_tcp_connection(state.clone(), tcp_listener).await +} - // UDP Listener Loop +fn spawn_udp_listener(state: Arc, socket: Arc) { tokio::spawn(async move { - let mut buf = [0u8; 4096]; + let mut buf = [0u8; UDP_BUFFER_SIZE]; + loop { - match udp_socket_clone.recv_from(&mut buf).await { + match socket.recv_from(&mut buf).await { Ok((len, addr)) => { let data = &buf[..len]; - if let Err(e) = handle_udp_packet(&udp_state, data, addr).await { - error!("UDP Error from {}: {}", addr, e); + if let Err(error) = handle_udp_packet(&state, data, addr).await { + error!("UDP Error from {}: {}", addr, error); } } - Err(e) => { - error!("UDP Receive Error: {}", e); - } + Err(e) => error!("UDP Receive Error: {}", e), } } }); +} +async fn accept_tcp_connection(state: Arc, listener: TcpListener) -> anyhow::Result<()> { loop { let (socket, addr) = listener.accept().await?; + info!("New connection from {}", addr); + let state = state.clone(); tokio::spawn(async move { @@ -68,140 +80,137 @@ async fn handle_udp_packet( data: &[u8], addr: SocketAddr, ) -> anyhow::Result<()> { - // Basic packet structure check: Length (2 bytes) + Payload - info!("UDP: Received {} bytes from {}", data.len(), addr); - // if data.len() < PACKET_LENGTH_LENGTH { - // return Ok(()); - // } - - // let mut cursor = Cursor::new(data); - // let len = cursor.get_u16() as usize; - - // if data.len() < PACKET_LENGTH_LENGTH + len { - // warn!("Invalid packet length {} from {}", len, addr); - // return Ok(()); - // } - - // let payload = &data[PACKET_LENGTH_LENGTH..PACKET_LENGTH_LENGTH ]; let mut payload_cursor = Cursor::new(data); - let packet = AnyPacket::read(&mut payload_cursor); + let packet = AnyPacket::read(&mut payload_cursor)?; match packet { - Ok(AnyPacket::Framework(FrameworkMessage::RegisterUDP { connection_id })) => { - let tx = if let Some(mut conn) = state.connections.get_mut(&connection_id) { - info!( - "Registering UDP for connection {} at {}", - connection_id, addr - ); - conn.udp_addr = Some(addr); - - Some(conn.tx.clone()) - } else { - info!( - "Received RegisterUDP for unknown connection {}", - connection_id - ); - None - }; - - match tx { - Some(tx) => { - // Send RegisterUDP reply via TCP - let reply = - AnyPacket::Framework(FrameworkMessage::RegisterUDP { connection_id }); + AnyPacket::Framework(framework) => match framework { + FrameworkMessage::RegisterUDP { connection_id } => { + handle_register_udp(state, connection_id, addr).await; + } + FrameworkMessage::KeepAlive => { + info!("DO SOMETHING WITH THIS"); + } + _ => panic!("Unhandled UDP packet: {:?}", framework), + }, + _ => panic!("UDP: Unhandled packet {:?}", packet), + } + Ok(()) +} - let mut buf = BytesMut::new(); +async fn handle_register_udp(state: &Arc, connection_id: i32, addr: SocketAddr) { + if let Some((_, pending_conn)) = state.pending_connections.remove(&connection_id) { + info!( + "Registering UDP for connection {} at {}", + connection_id, addr + ); + + state.connections.insert( + connection_id, + ConnectionState { + id: pending_conn.id, + tx: pending_conn.tx, + last_write_time: pending_conn.last_write_time, + created_at: pending_conn.created_at, + rate: pending_conn.rate, + udp_addr: addr, + }, + ); + } else { + info!( + "Received RegisterUDP for unknown connection {}", + connection_id + ); + return; + }; - reply.write(&mut buf); + let packet = AnyPacket::Framework(FrameworkMessage::RegisterUDP { connection_id }); - if let Err(e) = tx.send(buf.freeze()).await { - error!("Failed to send RegisterUDP reply via TCP: {}", e); - } else { - if let Some(mut conn) = state.connections.get_mut(&connection_id) { - conn.last_write_time = Instant::now(); - } - } - } - None => { - warn!( - "Received RegisterUDP for unknown connection {}", - connection_id - ); - } - } - } - Ok(packet) => { - // Handle other UDP packets if necessary (e.g. game data) - // For now, we only care about registration or forwarding - info!("UDP: Unhandled packet {:?}", packet); - } - Err(e) => { - error!("Error parsing UDP packet from {}: {}", addr, e); - } + if let Err(e) = state.send(packet, connection_id).await { + error!("Failed to send RegisterUDP reply via TCP: {}", e); } - Ok(()) } -async fn handle_connection( - state: Arc, - socket: tokio::net::TcpStream, -) -> anyhow::Result<()> { +async fn handle_connection(state: Arc, socket: TcpStream) -> anyhow::Result<()> { let connection_id = NEXT_CONNECTION_ID.fetch_add(1, Ordering::Relaxed); - let (tx, mut rx) = mpsc::channel(100); + let (tx, rx) = mpsc::channel(CHANNEL_CAPACITY); + + register_pending_connection(&state, connection_id, tx.clone()); + + let (reader, writer) = socket.into_split(); + let write_handle = spawn_write_handler(rx, writer); + + send_register_tcp(&state.clone(), connection_id).await?; + process_tcp_stream(state.clone(), connection_id, reader).await?; + + // Cleanup + state.disconnect(connection_id).await?; + + write_handle.abort(); + + info!("Connection {} cleanup complete", connection_id); + Ok(()) +} - // Register connection - state.connections.insert( +fn register_pending_connection( + state: &Arc, + connection_id: i32, + tx: mpsc::Sender, +) { + state.pending_connections.insert( connection_id, - ConnectionState { + PendingConnectionState { id: connection_id, - room_id: None, - is_host: false, - tx: tx.clone(), - udp_addr: None, + tx, + rate: RateLimiter::new(PACKET_RATE_LIMIT, PACKET_RATE_LIMIT_WINDOW), last_write_time: Instant::now(), + created_at: Instant::now(), }, ); - info!("Connection {} registered", connection_id); +} - // Split socket - let (mut reader, mut writer) = socket.into_split(); - - // Spawn Write Loop - let write_handle = tokio::spawn(async move { +fn spawn_write_handler( + mut rx: mpsc::Receiver, + mut writer: tokio::net::tcp::OwnedWriteHalf, +) -> tokio::task::JoinHandle<()> { + tokio::spawn(async move { while let Some(bytes) = rx.recv().await { if let Err(e) = writer.write_all(&bytes).await { error!("Write error: {}", e); break; } } - }); - - // Send RegisterTCP to client - let mut write_buf: BytesMut = BytesMut::new(); - let register_packet = AnyPacket::Framework(FrameworkMessage::RegisterTCP { connection_id }); + }) +} - register_packet.write(&mut write_buf); +async fn send_register_tcp(state: &Arc, connection_id: i32) -> anyhow::Result<()> { + let packet = AnyPacket::Framework(FrameworkMessage::RegisterTCP { connection_id }); - if let Err(e) = tx.send(write_buf.freeze()).await { - error!("Failed to send RegisterTCP: {}", e); - return Ok(()); - } + state + .send(packet, connection_id) + .await + .map_err(|e| anyhow::anyhow!("Failed to send RegisterTCP: {}", e)) +} - // Read Loop - let mut buf = BytesMut::with_capacity(BUFFER_SIZE); - let mut tmp_buf = [0u8; BUFFER_SIZE]; - let mut tick_interval = tokio::time::interval(Duration::from_millis(KEEP_ALIVE_INTERVAL)); +async fn process_tcp_stream( + state: Arc, + connection_id: i32, + mut reader: tokio::net::tcp::OwnedReadHalf, +) -> anyhow::Result<()> { + let mut buf = BytesMut::with_capacity(TCP_BUFFER_SIZE); + let mut tmp_buf = [0u8; TCP_BUFFER_SIZE]; + let mut tick_interval = tokio::time::interval(Duration::from_secs(TICK_INTERVAL_SECS)); let mut last_read_time = Instant::now(); loop { tokio::select! { read_result = reader.read(&mut tmp_buf) => { match read_result { - Ok(n) if n == 0 => { + Ok(0) => { info!("Connection {} closed by peer", connection_id); break; } @@ -209,45 +218,7 @@ async fn handle_connection( last_read_time = Instant::now(); buf.extend_from_slice(&tmp_buf[..n]); - loop { - info!("TCP: Received {} bytes from connection {}", n, connection_id); - - if buf.len() < PACKET_LENGTH_LENGTH { - break; - } - - // Peek length - let len = { - let mut cur = Cursor::new(&buf[..]); - cur.get_u16() as usize - }; - - if buf.len() < PACKET_LENGTH_LENGTH + len { - break; - } - - buf.advance(PACKET_LENGTH_LENGTH); - - // Extract payload - let payload = buf.split_to(len); - let mut cursor = Cursor::new(&payload[..]); - - match AnyPacket::read(&mut cursor) { - Ok(packet) => { - info!("TCP: Received packet {:?} from connection {}", packet, connection_id); - - if let Err(e) = handle_packet(&state, connection_id, packet).await { - error!("Error handling packet for connection {}: {}", connection_id, e); - // Depending on severity, we might want to close connection - // For now, log and continue - } - } - Err(e) => { - error!("Error parsing packet for connection {} with size {}: {}", connection_id,len, e); - // Packet is consumed and discarded - } - } - } + process_buffer(&state, connection_id, &mut buf).await?; } Err(e) => { error!("Read error for connection {}: {}", connection_id, e); @@ -256,113 +227,181 @@ async fn handle_connection( } } _ = tick_interval.tick() => { - if last_read_time.elapsed() > Duration::from_millis(CONNECTION_TIME_OUT) { - info!("Connection {} timed out", connection_id); + if !handle_tick(&state, connection_id, last_read_time).await { break; } + } + } + } + Ok(()) +} - let should_send = if let Some(conn) = state.connections.get(&connection_id) { - conn.last_write_time.elapsed() > Duration::from_secs(8) - } else { - break; - }; - - if should_send { - let mut buf = BytesMut::new(); - let packet = AnyPacket::Framework(FrameworkMessage::KeepAlive); - packet.write(&mut buf); - if tx.send(buf.freeze()).await.is_err() { - break; - } - if let Some(mut conn) = state.connections.get_mut(&connection_id) { - conn.last_write_time = Instant::now(); - } +async fn process_buffer( + state: &Arc, + connection_id: i32, + buf: &mut BytesMut, +) -> anyhow::Result<()> { + loop { + if buf.len() < PACKET_LENGTH_LENGTH { + break; + } + + let len = { + let mut cur = Cursor::new(&buf[..]); + cur.get_u16() as usize + }; + + if buf.len() < PACKET_LENGTH_LENGTH + len { + break; + } + + buf.advance(PACKET_LENGTH_LENGTH); + + let payload = buf.split_to(len); + let mut cursor = Cursor::new(&payload[..]); + + match AnyPacket::read(&mut cursor) { + Err(e) => { + error!( + "Error parsing packet for connection {} with size {}: {}", + connection_id, len, e + ); + } + Ok(packet) => { + info!( + "TCP: Received packet {:?} from connection {}", + packet, connection_id + ); + + if let Err(e) = handle_packet(state, connection_id, packet).await { + error!( + "Error handling packet for connection {}: {}", + connection_id, e + ); } } } } - // Cleanup - state.connections.remove(&connection_id); - write_handle.abort(); - info!("Connection {} cleanup complete", connection_id); Ok(()) } +async fn handle_tick(state: &Arc, connection_id: i32, last_read_time: Instant) -> bool { + if last_read_time.elapsed() > Duration::from_millis(CONNECTION_TIME_OUT_MS) { + info!("Connection {} timed out", connection_id); + return false; + } + + let should_send = if let Some(conn) = state.connections.get(&connection_id) { + conn.last_write_time.elapsed() > Duration::from_millis(KEEP_ALIVE_INTERVAL_MS) + } else if let Some(conn) = state.pending_connections.get(&connection_id) { + conn.last_write_time.elapsed() > Duration::from_millis(KEEP_ALIVE_INTERVAL_MS) + } else { + info!("Unknown connection id: {}", connection_id); + return false; + }; + + if should_send { + let packet = AnyPacket::Framework(FrameworkMessage::KeepAlive); + + if let Err(err) = state.send(packet, connection_id).await { + error!("Fail to send keep alive: {}", err) + }; + } + + true +} + async fn handle_packet( state: &Arc, connection_id: i32, packet: AnyPacket, ) -> anyhow::Result<()> { match packet { - AnyPacket::Framework(msg) => match msg { - FrameworkMessage::Ping { id, is_reply } => { - if !is_reply { - let pong = AnyPacket::Framework(FrameworkMessage::Ping { id, is_reply: true }); - send_packet(state, connection_id, pong).await; - } - } - FrameworkMessage::KeepAlive => {} - _ => {} - }, - AnyPacket::App(app_msg) => { - handle_app_packet(state, connection_id, app_msg).await?; + AnyPacket::Framework(packet) => { + handle_framework(state, connection_id, packet).await?; + } + AnyPacket::App(packet) => { + handle_app_packet(state, connection_id, packet).await?; } AnyPacket::Raw(_) => todo!("Handle raw packet {:?}", packet), } + Ok(()) } -async fn handle_app_packet( +async fn handle_framework( state: &Arc, connection_id: i32, - packet: AppPacket, + packet: FrameworkMessage, ) -> anyhow::Result<()> { match packet { - AppPacket::RoomJoin(pkt) => { - info!("Connection {} joining room {}", connection_id, pkt.room_id); + FrameworkMessage::Ping { id, is_reply } => { + if !is_reply { + let packet = AnyPacket::Framework(FrameworkMessage::Ping { id, is_reply: true }); + + state.send(packet, connection_id).await?; + } + } + FrameworkMessage::KeepAlive => { if let Some(mut conn) = state.connections.get_mut(&connection_id) { - conn.room_id = Some(pkt.room_id.clone()); + conn.last_write_time = Instant::now(); } } - AppPacket::Message(pkt) => { - info!("Connection {} sent message: {}", connection_id, pkt.message); + _ => panic!("Unhandled packet: {:?}", packet), + } + + Ok(()) +} + +async fn handle_app_packet( + state: &Arc, + connection_id: i32, + packet: AppPacket, +) -> anyhow::Result<()> { + let room = state.rooms.find_connection_room(&connection_id); + + let is_rate_limited = { + let mut connection = state + .connections + .get_mut(&connection_id) + .expect("Connection must exist"); + + room.map(|r| r.host_connection_id == connection_id) + .unwrap_or(true) + && !connection.rate.allow() + }; + + if is_rate_limited { + info!("Connection rate limited: {}", connection_id); + state.disconnect(connection_id).await?; + return Err(anyhow!("Rate limited")); + } + + match packet { + AppPacket::RoomJoin(package) => { + // Add pending request + state.rooms.join(connection_id, &package.room_id); } AppPacket::RoomCreationRequest(RoomCreationRequestPacket { - version, + version: _, password, - data, + data: stats, }) => { - send_packet( - state, + let room_id = state.rooms.create(RoomInit { connection_id, - AnyPacket::App(AppPacket::RoomLink(RoomLinkPacket { - room_id: Uuid::now_v7().to_string(), - })), - ) - .await; - } - _ => todo!("Handle app packet {:?}", packet), - } - Ok(()) -} + password, + stats, + }); -async fn send_packet(state: &Arc, connection_id: i32, packet: AnyPacket) { - let tx = if let Some(conn) = state.connections.get(&connection_id) { - Some(conn.tx.clone()) - } else { - None - }; + let packet = AnyPacket::App(AppPacket::RoomLink(RoomLinkPacket { + room_id: room_id.clone(), + })); - if let Some(tx) = tx { - let mut buf = BytesMut::new(); - packet.write(&mut buf); - if let Ok(_) = tx.send(buf.freeze()).await { - if let Some(mut conn) = state.connections.get_mut(&connection_id) { - conn.last_write_time = Instant::now(); - } + state.send(packet, connection_id).await?; } - } else { - error!("No connection found for ID {}", connection_id); + _ => info!("Handle app packet {:?}", packet), } + + Ok(()) } diff --git a/src/rate.rs b/src/rate.rs new file mode 100644 index 0000000..6f74fb0 --- /dev/null +++ b/src/rate.rs @@ -0,0 +1,39 @@ +use std::collections::VecDeque; +use std::time::{Duration, Instant}; + +pub struct RateLimiter { + max_requests: usize, + window: Duration, + requests: VecDeque, +} + +impl RateLimiter { + pub fn new(max_requests: usize, window: Duration) -> Self { + Self { + max_requests, + window, + requests: VecDeque::new(), + } + } + + /// Returns true if request is allowed + pub fn allow(&mut self) -> bool { + let now = Instant::now(); + + // Remove expired timestamps + while let Some(&front) = self.requests.front() { + if now.duration_since(front) > self.window { + self.requests.pop_front(); + } else { + break; + } + } + + if self.requests.len() < self.max_requests { + self.requests.push_back(now); + true + } else { + false + } + } +} diff --git a/src/state.rs b/src/state.rs index 50848c9..e6058ef 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,17 +1,168 @@ -use crate::models::Room; +use crate::models::{ArcCloseReason, Room, Stats}; +use crate::packets::{AnyPacket, AppPacket, ConnectionClosedPacket}; +use crate::rate::RateLimiter; +use crate::utils::current_time_millis; +use anyhow::anyhow; use bytes::Bytes; use dashmap::DashMap; use std::net::SocketAddr; +use std::sync::Arc; use std::time::Instant; -use tokio::sync::mpsc; +use tokio::sync::broadcast::Receiver; +use tokio::sync::mpsc::{self}; +use tracing::info; +use uuid::Uuid; + +pub struct Rooms { + rooms: DashMap, + connection_room: DashMap, + // Channel to broadcast room updates to SSE (sender) + tx: tokio::sync::broadcast::Sender, +} +pub struct RoomInit { + pub connection_id: i32, + pub password: String, + pub stats: Stats, +} + +impl Rooms { + pub fn get( + &self, + room_id: &String, + ) -> std::option::Option> { + return self.rooms.get(room_id); + } + + pub fn find_connection_room( + &self, + connection_id: &i32, + ) -> std::option::Option> { + self.connection_room + .get(connection_id) + .and_then(|room_id| self.rooms.get(&room_id.clone())) + } + + pub fn join(&self, connection_id: i32, room_id: &String) { + self.connection_room.insert(connection_id, room_id.clone()); + } + + pub fn create(&self, init: RoomInit) -> String { + let RoomInit { + password, + connection_id, + stats, + } = init; + + let password = if password.len() == 0 { + None + } else { + Some(password) + }; + + let room_id = Uuid::now_v7().to_string(); + let room = Room { + id: room_id.clone(), + host_connection_id: connection_id, + password, + ping: 0, + is_closed: false, + stats, + created_at: current_time_millis(), + updated_at: current_time_millis(), + }; + + self.rooms.insert(room_id.clone(), room); + + return room_id; + } + + pub fn close(&self, room_id: &String) { + if let Some((_, _)) = self.rooms.remove(room_id) { + let _ = self + .tx + .send(crate::state::RoomUpdate::Remove(room_id.clone())); + + info!("Room removed: {}", room_id); + } else { + info!("Room not exists: {}", room_id); + } + } + + pub async fn disconnect( + &self, + state: &AppState, + connection_id: i32, + reason: ArcCloseReason, + ) -> anyhow::Result<()> { + if let Some((_, room_id)) = self.connection_room.remove(&connection_id) { + let is_host = self + .rooms + .get(&room_id) + .map(|r| r.host_connection_id == connection_id) + .unwrap_or(false); + + if is_host { + self.close(&room_id); + } else { + let packet = AnyPacket::App(AppPacket::ConnectionClosed(ConnectionClosedPacket { + connection_id, + reason, + })); + + state.send(packet, connection_id).await?; + } + } + + Ok(()) + } + + pub fn subscribe(&self) -> Receiver { + return self.tx.subscribe(); + } +} pub struct AppState { - pub rooms: DashMap, + pub rooms: Rooms, + pub pending_connections: DashMap, pub connections: DashMap, // Maps connection ID to its queue of packets waiting for room join pub packet_queue: DashMap>, - // Channel to broadcast room updates to SSE (sender) - pub room_updates_tx: tokio::sync::broadcast::Sender, +} + +impl AppState { + pub async fn disconnect(&self, connection_id: i32) -> anyhow::Result<()> { + self.pending_connections.remove(&connection_id); + self.connections.remove(&connection_id); + self.packet_queue.remove(&connection_id); + self.rooms + .disconnect(&self, connection_id, ArcCloseReason::Closed) + .await?; + + info!("Disconnected connection {}", connection_id); + + Ok(()) + } + + pub async fn send(&self, packet: AnyPacket, connection_id: i32) -> anyhow::Result<()> { + let bytes = packet.bytes(); + let length = bytes.len(); + + // info!("Sending packet: {:?} with len: {}", packet, length); + + if let Some(mut conn) = self.connections.get_mut(&connection_id) { + conn.tx.send(bytes).await?; + conn.last_write_time = Instant::now(); + } else if let Some(mut conn) = self.pending_connections.get_mut(&connection_id) { + conn.tx.send(bytes).await?; + conn.last_write_time = Instant::now(); + } else { + return Err(anyhow!("No connection for id: {}", connection_id)); + } + + info!("Sent packet: {:?} with len: {}", packet, length); + + Ok(()) + } } #[derive(Clone, Debug)] @@ -20,23 +171,35 @@ pub enum RoomUpdate { Remove(String), } +pub struct PendingConnectionState { + pub id: i32, + pub tx: mpsc::Sender, // To send data back to the TCP connection + pub last_write_time: Instant, + pub rate: RateLimiter, + pub created_at: Instant, +} + pub struct ConnectionState { pub id: i32, - pub room_id: Option, - pub is_host: bool, pub tx: mpsc::Sender, // To send data back to the TCP connection - pub udp_addr: Option, + pub udp_addr: SocketAddr, + pub rate: RateLimiter, pub last_write_time: Instant, + pub created_at: Instant, } impl AppState { pub fn new() -> Self { let (tx, _) = tokio::sync::broadcast::channel(100); Self { - rooms: DashMap::new(), connections: DashMap::new(), + pending_connections: DashMap::new(), packet_queue: DashMap::new(), - room_updates_tx: tx, + rooms: Rooms { + rooms: DashMap::new(), + connection_room: DashMap::new(), + tx: tx, + }, } } } diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..23b2de9 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,8 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + +pub fn current_time_millis() -> u128 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_millis() +} From f51d702efe77e231757226bf91aa5ba51c2bb3fb Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sat, 17 Jan 2026 00:16:57 +0700 Subject: [PATCH 006/115] refactor(proxy): restructure codebase and improve connection handling - Move constants from models.rs to new constant.rs module - Rename packets::bytes() to to_bytes() for clarity - Replace DashMap with RwLock for thread-safe state management - Implement atomic rate limiting with AtomicRateLimiter - Simplify connection handling with ConnectionActor pattern - Improve room management with proper member tracking - Add UDP routing support with rate limiting - Clean up packet handling and error management --- src/{models.rs => constant.rs} | 33 -- src/http_server.rs | 11 +- src/main.rs | 2 +- src/packets.rs | 7 +- src/proxy_server.rs | 558 +++++++++++++++------------------ src/rate.rs | 96 ++++-- src/state.rs | 347 +++++++++++++------- 7 files changed, 560 insertions(+), 494 deletions(-) rename src/{models.rs => constant.rs} (54%) diff --git a/src/models.rs b/src/constant.rs similarity index 54% rename from src/models.rs rename to src/constant.rs index c4731bc..181200b 100644 --- a/src/models.rs +++ b/src/constant.rs @@ -1,38 +1,5 @@ -use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; -#[derive(Debug, Clone)] -pub struct Room { - pub id: String, - pub host_connection_id: i32, // Internal reference - pub password: Option, - pub ping: i64, - pub is_closed: bool, - pub created_at: u128, - pub updated_at: u128, - pub stats: Stats, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Stats { - pub players: Vec, - #[serde(rename = "mapName")] - pub map_name: String, - pub name: String, - pub gamemode: String, - pub mods: Vec, - pub locale: String, - pub version: String, - #[serde(rename = "createdAt")] - pub created_at: i64, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Player { - pub name: String, - pub locale: String, -} - #[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr, PartialEq)] #[repr(u8)] pub enum CloseReason { diff --git a/src/http_server.rs b/src/http_server.rs index 1747808..d2f6c9b 100644 --- a/src/http_server.rs +++ b/src/http_server.rs @@ -63,8 +63,15 @@ async fn room_page( Path(room_id): Path, State(state): State>, ) -> impl IntoResponse { - if let Some(room) = state.rooms.get(&room_id) { - let stats = &room.stats; + let stats = { + if let Ok(rooms) = state.rooms.rooms.read() { + rooms.get(&room_id).map(|r| r.stats.clone()) + } else { + None + } + }; + + if let Some(stats) = stats { let html = format!( r#" diff --git a/src/main.rs b/src/main.rs index e1a20f8..86f0385 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,5 @@ mod config; -mod models; +mod constant; mod packets; mod state; mod proxy_server; diff --git a/src/packets.rs b/src/packets.rs index 3dfc21b..896b0bc 100644 --- a/src/packets.rs +++ b/src/packets.rs @@ -1,4 +1,7 @@ -use crate::models::{ArcCloseReason, CloseReason, MessageType, Stats}; +use crate::{ + constant::{ArcCloseReason, CloseReason, MessageType}, + state::Stats, +}; use anyhow::{anyhow, Result}; use bytes::{Buf, BufMut, Bytes, BytesMut}; use std::io::Cursor; @@ -133,7 +136,7 @@ impl AnyPacket { } } - pub fn bytes(&self) -> Bytes { + pub fn to_bytes(&self) -> Bytes { let mut payload = BytesMut::new(); match self { diff --git a/src/proxy_server.rs b/src/proxy_server.rs index 59d3dbb..1ebb0dd 100644 --- a/src/proxy_server.rs +++ b/src/proxy_server.rs @@ -1,10 +1,6 @@ -use crate::packets::{ - AnyPacket, AppPacket, FrameworkMessage, RoomCreationRequestPacket, RoomLinkPacket, -}; -use crate::rate::RateLimiter; -use crate::state::{AppState, ConnectionState, PendingConnectionState, RoomInit}; - -use anyhow::anyhow; +use crate::packets::{AnyPacket, AppPacket, FrameworkMessage, RoomLinkPacket}; +use crate::rate::AtomicRateLimiter; +use crate::state::{AppState, ConnectionAction, RoomInit}; use bytes::{Buf, BytesMut}; use std::io::Cursor; use std::net::SocketAddr; @@ -12,9 +8,9 @@ use std::sync::atomic::{AtomicI32, Ordering}; use std::sync::Arc; use std::time::{Duration, Instant}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; -use tokio::net::{TcpListener, TcpStream, UdpSocket}; +use tokio::net::{TcpListener, UdpSocket}; use tokio::sync::mpsc; -use tracing::{error, info}; +use tracing::{error, info, warn}; static NEXT_CONNECTION_ID: AtomicI32 = AtomicI32::new(1); @@ -27,18 +23,17 @@ const PACKET_LENGTH_LENGTH: usize = 2; const TICK_INTERVAL_SECS: u64 = 1; const PACKET_RATE_LIMIT_WINDOW: Duration = Duration::from_millis(3000); -const PACKET_RATE_LIMIT: usize = 300; +const PACKET_RATE_LIMIT: u32 = 300; pub async fn run(state: Arc, port: u16) -> anyhow::Result<()> { let address = format!("0.0.0.0:{}", port); - - let tcp_listener = TcpListener::bind(address.clone()).await?; - let udp_socket = Arc::new(UdpSocket::bind(address).await?); + let tcp_listener = TcpListener::bind(&address).await?; + let udp_socket = Arc::new(UdpSocket::bind(&address).await?); info!("Proxy Server listening on TCP/UDP {}", port); spawn_udp_listener(state.clone(), udp_socket); - accept_tcp_connection(state.clone(), tcp_listener).await + accept_tcp_connection(state, tcp_listener).await } fn spawn_udp_listener(state: Arc, socket: Arc) { @@ -49,8 +44,30 @@ fn spawn_udp_listener(state: Arc, socket: Arc) { match socket.recv_from(&mut buf).await { Ok((len, addr)) => { let data = &buf[..len]; - if let Err(error) = handle_udp_packet(&state, data, addr).await { - error!("UDP Error from {}: {}", addr, error); + let mut cursor = Cursor::new(data); + + match AnyPacket::read(&mut cursor) { + Ok(packet) => { + if let AnyPacket::Framework(FrameworkMessage::RegisterUDP { + connection_id, + }) = packet + { + // Handle Register UDP + handle_register_udp(&state, connection_id, addr).await; + } else { + // Normal packet, route it + if let Some((sender, limiter)) = state.get_route(&addr) { + if limiter.check() { + let _ = sender.try_send(ConnectionAction::Packet(packet)); + } + } else { + // Unknown UDP sender, ignore + } + } + } + Err(e) => { + warn!("UDP Parse Error from {}: {}", addr, e); + } } } Err(e) => error!("UDP Receive Error: {}", e), @@ -59,349 +76,270 @@ fn spawn_udp_listener(state: Arc, socket: Arc) { }); } +async fn handle_register_udp(state: &Arc, connection_id: i32, addr: SocketAddr) { + if let Some(sender) = state.get_sender(connection_id) { + info!( + "Registering UDP for connection {} at {}", + connection_id, addr + ); + let _ = sender.try_send(ConnectionAction::SetUdp(addr)); + } +} + async fn accept_tcp_connection(state: Arc, listener: TcpListener) -> anyhow::Result<()> { loop { let (socket, addr) = listener.accept().await?; - info!("New connection from {}", addr); let state = state.clone(); tokio::spawn(async move { - if let Err(e) = handle_connection(state, socket).await { - error!("Connection error: {}", e); + let _ = socket.set_nodelay(true); + let id = NEXT_CONNECTION_ID.fetch_add(1, Ordering::Relaxed); + + let (tx, rx) = mpsc::channel(CHANNEL_CAPACITY); + let limiter = Arc::new(AtomicRateLimiter::new( + PACKET_RATE_LIMIT, + PACKET_RATE_LIMIT_WINDOW, + )); + + state.register_connection(id, tx, limiter.clone()); + + let (reader, writer) = socket.into_split(); + + let mut actor = ConnectionActor { + id, + state: state.clone(), + rx, + writer, + udp_addr: None, + limiter, + last_write: Instant::now(), + last_read: Instant::now(), + }; + + if let Err(e) = actor.run(reader).await { + error!("Connection {} error: {}", id, e); } - }); - } -} - -async fn handle_udp_packet( - state: &Arc, - data: &[u8], - addr: SocketAddr, -) -> anyhow::Result<()> { - info!("UDP: Received {} bytes from {}", data.len(), addr); - let mut payload_cursor = Cursor::new(data); - let packet = AnyPacket::read(&mut payload_cursor)?; + state.remove_connection(id); - match packet { - AnyPacket::Framework(framework) => match framework { - FrameworkMessage::RegisterUDP { connection_id } => { - handle_register_udp(state, connection_id, addr).await; - } - FrameworkMessage::KeepAlive => { - info!("DO SOMETHING WITH THIS"); + if let Some(addr) = actor.udp_addr { + state.remove_udp(addr); } - _ => panic!("Unhandled UDP packet: {:?}", framework), - }, - _ => panic!("UDP: Unhandled packet {:?}", packet), - } - Ok(()) -} -async fn handle_register_udp(state: &Arc, connection_id: i32, addr: SocketAddr) { - if let Some((_, pending_conn)) = state.pending_connections.remove(&connection_id) { - info!( - "Registering UDP for connection {} at {}", - connection_id, addr - ); - - state.connections.insert( - connection_id, - ConnectionState { - id: pending_conn.id, - tx: pending_conn.tx, - last_write_time: pending_conn.last_write_time, - created_at: pending_conn.created_at, - rate: pending_conn.rate, - udp_addr: addr, - }, - ); - } else { - info!( - "Received RegisterUDP for unknown connection {}", - connection_id - ); - return; - }; - - let packet = AnyPacket::Framework(FrameworkMessage::RegisterUDP { connection_id }); - - if let Err(e) = state.send(packet, connection_id).await { - error!("Failed to send RegisterUDP reply via TCP: {}", e); + info!("Connection {} cleanup complete", id); + }); } } -async fn handle_connection(state: Arc, socket: TcpStream) -> anyhow::Result<()> { - let connection_id = NEXT_CONNECTION_ID.fetch_add(1, Ordering::Relaxed); - - let (tx, rx) = mpsc::channel(CHANNEL_CAPACITY); - - register_pending_connection(&state, connection_id, tx.clone()); - - let (reader, writer) = socket.into_split(); - let write_handle = spawn_write_handler(rx, writer); - - send_register_tcp(&state.clone(), connection_id).await?; - process_tcp_stream(state.clone(), connection_id, reader).await?; - - // Cleanup - state.disconnect(connection_id).await?; - - write_handle.abort(); - - info!("Connection {} cleanup complete", connection_id); - Ok(()) -} - -fn register_pending_connection( - state: &Arc, - connection_id: i32, - tx: mpsc::Sender, -) { - state.pending_connections.insert( - connection_id, - PendingConnectionState { - id: connection_id, - tx, - rate: RateLimiter::new(PACKET_RATE_LIMIT, PACKET_RATE_LIMIT_WINDOW), - last_write_time: Instant::now(), - created_at: Instant::now(), - }, - ); - info!("Connection {} registered", connection_id); +struct ConnectionActor { + id: i32, + state: Arc, + rx: mpsc::Receiver, + writer: tokio::net::tcp::OwnedWriteHalf, + udp_addr: Option, + limiter: Arc, + last_write: Instant, + last_read: Instant, } -fn spawn_write_handler( - mut rx: mpsc::Receiver, - mut writer: tokio::net::tcp::OwnedWriteHalf, -) -> tokio::task::JoinHandle<()> { - tokio::spawn(async move { - while let Some(bytes) = rx.recv().await { - if let Err(e) = writer.write_all(&bytes).await { - error!("Write error: {}", e); - break; - } - } - }) -} +impl ConnectionActor { + async fn run(&mut self, mut reader: tokio::net::tcp::OwnedReadHalf) -> anyhow::Result<()> { + let register_packet = AnyPacket::Framework(FrameworkMessage::RegisterTCP { + connection_id: self.id, + }); -async fn send_register_tcp(state: &Arc, connection_id: i32) -> anyhow::Result<()> { - let packet = AnyPacket::Framework(FrameworkMessage::RegisterTCP { connection_id }); + self.write_packet(register_packet).await?; - state - .send(packet, connection_id) - .await - .map_err(|e| anyhow::anyhow!("Failed to send RegisterTCP: {}", e)) -} + let mut buf = BytesMut::with_capacity(TCP_BUFFER_SIZE); + let mut tmp_buf = [0u8; TCP_BUFFER_SIZE]; + let mut tick_interval = tokio::time::interval(Duration::from_secs(TICK_INTERVAL_SECS)); -async fn process_tcp_stream( - state: Arc, - connection_id: i32, - mut reader: tokio::net::tcp::OwnedReadHalf, -) -> anyhow::Result<()> { - let mut buf = BytesMut::with_capacity(TCP_BUFFER_SIZE); - let mut tmp_buf = [0u8; TCP_BUFFER_SIZE]; - let mut tick_interval = tokio::time::interval(Duration::from_secs(TICK_INTERVAL_SECS)); - let mut last_read_time = Instant::now(); + loop { + let mut batch = BytesMut::new(); + + tokio::select! { + // TCP Read + read_result = reader.read(&mut tmp_buf) => { + match read_result { + Ok(0) => break, // EOF + Ok(n) => { + self.last_read = Instant::now(); + + if !self.limiter.check() { + info!("TCP Rate limit exceeded for {}", self.id); + break; + } + + buf.extend_from_slice(&tmp_buf[..n]); + self.process_tcp_buffer(&mut buf).await?; + } + Err(e) => return Err(e.into()), + } + } - loop { - tokio::select! { - read_result = reader.read(&mut tmp_buf) => { - match read_result { - Ok(0) => { - info!("Connection {} closed by peer", connection_id); + // Channel Read + action = self.rx.recv() => { + if let Some(action) = action { + self.handle_action(action, &mut batch).await?; + + while let Ok(action) = self.rx.try_recv() { + self.handle_action(action, &mut batch).await?; + } + // Flush batch + if !batch.is_empty() { + self.writer.write_all(&batch).await?; + self.last_write = Instant::now(); + } + } else { + // Channel closed break; } - Ok(n) => { - last_read_time = Instant::now(); - buf.extend_from_slice(&tmp_buf[..n]); + } - process_buffer(&state, connection_id, &mut buf).await?; - } - Err(e) => { - error!("Read error for connection {}: {}", connection_id, e); + // Tick + _ = tick_interval.tick() => { + if self.last_read.elapsed() > Duration::from_millis(CONNECTION_TIME_OUT_MS) { + info!("Connection {} timed out", self.id); break; } - } - } - _ = tick_interval.tick() => { - if !handle_tick(&state, connection_id, last_read_time).await { - break; + + if self.last_write.elapsed() > Duration::from_millis(KEEP_ALIVE_INTERVAL_MS) { + self.write_packet(AnyPacket::Framework(FrameworkMessage::KeepAlive)).await?; + } } } } + Ok(()) } - Ok(()) -} - -async fn process_buffer( - state: &Arc, - connection_id: i32, - buf: &mut BytesMut, -) -> anyhow::Result<()> { - loop { - if buf.len() < PACKET_LENGTH_LENGTH { - break; - } - - let len = { - let mut cur = Cursor::new(&buf[..]); - cur.get_u16() as usize - }; - if buf.len() < PACKET_LENGTH_LENGTH + len { - break; - } - - buf.advance(PACKET_LENGTH_LENGTH); - - let payload = buf.split_to(len); - let mut cursor = Cursor::new(&payload[..]); - - match AnyPacket::read(&mut cursor) { - Err(e) => { - error!( - "Error parsing packet for connection {} with size {}: {}", - connection_id, len, e - ); + async fn process_tcp_buffer(&mut self, buf: &mut BytesMut) -> anyhow::Result<()> { + loop { + if buf.len() < PACKET_LENGTH_LENGTH { + break; } - Ok(packet) => { - info!( - "TCP: Received packet {:?} from connection {}", - packet, connection_id - ); - - if let Err(e) = handle_packet(state, connection_id, packet).await { - error!( - "Error handling packet for connection {}: {}", - connection_id, e - ); - } + let len = { + let mut cur = Cursor::new(&buf[..]); + cur.get_u16() as usize + }; + if buf.len() < PACKET_LENGTH_LENGTH + len { + break; } - } - } - - Ok(()) -} + buf.advance(PACKET_LENGTH_LENGTH); + let payload = buf.split_to(len); + let mut cursor = Cursor::new(&payload[..]); -async fn handle_tick(state: &Arc, connection_id: i32, last_read_time: Instant) -> bool { - if last_read_time.elapsed() > Duration::from_millis(CONNECTION_TIME_OUT_MS) { - info!("Connection {} timed out", connection_id); - return false; - } - - let should_send = if let Some(conn) = state.connections.get(&connection_id) { - conn.last_write_time.elapsed() > Duration::from_millis(KEEP_ALIVE_INTERVAL_MS) - } else if let Some(conn) = state.pending_connections.get(&connection_id) { - conn.last_write_time.elapsed() > Duration::from_millis(KEEP_ALIVE_INTERVAL_MS) - } else { - info!("Unknown connection id: {}", connection_id); - return false; - }; - - if should_send { - let packet = AnyPacket::Framework(FrameworkMessage::KeepAlive); - - if let Err(err) = state.send(packet, connection_id).await { - error!("Fail to send keep alive: {}", err) - }; + let packet = AnyPacket::read(&mut cursor)?; + // Handle packet + self.handle_packet(packet).await?; + } + Ok(()) } - true -} + async fn handle_packet(&mut self, packet: AnyPacket) -> anyhow::Result<()> { + info!("Received packet: {:?}", packet); -async fn handle_packet( - state: &Arc, - connection_id: i32, - packet: AnyPacket, -) -> anyhow::Result<()> { - match packet { - AnyPacket::Framework(packet) => { - handle_framework(state, connection_id, packet).await?; + match packet { + AnyPacket::Framework(f) => self.handle_framework(f).await?, + AnyPacket::App(a) => self.handle_app(a).await?, + _ => {} } - AnyPacket::App(packet) => { - handle_app_packet(state, connection_id, packet).await?; - } - AnyPacket::Raw(_) => todo!("Handle raw packet {:?}", packet), + Ok(()) } - Ok(()) -} - -async fn handle_framework( - state: &Arc, - connection_id: i32, - packet: FrameworkMessage, -) -> anyhow::Result<()> { - match packet { - FrameworkMessage::Ping { id, is_reply } => { - if !is_reply { - let packet = AnyPacket::Framework(FrameworkMessage::Ping { id, is_reply: true }); - - state.send(packet, connection_id).await?; + async fn handle_framework(&mut self, packet: FrameworkMessage) -> anyhow::Result<()> { + match packet { + FrameworkMessage::Ping { id, is_reply } => { + if !is_reply { + self.write_packet(AnyPacket::Framework(FrameworkMessage::Ping { + id, + is_reply: true, + })) + .await?; + } } - } - FrameworkMessage::KeepAlive => { - if let Some(mut conn) = state.connections.get_mut(&connection_id) { - conn.last_write_time = Instant::now(); + FrameworkMessage::KeepAlive => { + // Handled by activity update } + FrameworkMessage::RegisterUDP { .. } => { + // Should not happen via TCP? + // But if it does, ignore? + } + _ => {} } - _ => panic!("Unhandled packet: {:?}", packet), + Ok(()) } - Ok(()) -} - -async fn handle_app_packet( - state: &Arc, - connection_id: i32, - packet: AppPacket, -) -> anyhow::Result<()> { - let room = state.rooms.find_connection_room(&connection_id); - - let is_rate_limited = { - let mut connection = state - .connections - .get_mut(&connection_id) - .expect("Connection must exist"); - - room.map(|r| r.host_connection_id == connection_id) - .unwrap_or(true) - && !connection.rate.allow() - }; - - if is_rate_limited { - info!("Connection rate limited: {}", connection_id); - state.disconnect(connection_id).await?; - return Err(anyhow!("Rate limited")); + async fn handle_app(&mut self, packet: AppPacket) -> anyhow::Result<()> { + match packet { + AppPacket::RoomJoin(p) => { + // Logic to join room + // Need to get sender for this actor. + // We are the actor. We have rx. + // But we need to give a Sender to the Room. + // We don't have a clone of our own Sender here easily unless we stored it. + // Or we can get it from state (registry). + if let Some(sender) = self.state.get_sender(self.id) { + self.state.rooms.join(self.id, &p.room_id, sender)?; + } + } + AppPacket::RoomCreationRequest(p) => { + if let Some(sender) = self.state.get_sender(self.id) { + let room_id = self.state.rooms.create(RoomInit { + connection_id: self.id, + password: p.password, + stats: p.data, + sender, + }); + self.write_packet(AnyPacket::App(AppPacket::RoomLink(RoomLinkPacket { + room_id, + }))) + .await?; + } + } + _ => {} + } + Ok(()) } - match packet { - AppPacket::RoomJoin(package) => { - // Add pending request - state.rooms.join(connection_id, &package.room_id); - } - AppPacket::RoomCreationRequest(RoomCreationRequestPacket { - version: _, - password, - data: stats, - }) => { - let room_id = state.rooms.create(RoomInit { - connection_id, - password, - stats, - }); - - let packet = AnyPacket::App(AppPacket::RoomLink(RoomLinkPacket { - room_id: room_id.clone(), - })); - - state.send(packet, connection_id).await?; + async fn handle_action( + &mut self, + action: ConnectionAction, + batch: &mut BytesMut, + ) -> anyhow::Result<()> { + match action { + ConnectionAction::Packet(p) => { + let bytes = p.to_bytes(); + batch.extend_from_slice(&bytes); + } + ConnectionAction::Raw(b) => { + batch.extend_from_slice(&b); + } + ConnectionAction::Close => { + // Return error to break loop + return Err(anyhow::anyhow!("Closed")); + } + ConnectionAction::SetUdp(addr) => { + self.udp_addr = Some(addr); + // Register in state + if let Some(sender) = self.state.get_sender(self.id) { + self.state.register_udp(addr, sender, self.limiter.clone()); + } + // Send reply + self.write_packet(AnyPacket::Framework(FrameworkMessage::RegisterUDP { + connection_id: self.id, + })) + .await?; + } } - _ => info!("Handle app packet {:?}", packet), + Ok(()) } - Ok(()) + async fn write_packet(&mut self, packet: AnyPacket) -> anyhow::Result<()> { + let bytes = packet.to_bytes(); + self.writer.write_all(&bytes).await?; + self.last_write = Instant::now(); + Ok(()) + } } diff --git a/src/rate.rs b/src/rate.rs index 6f74fb0..f65717b 100644 --- a/src/rate.rs +++ b/src/rate.rs @@ -1,39 +1,83 @@ -use std::collections::VecDeque; -use std::time::{Duration, Instant}; +use std::sync::atomic::{AtomicU32, AtomicU64, Ordering}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; -pub struct RateLimiter { - max_requests: usize, - window: Duration, - requests: VecDeque, +#[derive(Debug)] +pub struct AtomicRateLimiter { + rate: u32, + window_start: AtomicU64, + count: AtomicU32, + window_duration_ms: u64, } -impl RateLimiter { - pub fn new(max_requests: usize, window: Duration) -> Self { +impl AtomicRateLimiter { + pub fn new(rate: u32, window: Duration) -> Self { Self { - max_requests, - window, - requests: VecDeque::new(), + rate, + window_start: AtomicU64::new(current_millis()), + count: AtomicU32::new(0), + window_duration_ms: window.as_millis() as u64, } } - /// Returns true if request is allowed - pub fn allow(&mut self) -> bool { - let now = Instant::now(); + pub fn check(&self) -> bool { + let now = current_millis(); + let window_start = self.window_start.load(Ordering::Relaxed); - // Remove expired timestamps - while let Some(&front) = self.requests.front() { - if now.duration_since(front) > self.window { - self.requests.pop_front(); - } else { - break; - } + if now >= window_start + self.window_duration_ms { + // New window + // Try to advance window. If another thread did it, we just add to that new window. + let _ = self.window_start.compare_exchange( + window_start, + now, + Ordering::Relaxed, + Ordering::Relaxed, + ); + // Reset count if we successfully changed window, or if the window changed heavily. + // Actually, simpler approach: + // If window changed, we can try to reset count. + // But to be safe and lock-free: + // If we see old window, we try to update window and reset count. + // The simplest "lock-free" approximation is: } - if self.requests.len() < self.max_requests { - self.requests.push_back(now); - true - } else { - false + // Let's use a standard atomic window algorithm. + // If (now - window_start) > window: + // reset window_start = now + // count = 0 + // + // This requires CAS loop if we want strict correctness, or relaxed if we tolerate some burst. + // Given "Safe for TCP + UDP", let's do a proper CAS loop for window rotation. + + loop { + let start = self.window_start.load(Ordering::Relaxed); + if now >= start + self.window_duration_ms { + // Try to rotate window + if self + .window_start + .compare_exchange(start, now, Ordering::Relaxed, Ordering::Relaxed) + .is_ok() + { + // We won the race to rotate. Reset count. + self.count.store(1, Ordering::Relaxed); + return true; + } + // If we lost, loop again to see new state + } else { + // Current window + let c = self.count.fetch_add(1, Ordering::Relaxed); + if c < self.rate { + return true; + } else { + return false; + } + } } } } + +fn current_millis() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis() as u64 +} diff --git a/src/state.rs b/src/state.rs index e6058ef..b81497a 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,49 +1,116 @@ -use crate::models::{ArcCloseReason, Room, Stats}; -use crate::packets::{AnyPacket, AppPacket, ConnectionClosedPacket}; -use crate::rate::RateLimiter; +use crate::packets::AnyPacket; +use crate::rate::AtomicRateLimiter; use crate::utils::current_time_millis; use anyhow::anyhow; use bytes::Bytes; -use dashmap::DashMap; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; use std::net::SocketAddr; -use std::sync::Arc; -use std::time::Instant; +use std::sync::{Arc, RwLock}; use tokio::sync::broadcast::Receiver; -use tokio::sync::mpsc::{self}; -use tracing::info; +use tokio::sync::mpsc; +use tracing::{error, info}; use uuid::Uuid; +#[derive(Debug)] +pub enum ConnectionAction { + Packet(AnyPacket), + Raw(Bytes), + Close, + SetUdp(SocketAddr), +} + +#[derive(Debug)] +pub struct Room { + pub id: String, + pub host_connection_id: i32, + pub password: Option, + pub created_at: u128, + pub updated_at: u128, + pub members: HashMap>, + pub stats: Stats, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Stats { + pub players: Vec, + #[serde(rename = "mapName")] + pub map_name: String, + pub name: String, + pub gamemode: String, + pub mods: Vec, + pub locale: String, + pub version: String, + #[serde(rename = "createdAt")] + pub created_at: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Player { + pub name: String, + pub locale: String, +} + pub struct Rooms { - rooms: DashMap, - connection_room: DashMap, - // Channel to broadcast room updates to SSE (sender) + pub rooms: RwLock>, tx: tokio::sync::broadcast::Sender, } + pub struct RoomInit { pub connection_id: i32, pub password: String, pub stats: Stats, + pub sender: mpsc::Sender, } impl Rooms { - pub fn get( + pub fn get_sender( &self, - room_id: &String, - ) -> std::option::Option> { - return self.rooms.get(room_id); + room_id: &str, + connection_id: i32, + ) -> Option> { + let rooms = self.rooms.read().ok()?; + + rooms.get(room_id)?.members.get(&connection_id).cloned() + } + + pub fn find_connection_room_id(&self, connection_id: i32) -> Option { + let rooms = self.rooms.read().ok()?; + + rooms + .iter() + .find(|(_, room)| room.members.contains_key(&connection_id)) + .map(|(id, _)| id.clone()) } - pub fn find_connection_room( + pub fn join( &self, - connection_id: &i32, - ) -> std::option::Option> { - self.connection_room - .get(connection_id) - .and_then(|room_id| self.rooms.get(&room_id.clone())) + connection_id: i32, + room_id: &String, + sender: mpsc::Sender, + ) -> anyhow::Result<()> { + let mut rooms = self.rooms.write().map_err(|_| anyhow!("Lock poison"))?; + + if let Some(room) = rooms.get_mut(room_id) { + room.members.insert(connection_id, sender); + Ok(()) + } else { + Err(anyhow!("Room not found")) + } } - pub fn join(&self, connection_id: i32, room_id: &String) { - self.connection_room.insert(connection_id, room_id.clone()); + pub fn leave(&self, connection_id: i32) -> Option { + let mut rooms = self.rooms.write().ok()?; + let room_id = rooms + .iter() + .find(|(_, room)| room.members.contains_key(&connection_id)) + .map(|(id, _)| id.clone())?; + + if let Some(room) = rooms.get_mut(&room_id) { + room.members.remove(&connection_id); + } + + Some(room_id) } pub fn create(&self, init: RoomInit) -> String { @@ -51,155 +118,195 @@ impl Rooms { password, connection_id, stats, + sender, } = init; - let password = if password.len() == 0 { + let password = if password.is_empty() { None } else { Some(password) }; let room_id = Uuid::now_v7().to_string(); + let mut members = HashMap::new(); + + members.insert(connection_id, sender); + let room = Room { id: room_id.clone(), host_connection_id: connection_id, password, - ping: 0, - is_closed: false, stats, + members, created_at: current_time_millis(), updated_at: current_time_millis(), }; - self.rooms.insert(room_id.clone(), room); + if let Ok(mut rooms) = self.rooms.write() { + rooms.insert(room_id.clone(), room); + } - return room_id; + room_id } pub fn close(&self, room_id: &String) { - if let Some((_, _)) = self.rooms.remove(room_id) { - let _ = self - .tx - .send(crate::state::RoomUpdate::Remove(room_id.clone())); + let removed = { + if let Ok(mut rooms) = self.rooms.write() { + rooms.remove(room_id) + } else { + None + } + }; + if removed.is_some() { + let _ = self.tx.send(RoomUpdate::Remove(room_id.clone())); info!("Room removed: {}", room_id); - } else { - info!("Room not exists: {}", room_id); } } - pub async fn disconnect( - &self, - state: &AppState, - connection_id: i32, - reason: ArcCloseReason, - ) -> anyhow::Result<()> { - if let Some((_, room_id)) = self.connection_room.remove(&connection_id) { - let is_host = self - .rooms - .get(&room_id) - .map(|r| r.host_connection_id == connection_id) - .unwrap_or(false); - - if is_host { - self.close(&room_id); - } else { - let packet = AnyPacket::App(AppPacket::ConnectionClosed(ConnectionClosedPacket { - connection_id, - reason, - })); + pub fn subscribe(&self) -> Receiver { + self.tx.subscribe() + } - state.send(packet, connection_id).await?; + pub fn broadcast(&self, room_id: &str, bytes: Bytes, exclude_id: Option) { + if let Ok(rooms) = self.rooms.read() { + if let Some(room) = rooms.get(room_id) { + for (id, sender) in &room.members { + if Some(*id) == exclude_id { + continue; + } + let _ = sender.try_send(ConnectionAction::Raw(bytes.clone())); + } } } - - Ok(()) - } - - pub fn subscribe(&self) -> Receiver { - return self.tx.subscribe(); } } pub struct AppState { pub rooms: Rooms, - pub pending_connections: DashMap, - pub connections: DashMap, - // Maps connection ID to its queue of packets waiting for room join - pub packet_queue: DashMap>, + pub connections: RwLock, Arc)>>, + pub udp_routes: + RwLock, Arc)>>, } impl AppState { - pub async fn disconnect(&self, connection_id: i32) -> anyhow::Result<()> { - self.pending_connections.remove(&connection_id); - self.connections.remove(&connection_id); - self.packet_queue.remove(&connection_id); - self.rooms - .disconnect(&self, connection_id, ArcCloseReason::Closed) - .await?; + pub fn new() -> Self { + let (tx, _) = tokio::sync::broadcast::channel(100); + Self { + rooms: Rooms { + rooms: RwLock::new(HashMap::new()), + tx, + }, + connections: RwLock::new(HashMap::new()), + udp_routes: RwLock::new(HashMap::new()), + } + } - info!("Disconnected connection {}", connection_id); + pub fn register_connection( + &self, + id: i32, + sender: mpsc::Sender, + limiter: Arc, + ) { + match self.connections.write() { + Ok(mut conns) => { + conns.insert(id, (sender, limiter)); + } + Err(err) => { + error!("{}", err) + } + } + } - Ok(()) + pub fn register_udp( + &self, + addr: SocketAddr, + sender: mpsc::Sender, + limiter: Arc, + ) { + if let Ok(mut routes) = self.udp_routes.write() { + routes.insert(addr, (sender, limiter)); + } } - pub async fn send(&self, packet: AnyPacket, connection_id: i32) -> anyhow::Result<()> { - let bytes = packet.bytes(); - let length = bytes.len(); + pub fn remove_udp(&self, addr: SocketAddr) { + if let Ok(mut routes) = self.udp_routes.write() { + routes.remove(&addr); + } + } - // info!("Sending packet: {:?} with len: {}", packet, length); + pub fn get_sender(&self, id: i32) -> Option> { + self.connections + .read() + .ok()? + .get(&id) + .map(|(s, _)| s.clone()) + } - if let Some(mut conn) = self.connections.get_mut(&connection_id) { - conn.tx.send(bytes).await?; - conn.last_write_time = Instant::now(); - } else if let Some(mut conn) = self.pending_connections.get_mut(&connection_id) { - conn.tx.send(bytes).await?; - conn.last_write_time = Instant::now(); - } else { - return Err(anyhow!("No connection for id: {}", connection_id)); + pub fn get_route( + &self, + addr: &SocketAddr, + ) -> Option<(mpsc::Sender, Arc)> { + self.udp_routes.read().ok()?.get(addr).cloned() + } + + pub fn remove_connection(&self, connection_id: i32) { + if let Ok(mut conns) = self.connections.write() { + conns.remove(&connection_id); } - info!("Sent packet: {:?} with len: {}", packet, length); + // Handle room logic + let room_id_opt = self.rooms.leave(connection_id); + if let Some(room_id) = room_id_opt { + // Check if host + let should_close = { + if let Ok(rooms) = self.rooms.rooms.read() { + if let Some(room) = rooms.get(&room_id) { + room.host_connection_id == connection_id + } else { + false + } + } else { + false + } + }; + + if should_close { + // Close room and disconnect all members + let members = { + if let Ok(rooms) = self.rooms.rooms.read() { + if let Some(room) = rooms.get(&room_id) { + room.members.values().cloned().collect::>() + } else { + Vec::new() + } + } else { + Vec::new() + } + }; + + for sender in members { + let _ = sender.try_send(ConnectionAction::Close); + } - Ok(()) + self.rooms.close(&room_id); + } + } } } #[derive(Clone, Debug)] pub enum RoomUpdate { - Update(Room), + Update(RoomDisplay), Remove(String), } -pub struct PendingConnectionState { - pub id: i32, - pub tx: mpsc::Sender, // To send data back to the TCP connection - pub last_write_time: Instant, - pub rate: RateLimiter, - pub created_at: Instant, -} - -pub struct ConnectionState { - pub id: i32, - pub tx: mpsc::Sender, // To send data back to the TCP connection - pub udp_addr: SocketAddr, - pub rate: RateLimiter, - pub last_write_time: Instant, - pub created_at: Instant, -} - -impl AppState { - pub fn new() -> Self { - let (tx, _) = tokio::sync::broadcast::channel(100); - Self { - connections: DashMap::new(), - pending_connections: DashMap::new(), - packet_queue: DashMap::new(), - rooms: Rooms { - rooms: DashMap::new(), - connection_room: DashMap::new(), - tx: tx, - }, - } - } +#[derive(Clone, Debug, Serialize)] +pub struct RoomDisplay { + pub id: String, + pub host_connection_id: i32, + pub password: bool, + pub players: usize, + pub max_players: usize, + pub stats: Stats, } From f3b7c32cd1a9b7115bb7781acd4cd4cd2a7a282b Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sat, 17 Jan 2026 01:15:11 +0700 Subject: [PATCH 007/115] c --- src/packets.rs | 32 ++++++++-------- src/proxy_server.rs | 89 ++++++++++++++++++++++++++++++++++++--------- src/rate.rs | 42 +++------------------ src/state.rs | 14 ++++--- 4 files changed, 101 insertions(+), 76 deletions(-) diff --git a/src/packets.rs b/src/packets.rs index 896b0bc..b9ce022 100644 --- a/src/packets.rs +++ b/src/packets.rs @@ -10,14 +10,14 @@ use tracing::info; pub const APP_PACKET_ID: i8 = -4; pub const FRAMEWORK_PACKET_ID: i8 = -2; -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum AnyPacket { Framework(FrameworkMessage), App(AppPacket), Raw(Bytes), } -#[derive(Debug)] +#[derive(Debug, Clone, Copy)] pub enum FrameworkMessage { Ping { id: i32, is_reply: bool }, DiscoverHost, @@ -26,7 +26,7 @@ pub enum FrameworkMessage { RegisterTCP { connection_id: i32 }, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum AppPacket { ConnectionPacketWrap(ConnectionPacketWrapPacket), ConnectionClosed(ConnectionClosedPacket), @@ -43,72 +43,72 @@ pub enum AppPacket { Stats(StatsPacket), } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct ConnectionPacketWrapPacket { pub connection_id: i32, pub is_tcp: bool, pub buffer: Bytes, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct ConnectionClosedPacket { pub connection_id: i32, pub reason: ArcCloseReason, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct ConnectionJoinPacket { pub connection_id: i32, pub room_id: String, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct ConnectionIdlingPacket { pub connection_id: i32, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct RoomLinkPacket { pub room_id: String, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct RoomJoinPacket { pub room_id: String, pub password: String, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct RoomClosureRequestPacket; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct RoomClosedPacket { pub reason: CloseReason, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct RoomCreationRequestPacket { pub version: String, pub password: String, pub data: Stats, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct MessagePacket { pub message: String, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct PopupPacket { pub message: String, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Message2Packet { pub message: MessageType, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct StatsPacket { pub room_id: String, pub data: Stats, diff --git a/src/proxy_server.rs b/src/proxy_server.rs index 1ebb0dd..f702d66 100644 --- a/src/proxy_server.rs +++ b/src/proxy_server.rs @@ -1,6 +1,9 @@ -use crate::packets::{AnyPacket, AppPacket, FrameworkMessage, RoomLinkPacket}; +use crate::packets::{ + AnyPacket, AppPacket, ConnectionPacketWrapPacket, FrameworkMessage, RoomLinkPacket, +}; use crate::rate::AtomicRateLimiter; use crate::state::{AppState, ConnectionAction, RoomInit}; +use anyhow::anyhow; use bytes::{Buf, BytesMut}; use std::io::Cursor; use std::net::SocketAddr; @@ -58,7 +61,7 @@ fn spawn_udp_listener(state: Arc, socket: Arc) { // Normal packet, route it if let Some((sender, limiter)) = state.get_route(&addr) { if limiter.check() { - let _ = sender.try_send(ConnectionAction::Packet(packet)); + let _ = sender.try_send(ConnectionAction::SendTCP(packet)); } } else { // Unknown UDP sender, ignore @@ -82,7 +85,7 @@ async fn handle_register_udp(state: &Arc, connection_id: i32, addr: So "Registering UDP for connection {} at {}", connection_id, addr ); - let _ = sender.try_send(ConnectionAction::SetUdp(addr)); + let _ = sender.try_send(ConnectionAction::RegisterUDP(addr)); } } @@ -243,7 +246,16 @@ impl ConnectionActor { match packet { AnyPacket::Framework(f) => self.handle_framework(f).await?, AnyPacket::App(a) => self.handle_app(a).await?, - _ => {} + AnyPacket::Raw(bytes) => { + // TODO: Queue + if let Some(room_id) = self.state.rooms.find_connection_room_id(self.id) { + self.state + .rooms + .broadcast(&room_id, ConnectionAction::SendTCPRaw(bytes), None); + } else { + info!("No room found for connection {}", self.id); + } + } } Ok(()) } @@ -274,17 +286,20 @@ impl ConnectionActor { async fn handle_app(&mut self, packet: AppPacket) -> anyhow::Result<()> { match packet { AppPacket::RoomJoin(p) => { - // Logic to join room - // Need to get sender for this actor. - // We are the actor. We have rx. - // But we need to give a Sender to the Room. - // We don't have a clone of our own Sender here easily unless we stored it. - // Or we can get it from state (registry). - if let Some(sender) = self.state.get_sender(self.id) { - self.state.rooms.join(self.id, &p.room_id, sender)?; - } + let Some(sender) = self.state.get_sender(self.id) else { + return Err(anyhow::anyhow!( + "No sender found for connection {}", + self.id + )); + }; + self.state.rooms.join(self.id, &p.room_id, sender)?; } AppPacket::RoomCreationRequest(p) => { + if let Some(room_id) = self.state.rooms.find_connection_room_id(self.id) { + info!("Connection {} already in room {}", self.id, room_id); + + return Ok(()); + } if let Some(sender) = self.state.get_sender(self.id) { let room_id = self.state.rooms.create(RoomInit { connection_id: self.id, @@ -298,7 +313,41 @@ impl ConnectionActor { .await?; } } - _ => {} + AppPacket::ConnectionPacketWrap(ConnectionPacketWrapPacket { + connection_id, + is_tcp, + buffer, + }) => { + if let Some(room_id) = self.state.rooms.find_connection_room_id(self.id) { + if let Ok(rooms) = self.state.rooms.rooms.read() { + let is_owner = rooms + .get(&room_id) + .map(|r| r.host_connection_id == self.id) + .unwrap_or(false); + + if !is_owner { + return Err(anyhow!("Not room owner")); + } + + let action = if is_tcp { + ConnectionAction::SendTCPRaw(buffer) + } else { + ConnectionAction::SendUDPRaw(buffer) + }; + + let Some(sender) = self.state.get_sender(connection_id) else { + return Err(anyhow!("Connection not found: {}", connection_id)); + }; + + sender.try_send(action)?; + } + } else { + info!("No room found for connection {}", self.id); + } + } + _ => { + info!("Unhandled {:?}", packet); + } } Ok(()) } @@ -309,18 +358,24 @@ impl ConnectionActor { batch: &mut BytesMut, ) -> anyhow::Result<()> { match action { - ConnectionAction::Packet(p) => { + ConnectionAction::SendTCP(p) => { let bytes = p.to_bytes(); batch.extend_from_slice(&bytes); } - ConnectionAction::Raw(b) => { + ConnectionAction::SendUDP(p) => { + info!("Send UDP: {:?}", p); + } + ConnectionAction::SendTCPRaw(b) => { batch.extend_from_slice(&b); } + ConnectionAction::SendUDPRaw(b) => { + info!("Send UDP raw to {}", self.id); + } ConnectionAction::Close => { // Return error to break loop return Err(anyhow::anyhow!("Closed")); } - ConnectionAction::SetUdp(addr) => { + ConnectionAction::RegisterUDP(addr) => { self.udp_addr = Some(addr); // Register in state if let Some(sender) = self.state.get_sender(self.id) { diff --git a/src/rate.rs b/src/rate.rs index f65717b..4cc7875 100644 --- a/src/rate.rs +++ b/src/rate.rs @@ -21,56 +21,24 @@ impl AtomicRateLimiter { pub fn check(&self) -> bool { let now = current_millis(); - let window_start = self.window_start.load(Ordering::Relaxed); - - if now >= window_start + self.window_duration_ms { - // New window - // Try to advance window. If another thread did it, we just add to that new window. - let _ = self.window_start.compare_exchange( - window_start, - now, - Ordering::Relaxed, - Ordering::Relaxed, - ); - // Reset count if we successfully changed window, or if the window changed heavily. - // Actually, simpler approach: - // If window changed, we can try to reset count. - // But to be safe and lock-free: - // If we see old window, we try to update window and reset count. - // The simplest "lock-free" approximation is: - } - - // Let's use a standard atomic window algorithm. - // If (now - window_start) > window: - // reset window_start = now - // count = 0 - // - // This requires CAS loop if we want strict correctness, or relaxed if we tolerate some burst. - // Given "Safe for TCP + UDP", let's do a proper CAS loop for window rotation. loop { let start = self.window_start.load(Ordering::Relaxed); + if now >= start + self.window_duration_ms { - // Try to rotate window if self .window_start .compare_exchange(start, now, Ordering::Relaxed, Ordering::Relaxed) .is_ok() { - // We won the race to rotate. Reset count. self.count.store(1, Ordering::Relaxed); return true; } - // If we lost, loop again to see new state - } else { - // Current window - let c = self.count.fetch_add(1, Ordering::Relaxed); - if c < self.rate { - return true; - } else { - return false; - } + continue; } + + let c = self.count.fetch_add(1, Ordering::Relaxed); + return c < self.rate; } } } diff --git a/src/state.rs b/src/state.rs index b81497a..25d5cf3 100644 --- a/src/state.rs +++ b/src/state.rs @@ -12,12 +12,14 @@ use tokio::sync::mpsc; use tracing::{error, info}; use uuid::Uuid; -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum ConnectionAction { - Packet(AnyPacket), - Raw(Bytes), + SendTCP(AnyPacket), + SendUDP(AnyPacket), + SendTCPRaw(Bytes), + SendUDPRaw(Bytes), Close, - SetUdp(SocketAddr), + RegisterUDP(SocketAddr), } #[derive(Debug)] @@ -168,14 +170,14 @@ impl Rooms { self.tx.subscribe() } - pub fn broadcast(&self, room_id: &str, bytes: Bytes, exclude_id: Option) { + pub fn broadcast(&self, room_id: &str, action: ConnectionAction, exclude_id: Option) { if let Ok(rooms) = self.rooms.read() { if let Some(room) = rooms.get(room_id) { for (id, sender) in &room.members { if Some(*id) == exclude_id { continue; } - let _ = sender.try_send(ConnectionAction::Raw(bytes.clone())); + let _ = sender.try_send(action.clone()); } } } From c9b48ce08b7f1441bb1c261bb78816cb8068b3cc Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sat, 17 Jan 2026 01:26:29 +0700 Subject: [PATCH 008/115] refactor(proxy): extract TCP/UDP writers into separate structs Move TCP and UDP writing logic into dedicated TcpWriter and UdpWriter structs to improve code organization and maintainability. This change also removes duplicate write tracking logic and centralizes UDP address handling. --- src/proxy_server.rs | 94 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 76 insertions(+), 18 deletions(-) diff --git a/src/proxy_server.rs b/src/proxy_server.rs index f702d66..2f675d5 100644 --- a/src/proxy_server.rs +++ b/src/proxy_server.rs @@ -35,8 +35,8 @@ pub async fn run(state: Arc, port: u16) -> anyhow::Result<()> { info!("Proxy Server listening on TCP/UDP {}", port); - spawn_udp_listener(state.clone(), udp_socket); - accept_tcp_connection(state, tcp_listener).await + spawn_udp_listener(state.clone(), udp_socket.clone()); + accept_tcp_connection(state, tcp_listener, udp_socket).await } fn spawn_udp_listener(state: Arc, socket: Arc) { @@ -89,12 +89,17 @@ async fn handle_register_udp(state: &Arc, connection_id: i32, addr: So } } -async fn accept_tcp_connection(state: Arc, listener: TcpListener) -> anyhow::Result<()> { +async fn accept_tcp_connection( + state: Arc, + listener: TcpListener, + udp_socket: Arc, +) -> anyhow::Result<()> { loop { let (socket, addr) = listener.accept().await?; info!("New connection from {}", addr); let state = state.clone(); + let udp_socket = udp_socket.clone(); tokio::spawn(async move { let _ = socket.set_nodelay(true); @@ -114,10 +119,9 @@ async fn accept_tcp_connection(state: Arc, listener: TcpListener) -> a id, state: state.clone(), rx, - writer, - udp_addr: None, + tcp_writer: TcpWriter::new(writer), + udp_writer: UdpWriter::new(udp_socket), limiter, - last_write: Instant::now(), last_read: Instant::now(), }; @@ -127,7 +131,7 @@ async fn accept_tcp_connection(state: Arc, listener: TcpListener) -> a state.remove_connection(id); - if let Some(addr) = actor.udp_addr { + if let Some(addr) = actor.udp_writer.addr { state.remove_udp(addr); } @@ -140,10 +144,9 @@ struct ConnectionActor { id: i32, state: Arc, rx: mpsc::Receiver, - writer: tokio::net::tcp::OwnedWriteHalf, - udp_addr: Option, + tcp_writer: TcpWriter, + udp_writer: UdpWriter, limiter: Arc, - last_write: Instant, last_read: Instant, } @@ -192,8 +195,7 @@ impl ConnectionActor { } // Flush batch if !batch.is_empty() { - self.writer.write_all(&batch).await?; - self.last_write = Instant::now(); + self.tcp_writer.write(&batch).await?; } } else { // Channel closed @@ -208,7 +210,7 @@ impl ConnectionActor { break; } - if self.last_write.elapsed() > Duration::from_millis(KEEP_ALIVE_INTERVAL_MS) { + if self.tcp_writer.last_write.elapsed() > Duration::from_millis(KEEP_ALIVE_INTERVAL_MS) { self.write_packet(AnyPacket::Framework(FrameworkMessage::KeepAlive)).await?; } } @@ -363,23 +365,28 @@ impl ConnectionActor { batch.extend_from_slice(&bytes); } ConnectionAction::SendUDP(p) => { - info!("Send UDP: {:?}", p); + self.udp_writer.send(p).await?; } ConnectionAction::SendTCPRaw(b) => { batch.extend_from_slice(&b); } ConnectionAction::SendUDPRaw(b) => { - info!("Send UDP raw to {}", self.id); + self.udp_writer.send_raw(&b).await?; } ConnectionAction::Close => { // Return error to break loop return Err(anyhow::anyhow!("Closed")); } ConnectionAction::RegisterUDP(addr) => { - self.udp_addr = Some(addr); + self.udp_writer.set_addr(addr); // Register in state if let Some(sender) = self.state.get_sender(self.id) { self.state.register_udp(addr, sender, self.limiter.clone()); + } else { + return Err(anyhow::anyhow!( + "No sender found for connection {}", + self.id + )); } // Send reply self.write_packet(AnyPacket::Framework(FrameworkMessage::RegisterUDP { @@ -392,9 +399,60 @@ impl ConnectionActor { } async fn write_packet(&mut self, packet: AnyPacket) -> anyhow::Result<()> { - let bytes = packet.to_bytes(); - self.writer.write_all(&bytes).await?; + self.tcp_writer.write_packet(packet).await + } +} + +struct TcpWriter { + writer: tokio::net::tcp::OwnedWriteHalf, + last_write: Instant, +} + +impl TcpWriter { + fn new(writer: tokio::net::tcp::OwnedWriteHalf) -> Self { + Self { + writer, + last_write: Instant::now(), + } + } + + async fn write(&mut self, data: &[u8]) -> anyhow::Result<()> { + self.writer.write_all(data).await?; self.last_write = Instant::now(); Ok(()) } + + async fn write_packet(&mut self, packet: AnyPacket) -> anyhow::Result<()> { + let bytes = packet.to_bytes(); + self.write(&bytes).await + } +} + +struct UdpWriter { + socket: Arc, + addr: Option, +} + +impl UdpWriter { + fn new(socket: Arc) -> Self { + Self { socket, addr: None } + } + + fn set_addr(&mut self, addr: SocketAddr) { + self.addr = Some(addr); + } + + async fn send(&self, packet: AnyPacket) -> anyhow::Result<()> { + return self.send_raw(&packet.to_bytes()).await; + } + + async fn send_raw(&self, bytes: &[u8]) -> anyhow::Result<()> { + if let Some(addr) = self.addr { + self.socket.send_to(bytes, addr).await?; + + return Ok(()); + } + + return Err(anyhow!("UPD not registered")); + } } From 06cdc89865a540fa5c06ecdbfb50edb8f1080d96 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sat, 17 Jan 2026 02:02:47 +0700 Subject: [PATCH 009/115] refactor(Dockerfile): optimize build process and remove redundant steps - Remove unnecessary user creation in build stage - Simplify dependency caching by using cargo fetch - Remove redundant mkdir and echo commands - Clean up formatting and comments --- Dockerfile | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/Dockerfile b/Dockerfile index 2c226b3..df602d2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,11 @@ # ---------- Build stage ---------- FROM rust:1.85-slim AS build - -# Create app user (optional but good practice) -RUN useradd -m rustuser - WORKDIR /app -# Cache dependencies first +# Cache dependencies COPY Cargo.toml Cargo.lock ./ -RUN mkdir src && echo "fn main() {}" > src/main.rs -RUN cargo build --release -RUN rm -rf src +RUN mkdir -p src && touch src/main.rs +RUN cargo fetch # Copy real source COPY . . @@ -18,13 +13,9 @@ RUN cargo build --release # ---------- Runtime stage ---------- FROM gcr.io/distroless/cc-debian12 - WORKDIR /app -# Copy compiled binary COPY --from=build /app/target/release/server /app/server -# Run as non-root USER nonroot - ENTRYPOINT ["/app/server"] From 80ee2f3edc818a55b3f6ab1941003d2d12790748 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sat, 17 Jan 2026 02:16:25 +0700 Subject: [PATCH 010/115] ci: update docker image tag in release workflow The tag was updated to include '-rust' suffix and use 'latest' tag instead of 'rust' to better reflect the image's purpose and version --- .github/workflows/rust-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index f0cf1b1..f7de017 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -23,4 +23,4 @@ jobs: with: context: ./ push: true - tags: ghcr.io/mindustrytool/player-connect-server:rust + tags: ghcr.io/mindustrytool/player-connect-server-rust:latest From da3ef73d47f7f887f90f49f7818e751018b27037 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sun, 18 Jan 2026 00:37:31 +0700 Subject: [PATCH 011/115] refactor(state): simplify rooms read access by adding helper method Add read() method to Rooms struct to handle RwLock read operations and error checking. Update all call sites to use the new helper method instead of directly accessing rooms field. --- src/http_server.rs | 2 +- src/proxy_server.rs | 2 +- src/state.rs | 8 ++++++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/http_server.rs b/src/http_server.rs index d2f6c9b..1d32b93 100644 --- a/src/http_server.rs +++ b/src/http_server.rs @@ -64,7 +64,7 @@ async fn room_page( State(state): State>, ) -> impl IntoResponse { let stats = { - if let Ok(rooms) = state.rooms.rooms.read() { + if let Some(rooms) = state.rooms.read() { rooms.get(&room_id).map(|r| r.stats.clone()) } else { None diff --git a/src/proxy_server.rs b/src/proxy_server.rs index 2f675d5..5e2285f 100644 --- a/src/proxy_server.rs +++ b/src/proxy_server.rs @@ -321,7 +321,7 @@ impl ConnectionActor { buffer, }) => { if let Some(room_id) = self.state.rooms.find_connection_room_id(self.id) { - if let Ok(rooms) = self.state.rooms.rooms.read() { + if let Some(rooms) = self.state.rooms.read() { let is_owner = rooms .get(&room_id) .map(|r| r.host_connection_id == self.id) diff --git a/src/state.rs b/src/state.rs index 25d5cf3..2616a5d 100644 --- a/src/state.rs +++ b/src/state.rs @@ -6,7 +6,7 @@ use bytes::Bytes; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::net::SocketAddr; -use std::sync::{Arc, RwLock}; +use std::sync::{Arc, RwLock, RwLockReadGuard}; use tokio::sync::broadcast::Receiver; use tokio::sync::mpsc; use tracing::{error, info}; @@ -54,7 +54,7 @@ pub struct Player { } pub struct Rooms { - pub rooms: RwLock>, + rooms: RwLock>, tx: tokio::sync::broadcast::Sender, } @@ -85,6 +85,10 @@ impl Rooms { .map(|(id, _)| id.clone()) } + pub fn read(&self) -> Option>> { + self.rooms.read().ok() + } + pub fn join( &self, connection_id: i32, From 93497d7889d43d2138a26f0a1448753712227c4d Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sun, 18 Jan 2026 02:52:38 +0700 Subject: [PATCH 012/115] refactor(rooms): restructure room management and connection handling - Move room update broadcasting to AppState level - Simplify Rooms struct by removing internal broadcast channel - Add packet queue for connections joining rooms - Implement proper room join/leave validation and error handling - Add rate limiting checks for non-host connections - Improve room closure and connection termination logic --- src/http_server.rs | 47 ++++++-- src/proxy_server.rs | 257 ++++++++++++++++++++++++++++++++++++++++---- src/state.rs | 35 +++--- 3 files changed, 286 insertions(+), 53 deletions(-) diff --git a/src/http_server.rs b/src/http_server.rs index 1d32b93..61c1d22 100644 --- a/src/http_server.rs +++ b/src/http_server.rs @@ -1,4 +1,4 @@ -use crate::state::{AppState, RoomUpdate}; +use crate::state::{AppState, RemoveRemoveEvent, RoomUpdate, RoomUpdateEvent}; use axum::{ extract::{Path, State}, response::{ @@ -8,7 +8,7 @@ use axum::{ routing::{get, post}, Router, }; -use futures::stream::Stream; +use futures::stream::{Stream, once}; use std::sync::Arc; use std::time::Duration; use tokio_stream::wrappers::BroadcastStream; @@ -36,25 +36,52 @@ async fn ping() -> impl IntoResponse { async fn rooms_sse( State(state): State>, ) -> Sse>> { - let rx = state.rooms.subscribe(); + let rx = state.tx.subscribe(); let stream = BroadcastStream::new(rx); - // Initial state: Send all current rooms - // We need to construct a stream that starts with current rooms and then follows updates - // For simplicity here, we just subscribe to updates. - // Ideally, we should send an initial "snapshot" event or individual add events. + let initial_rooms: Vec = { + let rooms = state.rooms.read(); - let stream = stream.map(|msg| match msg { + if let Some(rooms) = rooms { + rooms + .iter() + .map(|(key, room)| RoomUpdateEvent { + room_id: key.clone(), + data: room.stats.clone(), + }) + .collect() + } else { + vec![] + } + }; + + let init_stream = once(async move { + Event::default() + .event("update") + .json_data(initial_rooms) + .map_err(axum::BoxError::from) + }); + + let update_stream = stream.map(|msg| match msg { Ok(update) => { let event = match update { - RoomUpdate::Update(room) => Event::default().event("update").json_data(room.stats), - RoomUpdate::Remove(id) => Ok({ Event::default().event("remove").data(id) }), + RoomUpdate::Update { id, data } => { + Event::default().event("update").json_data(RoomUpdateEvent { + room_id: id.clone(), + data: data.clone(), + }) + } + RoomUpdate::Remove(id) => Event::default() + .event("remove") + .json_data(RemoveRemoveEvent { room_id: id }), }; event.map_err(|e| axum::BoxError::from(e)) } Err(e) => Err(axum::BoxError::from(e)), }); + let stream = init_stream.chain(update_stream); + Sse::new(stream) .keep_alive(axum::response::sse::KeepAlive::new().interval(Duration::from_secs(10))) } diff --git a/src/proxy_server.rs b/src/proxy_server.rs index 5e2285f..952e6c3 100644 --- a/src/proxy_server.rs +++ b/src/proxy_server.rs @@ -1,8 +1,11 @@ +use crate::constant::{ArcCloseReason, MessageType}; use crate::packets::{ - AnyPacket, AppPacket, ConnectionPacketWrapPacket, FrameworkMessage, RoomLinkPacket, + AnyPacket, AppPacket, ConnectionClosedPacket, ConnectionPacketWrapPacket, FrameworkMessage, + Message2Packet, MessagePacket, RoomLinkPacket, }; use crate::rate::AtomicRateLimiter; use crate::state::{AppState, ConnectionAction, RoomInit}; +use crate::utils::current_time_millis; use anyhow::anyhow; use bytes::{Buf, BytesMut}; use std::io::Cursor; @@ -123,6 +126,7 @@ async fn accept_tcp_connection( udp_writer: UdpWriter::new(udp_socket), limiter, last_read: Instant::now(), + packet_queue: Vec::new(), }; if let Err(e) = actor.run(reader).await { @@ -148,6 +152,7 @@ struct ConnectionActor { udp_writer: UdpWriter, limiter: Arc, last_read: Instant, + packet_queue: Vec, } impl ConnectionActor { @@ -173,11 +178,6 @@ impl ConnectionActor { Ok(n) => { self.last_read = Instant::now(); - if !self.limiter.check() { - info!("TCP Rate limit exceeded for {}", self.id); - break; - } - buf.extend_from_slice(&tmp_buf[..n]); self.process_tcp_buffer(&mut buf).await?; } @@ -243,19 +243,62 @@ impl ConnectionActor { } async fn handle_packet(&mut self, packet: AnyPacket) -> anyhow::Result<()> { - info!("Received packet: {:?}", packet); + let is_framework = matches!(packet, AnyPacket::Framework(_)); + if !is_framework { + let room_id_opt = self.state.rooms.find_connection_room_id(self.id); + let is_host = if let Some(ref room_id) = room_id_opt { + if let Some(rooms) = self.state.rooms.read() { + rooms + .get(room_id) + .map(|r| r.host_connection_id == self.id) + .unwrap_or(false) + } else { + false + } + } else { + false + }; + + if !is_host && !self.limiter.check() { + if let Some(ref room_id) = room_id_opt { + self.state.rooms.broadcast( + room_id, + ConnectionAction::SendTCP(AnyPacket::App(AppPacket::Message2( + Message2Packet { + message: MessageType::PacketSpamming, + }, + ))), + None, + ); + } + + self.write_packet(AnyPacket::App(AppPacket::ConnectionClosed( + ConnectionClosedPacket { + connection_id: self.id, + reason: ArcCloseReason::Closed, + }, + ))) + .await?; + + warn!("Connection {} disconnected for packet spamming.", self.id); + return Err(anyhow!("Packet Spamming")); + } + } match packet { AnyPacket::Framework(f) => self.handle_framework(f).await?, AnyPacket::App(a) => self.handle_app(a).await?, AnyPacket::Raw(bytes) => { - // TODO: Queue if let Some(room_id) = self.state.rooms.find_connection_room_id(self.id) { - self.state - .rooms - .broadcast(&room_id, ConnectionAction::SendTCPRaw(bytes), None); + self.state.rooms.broadcast( + &room_id, + ConnectionAction::SendTCPRaw(bytes), + Some(self.id), + ); } else { - info!("No room found for connection {}", self.id); + if self.packet_queue.len() < 16 { + self.packet_queue.push(AnyPacket::Raw(bytes)); + } } } } @@ -287,21 +330,117 @@ impl ConnectionActor { async fn handle_app(&mut self, packet: AppPacket) -> anyhow::Result<()> { match packet { + AppPacket::Stats(p) => { + if let Some(room_id) = self.state.rooms.find_connection_room_id(self.id) { + if let Ok(mut rooms) = self.state.rooms.rooms.write() { + if let Some(room) = rooms.get_mut(&room_id) { + room.stats = p.data; + room.updated_at = current_time_millis(); + } + } + } + } AppPacket::RoomJoin(p) => { - let Some(sender) = self.state.get_sender(self.id) else { - return Err(anyhow::anyhow!( - "No sender found for connection {}", - self.id - )); - }; - self.state.rooms.join(self.id, &p.room_id, sender)?; + if let Some(current_room_id) = self.state.rooms.find_connection_room_id(self.id) { + let is_host = if let Some(rooms) = self.state.rooms.read() { + rooms + .get(¤t_room_id) + .map(|r| r.host_connection_id == self.id) + .unwrap_or(false) + } else { + false + }; + + if is_host { + self.write_packet(AnyPacket::App(AppPacket::Message2(Message2Packet { + message: MessageType::AlreadyHosting, + }))) + .await?; + + warn!( + "Connection {} tried to join room {} but is already hosting {}", + self.id, p.room_id, current_room_id + ); + return Ok(()); + } + + info!( + "Connection {} left room {} to join {}", + self.id, current_room_id, p.room_id + ); + self.state.rooms.leave(self.id); + } + + let (can_join, wrong_password) = (|| { + let rooms = self.state.rooms.read()?; + let room = rooms.get(&p.room_id)?; + + if let Some(ref pass) = room.password { + if pass != &p.password { + return Some((false, true)); + } + } + + Some((true, false)) + })() + .unwrap_or((false, false)); + + if wrong_password { + info!( + "Connection {} tried to join room {} with wrong password.", + self.id, p.room_id + ); + self.write_packet(AnyPacket::App(AppPacket::Message(MessagePacket { + message: "Wrong password".to_string(), + }))) + .await?; + return Ok(()); + } + + if !can_join { + info!( + "Connection {} tried to join a non-existent room {}.", + self.id, p.room_id + ); + self.write_packet(AnyPacket::App(AppPacket::ConnectionClosed( + ConnectionClosedPacket { + connection_id: self.id, + reason: ArcCloseReason::Error, + }, + ))) + .await?; + return Ok(()); + } + + if let Some(sender) = self.state.get_sender(self.id) { + self.state.rooms.join(self.id, &p.room_id, sender)?; + info!("Connection {} joined the room {}.", self.id, p.room_id); + + for pkt in self.packet_queue.drain(..) { + self.state.rooms.broadcast( + &p.room_id, + match pkt { + AnyPacket::Raw(b) => ConnectionAction::SendTCPRaw(b), + _ => ConnectionAction::SendTCP(pkt), + }, + Some(self.id), + ); + } + } } AppPacket::RoomCreationRequest(p) => { - if let Some(room_id) = self.state.rooms.find_connection_room_id(self.id) { - info!("Connection {} already in room {}", self.id, room_id); - + if let Some(current_room_id) = self.state.rooms.find_connection_room_id(self.id) { + self.write_packet(AnyPacket::App(AppPacket::Message2(Message2Packet { + message: MessageType::AlreadyHosting, + }))) + .await?; + warn!( + "Connection {} tried to create a room but is already hosting/in the room {}.", + self.id, current_room_id + ); return Ok(()); } + if let Some(sender) = self.state.get_sender(self.id) { let room_id = self.state.rooms.create(RoomInit { connection_id: self.id, @@ -310,9 +449,81 @@ impl ConnectionActor { sender, }); self.write_packet(AnyPacket::App(AppPacket::RoomLink(RoomLinkPacket { - room_id, + room_id: room_id.clone(), }))) .await?; + info!("Room {} created by connection {}.", room_id, self.id); + } + } + AppPacket::RoomClosureRequest(_) => { + if let Some(room_id) = self.state.rooms.find_connection_room_id(self.id) { + let is_host = if let Some(rooms) = self.state.rooms.read() { + rooms + .get(&room_id) + .map(|r| r.host_connection_id == self.id) + .unwrap_or(false) + } else { + false + }; + + if !is_host { + self.write_packet(AnyPacket::App(AppPacket::Message2(Message2Packet { + message: MessageType::RoomClosureDenied, + }))) + .await?; + warn!( + "Connection {} tried to close the room {} but is not the host.", + self.id, room_id + ); + return Ok(()); + } + + self.state.rooms.close(&room_id); + info!( + "Room {} closed by connection {} (the host).", + room_id, self.id + ); + } + } + AppPacket::ConnectionClosed(p) => { + if let Some(room_id) = self.state.rooms.find_connection_room_id(self.id) { + let is_host = if let Some(rooms) = self.state.rooms.read() { + rooms + .get(&room_id) + .map(|r| r.host_connection_id == self.id) + .unwrap_or(false) + } else { + false + }; + + if !is_host { + self.write_packet(AnyPacket::App(AppPacket::Message2(Message2Packet { + message: MessageType::ConClosureDenied, + }))) + .await?; + warn!("Connection {} tried to close the connection {} but is not the host of room {}.", self.id, p.connection_id, room_id); + return Ok(()); + } + + if let Some(sender) = self.state.get_sender(p.connection_id) { + let target_room = self.state.rooms.find_connection_room_id(p.connection_id); + if target_room.as_ref() == Some(&room_id) { + info!( + "Connection {} (room {}) closed the connection {}.", + self.id, room_id, p.connection_id + ); + + let _ = sender.try_send(ConnectionAction::SendTCP(AnyPacket::App( + AppPacket::ConnectionClosed(ConnectionClosedPacket { + connection_id: p.connection_id, + reason: p.reason, + }), + ))); + let _ = sender.try_send(ConnectionAction::Close); + } else { + warn!("Connection {} (room {}) tried to close a connection from another room.", self.id, room_id); + } + } } } AppPacket::ConnectionPacketWrap(ConnectionPacketWrapPacket { diff --git a/src/state.rs b/src/state.rs index 2616a5d..b07d27d 100644 --- a/src/state.rs +++ b/src/state.rs @@ -54,8 +54,7 @@ pub struct Player { } pub struct Rooms { - rooms: RwLock>, - tx: tokio::sync::broadcast::Sender, + pub rooms: RwLock>, } pub struct RoomInit { @@ -155,7 +154,7 @@ impl Rooms { room_id } - pub fn close(&self, room_id: &String) { + pub fn close(&self, room_id: &String) -> bool { let removed = { if let Ok(mut rooms) = self.rooms.write() { rooms.remove(room_id) @@ -164,14 +163,7 @@ impl Rooms { } }; - if removed.is_some() { - let _ = self.tx.send(RoomUpdate::Remove(room_id.clone())); - info!("Room removed: {}", room_id); - } - } - - pub fn subscribe(&self) -> Receiver { - self.tx.subscribe() + removed.is_some() } pub fn broadcast(&self, room_id: &str, action: ConnectionAction, exclude_id: Option) { @@ -189,6 +181,7 @@ impl Rooms { } pub struct AppState { + pub tx: tokio::sync::broadcast::Sender, pub rooms: Rooms, pub connections: RwLock, Arc)>>, pub udp_routes: @@ -201,8 +194,8 @@ impl AppState { Self { rooms: Rooms { rooms: RwLock::new(HashMap::new()), - tx, }, + tx, connections: RwLock::new(HashMap::new()), udp_routes: RwLock::new(HashMap::new()), } @@ -296,6 +289,7 @@ impl AppState { } self.rooms.close(&room_id); + let _ = self.tx.send(RoomUpdate::Remove(room_id)); } } } @@ -303,16 +297,17 @@ impl AppState { #[derive(Clone, Debug)] pub enum RoomUpdate { - Update(RoomDisplay), + Update { id: String, data: Stats }, Remove(String), } #[derive(Clone, Debug, Serialize)] -pub struct RoomDisplay { - pub id: String, - pub host_connection_id: i32, - pub password: bool, - pub players: usize, - pub max_players: usize, - pub stats: Stats, +pub struct RemoveRemoveEvent { + pub room_id: String, +} + +#[derive(Clone, Debug, Serialize)] +pub struct RoomUpdateEvent { + pub room_id: String, + pub data: Stats, } From d9c08eb2cf5808b9f1dcc3819e61148566a2e60f Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sun, 18 Jan 2026 02:56:25 +0700 Subject: [PATCH 013/115] feat(proxy_server): notify state on room stats update Add RoomUpdate message sending when ro --- src/proxy_server.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/proxy_server.rs b/src/proxy_server.rs index 952e6c3..0adf21b 100644 --- a/src/proxy_server.rs +++ b/src/proxy_server.rs @@ -4,7 +4,7 @@ use crate::packets::{ Message2Packet, MessagePacket, RoomLinkPacket, }; use crate::rate::AtomicRateLimiter; -use crate::state::{AppState, ConnectionAction, RoomInit}; +use crate::state::{AppState, ConnectionAction, RoomInit, RoomUpdate}; use crate::utils::current_time_millis; use anyhow::anyhow; use bytes::{Buf, BytesMut}; @@ -336,6 +336,11 @@ impl ConnectionActor { if let Some(room) = rooms.get_mut(&room_id) { room.stats = p.data; room.updated_at = current_time_millis(); + + self.state.tx.send(RoomUpdate::Update { + id: room.id.clone(), + data: room.stats.clone(), + })?; } } } From a2c3eec3f18cd75115ed80915c88405603a60197 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sun, 18 Jan 2026 03:22:34 +0700 Subject: [PATCH 014/115] refactor(http_server,proxy_server): improve error handling and code organization - Simplify SSE keep-alive initialization in http_server - Add error logging for room updates and packet forwarding in proxy_server --- src/http_server.rs | 7 +++---- src/proxy_server.rs | 46 +++++++++++++++++++++++++++++++++------------ src/state.rs | 4 +++- 3 files changed, 40 insertions(+), 17 deletions(-) diff --git a/src/http_server.rs b/src/http_server.rs index 61c1d22..f878140 100644 --- a/src/http_server.rs +++ b/src/http_server.rs @@ -2,13 +2,13 @@ use crate::state::{AppState, RemoveRemoveEvent, RoomUpdate, RoomUpdateEvent}; use axum::{ extract::{Path, State}, response::{ - sse::{Event, Sse}, + sse::{Event, KeepAlive, Sse}, Html, IntoResponse, }, routing::{get, post}, Router, }; -use futures::stream::{Stream, once}; +use futures::stream::{once, Stream}; use std::sync::Arc; use std::time::Duration; use tokio_stream::wrappers::BroadcastStream; @@ -82,8 +82,7 @@ async fn rooms_sse( let stream = init_stream.chain(update_stream); - Sse::new(stream) - .keep_alive(axum::response::sse::KeepAlive::new().interval(Duration::from_secs(10))) + Sse::new(stream).keep_alive(KeepAlive::new().interval(Duration::from_secs(10))) } async fn room_page( diff --git a/src/proxy_server.rs b/src/proxy_server.rs index 0adf21b..ec7e53b 100644 --- a/src/proxy_server.rs +++ b/src/proxy_server.rs @@ -64,7 +64,11 @@ fn spawn_udp_listener(state: Arc, socket: Arc) { // Normal packet, route it if let Some((sender, limiter)) = state.get_route(&addr) { if limiter.check() { - let _ = sender.try_send(ConnectionAction::SendTCP(packet)); + if let Err(e) = + sender.try_send(ConnectionAction::SendTCP(packet)) + { + info!("Failed to forward UDP packet: {}", e); + } } } else { // Unknown UDP sender, ignore @@ -88,7 +92,12 @@ async fn handle_register_udp(state: &Arc, connection_id: i32, addr: So "Registering UDP for connection {} at {}", connection_id, addr ); - let _ = sender.try_send(ConnectionAction::RegisterUDP(addr)); + if let Err(e) = sender.try_send(ConnectionAction::RegisterUDP(addr)) { + info!( + "Failed to register UDP for connection {}: {}", + connection_id, e + ); + } } } @@ -337,10 +346,12 @@ impl ConnectionActor { room.stats = p.data; room.updated_at = current_time_millis(); - self.state.tx.send(RoomUpdate::Update { + if let Err(err) = self.state.tx.send(RoomUpdate::Update { id: room.id.clone(), data: room.stats.clone(), - })?; + }) { + info!("Fail to broadcast room update {}", err); + } } } } @@ -518,13 +529,22 @@ impl ConnectionActor { self.id, room_id, p.connection_id ); - let _ = sender.try_send(ConnectionAction::SendTCP(AnyPacket::App( - AppPacket::ConnectionClosed(ConnectionClosedPacket { - connection_id: p.connection_id, - reason: p.reason, - }), - ))); - let _ = sender.try_send(ConnectionAction::Close); + if let Err(e) = + sender.try_send(ConnectionAction::SendTCP(AnyPacket::App( + AppPacket::ConnectionClosed(ConnectionClosedPacket { + connection_id: p.connection_id, + reason: p.reason, + }), + ))) + { + info!( + "Failed to send connection closed packet to {}: {}", + p.connection_id, e + ); + } + if let Err(e) = sender.try_send(ConnectionAction::Close) { + info!("Failed to send close action to {}: {}", p.connection_id, e); + } } else { warn!("Connection {} (room {}) tried to close a connection from another room.", self.id, room_id); } @@ -557,7 +577,9 @@ impl ConnectionActor { return Err(anyhow!("Connection not found: {}", connection_id)); }; - sender.try_send(action)?; + if let Err(e) = sender.try_send(action) { + warn!("Failed to forward packet to {}: {}", connection_id, e); + } } } else { info!("No room found for connection {}", self.id); diff --git a/src/state.rs b/src/state.rs index b07d27d..70e2e30 100644 --- a/src/state.rs +++ b/src/state.rs @@ -173,7 +173,9 @@ impl Rooms { if Some(*id) == exclude_id { continue; } - let _ = sender.try_send(action.clone()); + if let Err(e) = sender.try_send(action.clone()) { + info!("Failed to broadcast to {}: {}", id, e); + } } } } From b72a8b208e9631efcc80aff5139ec26ed1a645fa Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sun, 18 Jan 2026 03:25:12 +0700 Subject: [PATCH 015/115] refactor(proxy_server): improve connection logging with unique IDs Move connection logging after socket setup and include unique connection ID to help track individual connections more effectively --- src/proxy_server.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/proxy_server.rs b/src/proxy_server.rs index ec7e53b..c404818 100644 --- a/src/proxy_server.rs +++ b/src/proxy_server.rs @@ -108,7 +108,6 @@ async fn accept_tcp_connection( ) -> anyhow::Result<()> { loop { let (socket, addr) = listener.accept().await?; - info!("New connection from {}", addr); let state = state.clone(); let udp_socket = udp_socket.clone(); @@ -117,6 +116,8 @@ async fn accept_tcp_connection( let _ = socket.set_nodelay(true); let id = NEXT_CONNECTION_ID.fetch_add(1, Ordering::Relaxed); + info!("New connection {} from {}", id, addr); + let (tx, rx) = mpsc::channel(CHANNEL_CAPACITY); let limiter = Arc::new(AtomicRateLimiter::new( PACKET_RATE_LIMIT, From a19ee836a1f9c3a9a93c4f058cb19b7c67c6bf75 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sun, 18 Jan 2026 04:04:34 +0700 Subject: [PATCH 016/115] refactor(proxy): improve connection handling and logging - Increase broadcast channel capacity from 100 to 1024 for better performance - Remove redundant logging for connection registration and cleanup - Move connection logging to RegisterUDP action for more accurate timing - Add duplicate registration check in RegisterUDP handler - Clean up unused imports and variables --- src/http_server.rs | 23 ++++++++++++++--------- src/proxy_server.rs | 21 +++++++++++---------- src/state.rs | 16 +++++++++------- 3 files changed, 34 insertions(+), 26 deletions(-) diff --git a/src/http_server.rs b/src/http_server.rs index f878140..ab2b447 100644 --- a/src/http_server.rs +++ b/src/http_server.rs @@ -11,6 +11,8 @@ use axum::{ use futures::stream::{once, Stream}; use std::sync::Arc; use std::time::Duration; +use tokio::sync::broadcast; +use tokio_stream::wrappers::errors::BroadcastStreamRecvError; use tokio_stream::wrappers::BroadcastStream; use tokio_stream::StreamExt; use tracing::info; @@ -36,7 +38,7 @@ async fn ping() -> impl IntoResponse { async fn rooms_sse( State(state): State>, ) -> Sse>> { - let rx = state.tx.subscribe(); + let rx = state.rooms.tx.subscribe(); let stream = BroadcastStream::new(rx); let initial_rooms: Vec = { @@ -62,22 +64,25 @@ async fn rooms_sse( .map_err(axum::BoxError::from) }); - let update_stream = stream.map(|msg| match msg { + let update_stream = stream.filter_map(|msg| match msg { Ok(update) => { - let event = match update { - RoomUpdate::Update { id, data } => { - Event::default().event("update").json_data(RoomUpdateEvent { + let data = match update { + RoomUpdate::Update { id, data } => Event::default() + .event("update") + .json_data(RoomUpdateEvent { room_id: id.clone(), data: data.clone(), }) - } + .map_err(axum::BoxError::from), RoomUpdate::Remove(id) => Event::default() .event("remove") - .json_data(RemoveRemoveEvent { room_id: id }), + .json_data(RemoveRemoveEvent { room_id: id }) + .map_err(axum::BoxError::from), }; - event.map_err(|e| axum::BoxError::from(e)) + + Some(data) } - Err(e) => Err(axum::BoxError::from(e)), + Err(BroadcastStreamRecvError::Lagged(_)) => None, }); let stream = init_stream.chain(update_stream); diff --git a/src/proxy_server.rs b/src/proxy_server.rs index c404818..ebdb9b3 100644 --- a/src/proxy_server.rs +++ b/src/proxy_server.rs @@ -88,10 +88,6 @@ fn spawn_udp_listener(state: Arc, socket: Arc) { async fn handle_register_udp(state: &Arc, connection_id: i32, addr: SocketAddr) { if let Some(sender) = state.get_sender(connection_id) { - info!( - "Registering UDP for connection {} at {}", - connection_id, addr - ); if let Err(e) = sender.try_send(ConnectionAction::RegisterUDP(addr)) { info!( "Failed to register UDP for connection {}: {}", @@ -107,7 +103,7 @@ async fn accept_tcp_connection( udp_socket: Arc, ) -> anyhow::Result<()> { loop { - let (socket, addr) = listener.accept().await?; + let (socket, _) = listener.accept().await?; let state = state.clone(); let udp_socket = udp_socket.clone(); @@ -116,8 +112,6 @@ async fn accept_tcp_connection( let _ = socket.set_nodelay(true); let id = NEXT_CONNECTION_ID.fetch_add(1, Ordering::Relaxed); - info!("New connection {} from {}", id, addr); - let (tx, rx) = mpsc::channel(CHANNEL_CAPACITY); let limiter = Arc::new(AtomicRateLimiter::new( PACKET_RATE_LIMIT, @@ -147,9 +141,8 @@ async fn accept_tcp_connection( if let Some(addr) = actor.udp_writer.addr { state.remove_udp(addr); + info!("Connection {} closed", id); } - - info!("Connection {} cleanup complete", id); }); } } @@ -226,6 +219,7 @@ impl ConnectionActor { } } } + Ok(()) } @@ -347,7 +341,7 @@ impl ConnectionActor { room.stats = p.data; room.updated_at = current_time_millis(); - if let Err(err) = self.state.tx.send(RoomUpdate::Update { + if let Err(err) = self.state.rooms.tx.send(RoomUpdate::Update { id: room.id.clone(), data: room.stats.clone(), }) { @@ -617,7 +611,14 @@ impl ConnectionActor { return Err(anyhow::anyhow!("Closed")); } ConnectionAction::RegisterUDP(addr) => { + if self.udp_writer.addr.is_some() { + return Ok(()); + } + self.udp_writer.set_addr(addr); + + info!("New connection {} from {}", self.id, addr); + // Register in state if let Some(sender) = self.state.get_sender(self.id) { self.state.register_udp(addr, sender, self.limiter.clone()); diff --git a/src/state.rs b/src/state.rs index 70e2e30..4ea9414 100644 --- a/src/state.rs +++ b/src/state.rs @@ -7,7 +7,6 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::net::SocketAddr; use std::sync::{Arc, RwLock, RwLockReadGuard}; -use tokio::sync::broadcast::Receiver; use tokio::sync::mpsc; use tracing::{error, info}; use uuid::Uuid; @@ -55,6 +54,7 @@ pub struct Player { pub struct Rooms { pub rooms: RwLock>, + pub tx: tokio::sync::broadcast::Sender, } pub struct RoomInit { @@ -154,7 +154,7 @@ impl Rooms { room_id } - pub fn close(&self, room_id: &String) -> bool { + pub fn close(&self, room_id: &String) { let removed = { if let Ok(mut rooms) = self.rooms.write() { rooms.remove(room_id) @@ -163,7 +163,11 @@ impl Rooms { } }; - removed.is_some() + if removed.is_some() { + if let Err(err) = self.tx.send(RoomUpdate::Remove(room_id.clone())) { + error!("Failed to send remove room event: {}", err); + }; + } } pub fn broadcast(&self, room_id: &str, action: ConnectionAction, exclude_id: Option) { @@ -183,7 +187,6 @@ impl Rooms { } pub struct AppState { - pub tx: tokio::sync::broadcast::Sender, pub rooms: Rooms, pub connections: RwLock, Arc)>>, pub udp_routes: @@ -192,12 +195,12 @@ pub struct AppState { impl AppState { pub fn new() -> Self { - let (tx, _) = tokio::sync::broadcast::channel(100); + let (tx, _) = tokio::sync::broadcast::channel(1024); Self { rooms: Rooms { rooms: RwLock::new(HashMap::new()), + tx, }, - tx, connections: RwLock::new(HashMap::new()), udp_routes: RwLock::new(HashMap::new()), } @@ -291,7 +294,6 @@ impl AppState { } self.rooms.close(&room_id); - let _ = self.tx.send(RoomUpdate::Remove(room_id)); } } } From f631ef76b1f84304d3ec0b877b34ffbc099a0ca4 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sun, 18 Jan 2026 04:13:08 +0700 Subject: [PATCH 017/115] fix(state): prevent broadcast channel from closing prematurely Add a receiver field to Rooms struct to maintain channel when clients disconnect --- src/http_server.rs | 1 - src/state.rs | 5 ++++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/http_server.rs b/src/http_server.rs index ab2b447..c61de63 100644 --- a/src/http_server.rs +++ b/src/http_server.rs @@ -11,7 +11,6 @@ use axum::{ use futures::stream::{once, Stream}; use std::sync::Arc; use std::time::Duration; -use tokio::sync::broadcast; use tokio_stream::wrappers::errors::BroadcastStreamRecvError; use tokio_stream::wrappers::BroadcastStream; use tokio_stream::StreamExt; diff --git a/src/state.rs b/src/state.rs index 4ea9414..778d86b 100644 --- a/src/state.rs +++ b/src/state.rs @@ -55,6 +55,8 @@ pub struct Player { pub struct Rooms { pub rooms: RwLock>, pub tx: tokio::sync::broadcast::Sender, + // Keep a receiver to prevent the channel from closing when all clients disconnect + pub _rx: tokio::sync::broadcast::Receiver, } pub struct RoomInit { @@ -195,11 +197,12 @@ pub struct AppState { impl AppState { pub fn new() -> Self { - let (tx, _) = tokio::sync::broadcast::channel(1024); + let (tx, rx) = tokio::sync::broadcast::channel(1024); Self { rooms: Rooms { rooms: RwLock::new(HashMap::new()), tx, + _rx: rx, }, connections: RwLock::new(HashMap::new()), udp_routes: RwLock::new(HashMap::new()), From 802d617537ecd68e78afaf724f1614b9b0c3afc6 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sun, 18 Jan 2026 04:25:55 +0700 Subject: [PATCH 018/115] perf(http_server): reduce SSE keep-alive interval to 3 seconds Improve real-time updates responsiveness by reducing the keep-alive interval from 10 to 3 seconds and adding keep-alive text --- src/http_server.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/http_server.rs b/src/http_server.rs index c61de63..5eb3c8c 100644 --- a/src/http_server.rs +++ b/src/http_server.rs @@ -86,7 +86,11 @@ async fn rooms_sse( let stream = init_stream.chain(update_stream); - Sse::new(stream).keep_alive(KeepAlive::new().interval(Duration::from_secs(10))) + Sse::new(stream).keep_alive( + KeepAlive::new() + .interval(Duration::from_secs(3)) + .text(": keep-alive\n\n"), + ) } async fn room_page( From b26100326a72bf4ebfff66a3fe1d1edac84ebb01 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sun, 18 Jan 2026 04:33:55 +0700 Subject: [PATCH 019/115] perf(http): remove redundant keep-alive text to optimize SSE --- src/http_server.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/http_server.rs b/src/http_server.rs index 5eb3c8c..0a67a6c 100644 --- a/src/http_server.rs +++ b/src/http_server.rs @@ -86,11 +86,7 @@ async fn rooms_sse( let stream = init_stream.chain(update_stream); - Sse::new(stream).keep_alive( - KeepAlive::new() - .interval(Duration::from_secs(3)) - .text(": keep-alive\n\n"), - ) + Sse::new(stream).keep_alive(KeepAlive::new().interval(Duration::from_secs(3))) } async fn room_page( From ee3b3521ee1797bcc630aed58d4441ec8f89ea38 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sun, 18 Jan 2026 14:19:15 +0700 Subject: [PATCH 020/115] refactor(packets): replace manual stats parsing with serde_json The read_stats function now uses serde_json for proper JSON parsing instead of returning a dummy Stats struct. This improves reliability and maintains consistency with the actual data format. --- src/packets.rs | 34 ++++++++++++++-------------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/src/packets.rs b/src/packets.rs index b9ce022..9e692ae 100644 --- a/src/packets.rs +++ b/src/packets.rs @@ -2,7 +2,7 @@ use crate::{ constant::{ArcCloseReason, CloseReason, MessageType}, state::Stats, }; -use anyhow::{anyhow, Result}; +use anyhow::anyhow; use bytes::{Buf, BufMut, Bytes, BytesMut}; use std::io::Cursor; use tracing::info; @@ -115,7 +115,7 @@ pub struct StatsPacket { } impl AnyPacket { - pub fn read(buf: &mut Cursor<&[u8]>) -> Result { + pub fn read(buf: &mut Cursor<&[u8]>) -> anyhow::Result { if !buf.has_remaining() { return Err(anyhow!("Empty packet")); } @@ -161,7 +161,7 @@ impl AnyPacket { } impl FrameworkMessage { - pub fn read(buf: &mut Cursor<&[u8]>) -> Result { + pub fn read(buf: &mut Cursor<&[u8]>) -> anyhow::Result { let fid = buf.get_u8(); match fid { @@ -205,7 +205,7 @@ impl FrameworkMessage { } impl AppPacket { - pub fn read(buf: &mut Cursor<&[u8]>) -> Result { + pub fn read(buf: &mut Cursor<&[u8]>) -> anyhow::Result { let pid = buf.get_u8(); match pid { @@ -343,7 +343,7 @@ impl AppPacket { } // Helper to read/write strings -pub fn read_string(buf: &mut Cursor<&[u8]>) -> Result { +pub fn read_string(buf: &mut Cursor<&[u8]>) -> anyhow::Result { if buf.remaining() < 2 { return Err(anyhow!( "Not enough bytes for string length: {}", @@ -374,22 +374,16 @@ pub fn write_string(buf: &mut BytesMut, s: &str) { buf.put_slice(bytes); } -pub fn read_stats(buf: &mut Cursor<&[u8]>) -> Result { +pub fn read_stats(buf: &mut Cursor<&[u8]>) -> anyhow::Result { let json = read_string(buf)?; - info!("Stats JSON: {}", json); - - // let data = serde_json::from_str(json.as_str()); - - Ok(Stats { - players: vec![], - map_name: String::new(), - name: String::new(), - gamemode: String::new(), - mods: vec![], - locale: String::new(), - version: String::new(), - created_at: 0, - }) + + match serde_json::from_str::(&json) { + Ok(data) => Ok(data), + Err(e) => { + info!("Failed to parse stats: {}", json); + Err(anyhow!(e)) + } + } } pub fn write_stats(buf: &mut BytesMut, stats: &Stats) { From 3652505cac393f0a2a7ca7afb49260fe69ca1a38 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sun, 18 Jan 2026 14:46:06 +0700 Subject: [PATCH 021/115] refactor(http_server): simplify rooms_sse response handling Replace Stream return type with IntoResponse and add required headers directly --- src/http_server.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/http_server.rs b/src/http_server.rs index 0a67a6c..f521b3f 100644 --- a/src/http_server.rs +++ b/src/http_server.rs @@ -1,6 +1,7 @@ use crate::state::{AppState, RemoveRemoveEvent, RoomUpdate, RoomUpdateEvent}; use axum::{ extract::{Path, State}, + http::header, response::{ sse::{Event, KeepAlive, Sse}, Html, IntoResponse, @@ -8,7 +9,7 @@ use axum::{ routing::{get, post}, Router, }; -use futures::stream::{once, Stream}; +use futures::stream::once; use std::sync::Arc; use std::time::Duration; use tokio_stream::wrappers::errors::BroadcastStreamRecvError; @@ -34,9 +35,7 @@ async fn ping() -> impl IntoResponse { "OK" } -async fn rooms_sse( - State(state): State>, -) -> Sse>> { +async fn rooms_sse(State(state): State>) -> impl IntoResponse { let rx = state.rooms.tx.subscribe(); let stream = BroadcastStream::new(rx); @@ -86,7 +85,14 @@ async fn rooms_sse( let stream = init_stream.chain(update_stream); - Sse::new(stream).keep_alive(KeepAlive::new().interval(Duration::from_secs(3))) + ( + [ + (header::CONTENT_TYPE, "text/event-stream"), + (header::CACHE_CONTROL, "no-cache"), + (header::CONNECTION, "keep-alive"), + ], + Sse::new(stream).keep_alive(KeepAlive::new().interval(Duration::from_secs(3))), + ) } async fn room_page( From ed3c73442c0ee4f40b4827027a9b85c856563ebb Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sun, 18 Jan 2026 15:00:07 +0700 Subject: [PATCH 022/115] refactor(http): simplify API endpoint paths Remove version prefix from API endpoints to make them more concise and consistent with current API design standards. --- src/http_server.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/http_server.rs b/src/http_server.rs index f521b3f..c5936b0 100644 --- a/src/http_server.rs +++ b/src/http_server.rs @@ -19,8 +19,8 @@ use tracing::info; pub async fn run(state: Arc, port: u16) -> anyhow::Result<()> { let app = Router::new() - .route("/api/v1/ping", get(ping)) - .route("/api/v1/rooms", get(rooms_sse)) + .route("/ping", get(ping)) + .route("/rooms", get(rooms_sse)) .route("/:roomId", get(room_page)) .route("/:roomId", post(room_port)) .with_state(state); From 5b8bfe895f0fab06f6c63343c88552d0b1a06e40 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sun, 18 Jan 2026 15:33:47 +0700 Subject: [PATCH 023/115] feat(room): add ping tracking and room view model - Change created_at from i64 to u128 in Stats for consistency - Add ping field to Room to track latency - Introduce RoomView struct to encapsulate room data for API responses - Update room update events to use RoomView instead of Stats --- src/http_server.rs | 6 +++--- src/packets.rs | 2 +- src/proxy_server.rs | 5 ++++- src/state.rs | 48 +++++++++++++++++++++++++++++++++++++++++---- 4 files changed, 52 insertions(+), 9 deletions(-) diff --git a/src/http_server.rs b/src/http_server.rs index c5936b0..d90bb7d 100644 --- a/src/http_server.rs +++ b/src/http_server.rs @@ -1,4 +1,4 @@ -use crate::state::{AppState, RemoveRemoveEvent, RoomUpdate, RoomUpdateEvent}; +use crate::state::{AppState, RemoveRemoveEvent, RoomUpdate, RoomUpdateEvent, RoomView}; use axum::{ extract::{Path, State}, http::header, @@ -47,7 +47,7 @@ async fn rooms_sse(State(state): State>) -> impl IntoResponse { .iter() .map(|(key, room)| RoomUpdateEvent { room_id: key.clone(), - data: room.stats.clone(), + data: RoomView::from(room), }) .collect() } else { @@ -69,7 +69,7 @@ async fn rooms_sse(State(state): State>) -> impl IntoResponse { .event("update") .json_data(RoomUpdateEvent { room_id: id.clone(), - data: data.clone(), + data: RoomView::from(&data), }) .map_err(axum::BoxError::from), RoomUpdate::Remove(id) => Event::default() diff --git a/src/packets.rs b/src/packets.rs index 9e692ae..40baddf 100644 --- a/src/packets.rs +++ b/src/packets.rs @@ -398,5 +398,5 @@ pub fn write_stats(buf: &mut BytesMut, stats: &Stats) { // write_string(buf, &stats.mods); // TODO write_string(buf, &stats.locale); write_string(buf, &stats.version); - buf.put_i64(stats.created_at); + buf.put_u128(stats.created_at); } diff --git a/src/proxy_server.rs b/src/proxy_server.rs index ebdb9b3..a14e89f 100644 --- a/src/proxy_server.rs +++ b/src/proxy_server.rs @@ -338,12 +338,15 @@ impl ConnectionActor { if let Some(room_id) = self.state.rooms.find_connection_room_id(self.id) { if let Ok(mut rooms) = self.state.rooms.rooms.write() { if let Some(room) = rooms.get_mut(&room_id) { + let sent_at = p.data.created_at; + room.stats = p.data; room.updated_at = current_time_millis(); + room.ping = current_time_millis() - sent_at; if let Err(err) = self.state.rooms.tx.send(RoomUpdate::Update { id: room.id.clone(), - data: room.stats.clone(), + data: room.clone(), }) { info!("Fail to broadcast room update {}", err); } diff --git a/src/state.rs b/src/state.rs index 778d86b..8cd2540 100644 --- a/src/state.rs +++ b/src/state.rs @@ -21,7 +21,7 @@ pub enum ConnectionAction { RegisterUDP(SocketAddr), } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Room { pub id: String, pub host_connection_id: i32, @@ -30,6 +30,7 @@ pub struct Room { pub updated_at: u128, pub members: HashMap>, pub stats: Stats, + pub ping: u128, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -43,7 +44,7 @@ pub struct Stats { pub locale: String, pub version: String, #[serde(rename = "createdAt")] - pub created_at: i64, + pub created_at: u128, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -147,6 +148,7 @@ impl Rooms { members, created_at: current_time_millis(), updated_at: current_time_millis(), + ping: 0, }; if let Ok(mut rooms) = self.rooms.write() { @@ -304,7 +306,7 @@ impl AppState { #[derive(Clone, Debug)] pub enum RoomUpdate { - Update { id: String, data: Stats }, + Update { id: String, data: Room }, Remove(String), } @@ -316,5 +318,43 @@ pub struct RemoveRemoveEvent { #[derive(Clone, Debug, Serialize)] pub struct RoomUpdateEvent { pub room_id: String, - pub data: Stats, + pub data: RoomView, +} + +#[derive(Clone, Debug, Serialize)] +pub struct RoomView { + pub name: String, + pub status: String, + #[serde(rename = "isPrivate")] + pub is_private: bool, + pub is_secured: bool, + pub players: Vec, + #[serde(rename = "mapName")] + pub map_name: String, + pub gamemode: String, + pub mods: Vec, + pub locale: String, + pub version: String, + #[serde(rename = "createdAt")] + pub created_at: u128, + pub ping: u128, +} + +impl RoomView { + pub fn from(room: &Room) -> Self { + Self { + name: room.stats.name.clone(), + status: "UP".to_string(), + is_private: false, + is_secured: room.password.is_some(), + players: room.stats.players.clone(), + map_name: room.stats.map_name.clone(), + gamemode: room.stats.gamemode.clone(), + mods: room.stats.mods.clone(), + locale: room.stats.locale.clone(), + version: room.stats.version.clone(), + created_at: room.stats.created_at, + ping: room.ping, + } + } } From 4aee19c2f257384db9b7d9fbf99829ee21c12cf0 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sun, 18 Jan 2026 15:49:25 +0700 Subject: [PATCH 024/115] fix(serialization): add missing serde rename attributes Add `#[serde(rename)]` attributes to fields to ensure proper JSON serialization. This matches the expected API contract where snake_case fields should be serialized as camelCase. --- src/state.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/state.rs b/src/state.rs index 8cd2540..0a46a19 100644 --- a/src/state.rs +++ b/src/state.rs @@ -312,11 +312,13 @@ pub enum RoomUpdate { #[derive(Clone, Debug, Serialize)] pub struct RemoveRemoveEvent { + #[serde(rename = "roomId")] pub room_id: String, } #[derive(Clone, Debug, Serialize)] pub struct RoomUpdateEvent { + #[serde(rename = "roomId")] pub room_id: String, pub data: RoomView, } @@ -327,6 +329,7 @@ pub struct RoomView { pub status: String, #[serde(rename = "isPrivate")] pub is_private: bool, + #[serde(rename = "isSecured")] pub is_secured: bool, pub players: Vec, #[serde(rename = "mapName")] From 22d08517d368112401f35ac01d3a8bfd12fe0fc4 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sun, 18 Jan 2026 16:30:15 +0700 Subject: [PATCH 025/115] fix(http_server): wrap RoomUpdateEvent in vector for SSE response The RoomUpdateEvent was being sent as a single object in the SSE stream, but client expectations require it to be wrapped in a vector for consistency with other event types. This change aligns the data format with client-side processing requirements. --- src/http_server.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/http_server.rs b/src/http_server.rs index d90bb7d..710ee4f 100644 --- a/src/http_server.rs +++ b/src/http_server.rs @@ -67,10 +67,10 @@ async fn rooms_sse(State(state): State>) -> impl IntoResponse { let data = match update { RoomUpdate::Update { id, data } => Event::default() .event("update") - .json_data(RoomUpdateEvent { + .json_data(vec![RoomUpdateEvent { room_id: id.clone(), data: RoomView::from(&data), - }) + }]) .map_err(axum::BoxError::from), RoomUpdate::Remove(id) => Event::default() .event("remove") From f78459c4636d8fbf5f40d41db1f0fdd3f58eeba8 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sun, 18 Jan 2026 16:49:48 +0700 Subject: [PATCH 026/115] fix(proxy_server): add warning logs for unhandled packets and queue full Add warning logs for unknown UDP senders, full packet queues, and unhandled framework/app packets to improve debugging visibility --- src/proxy_server.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/proxy_server.rs b/src/proxy_server.rs index a14e89f..898ac27 100644 --- a/src/proxy_server.rs +++ b/src/proxy_server.rs @@ -72,6 +72,7 @@ fn spawn_udp_listener(state: Arc, socket: Arc) { } } else { // Unknown UDP sender, ignore + warn!("Unknown UDP sender: {}", addr); } } } @@ -302,6 +303,11 @@ impl ConnectionActor { } else { if self.packet_queue.len() < 16 { self.packet_queue.push(AnyPacket::Raw(bytes)); + } else { + warn!( + "Connection {} packet queue full, dropping raw packet", + self.id + ); } } } @@ -327,7 +333,9 @@ impl ConnectionActor { // Should not happen via TCP? // But if it does, ignore? } - _ => {} + _ => { + warn!("Unhandled Framework Packet: {:?}", packet); + } } Ok(()) } @@ -339,7 +347,7 @@ impl ConnectionActor { if let Ok(mut rooms) = self.state.rooms.rooms.write() { if let Some(room) = rooms.get_mut(&room_id) { let sent_at = p.data.created_at; - + room.stats = p.data; room.updated_at = current_time_millis(); room.ping = current_time_millis() - sent_at; @@ -584,7 +592,7 @@ impl ConnectionActor { } } _ => { - info!("Unhandled {:?}", packet); + warn!("Unhandled App Packet: {:?}", packet); } } Ok(()) From aff15606d99955de21c415b20050aa6cc980e30c Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sun, 18 Jan 2026 16:57:41 +0700 Subject: [PATCH 027/115] fix(proxy_server): handle room creation and update properly Ensure room data is properly read and broadcasted after creation. Add error handling for room lookup and update broadcasting to prevent silent failures. --- src/proxy_server.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/proxy_server.rs b/src/proxy_server.rs index 898ac27..64bd517 100644 --- a/src/proxy_server.rs +++ b/src/proxy_server.rs @@ -474,6 +474,22 @@ impl ConnectionActor { room_id: room_id.clone(), }))) .await?; + + let Some(rooms) = self.state.rooms.read() else { + return Err(anyhow!("Can not read rooms")); + }; + + let Some(room) = rooms.get(&room_id) else { + return Err(anyhow!("Can not find room {}", room_id)); + }; + + if let Err(err) = self.state.rooms.tx.send(RoomUpdate::Update { + id: room.id.clone(), + data: room.clone(), + }) { + info!("Fail to broadcast room update {}", err); + } + info!("Room {} created by connection {}.", room_id, self.id); } } From 0e827dea450d5e8d67241e82a02d4417cfbf73fe Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sun, 18 Jan 2026 17:00:26 +0700 Subject: [PATCH 028/115] feat: add logging for received UDP and TCP packets Add debug logging to track incoming UDP and TCP packets in proxy server for better debugging and monitoring --- src/proxy_server.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/proxy_server.rs b/src/proxy_server.rs index 64bd517..4f39735 100644 --- a/src/proxy_server.rs +++ b/src/proxy_server.rs @@ -54,6 +54,8 @@ fn spawn_udp_listener(state: Arc, socket: Arc) { match AnyPacket::read(&mut cursor) { Ok(packet) => { + info!("Received UDP packet: {:?}", packet); + if let AnyPacket::Framework(FrameworkMessage::RegisterUDP { connection_id, }) = packet @@ -241,6 +243,8 @@ impl ConnectionActor { let mut cursor = Cursor::new(&payload[..]); let packet = AnyPacket::read(&mut cursor)?; + + info!("Received TCP packet: {:?}", packet); // Handle packet self.handle_packet(packet).await?; } From 5cffc9ca3020a32fd507723773e46d37acd4d7f9 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sun, 18 Jan 2026 17:26:58 +0700 Subject: [PATCH 029/115] fuck wifi --- src/proxy_server.rs | 30 ++++++++++++++++--------- src/state.rs | 55 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 73 insertions(+), 12 deletions(-) diff --git a/src/proxy_server.rs b/src/proxy_server.rs index 4f39735..b1b4d79 100644 --- a/src/proxy_server.rs +++ b/src/proxy_server.rs @@ -1,7 +1,6 @@ -use crate::constant::{ArcCloseReason, MessageType}; +use crate::constant::{ArcCloseReason, CloseReason, MessageType}; use crate::packets::{ - AnyPacket, AppPacket, ConnectionClosedPacket, ConnectionPacketWrapPacket, FrameworkMessage, - Message2Packet, MessagePacket, RoomLinkPacket, + AnyPacket, AppPacket, ConnectionClosedPacket, ConnectionPacketWrapPacket, FrameworkMessage, Message2Packet, MessagePacket, RoomClosedPacket, RoomLinkPacket }; use crate::rate::AtomicRateLimiter; use crate::state::{AppState, ConnectionAction, RoomInit, RoomUpdate}; @@ -243,7 +242,7 @@ impl ConnectionActor { let mut cursor = Cursor::new(&payload[..]); let packet = AnyPacket::read(&mut cursor)?; - + info!("Received TCP packet: {:?}", packet); // Handle packet self.handle_packet(packet).await?; @@ -299,11 +298,9 @@ impl ConnectionActor { AnyPacket::App(a) => self.handle_app(a).await?, AnyPacket::Raw(bytes) => { if let Some(room_id) = self.state.rooms.find_connection_room_id(self.id) { - self.state.rooms.broadcast( - &room_id, - ConnectionAction::SendTCPRaw(bytes), - Some(self.id), - ); + self.state + .rooms + .forward_to_host(&room_id, ConnectionAction::SendTCPRaw(bytes)); } else { if self.packet_queue.len() < 16 { self.packet_queue.push(AnyPacket::Raw(bytes)); @@ -443,13 +440,12 @@ impl ConnectionActor { info!("Connection {} joined the room {}.", self.id, p.room_id); for pkt in self.packet_queue.drain(..) { - self.state.rooms.broadcast( + self.state.rooms.forward_to_host( &p.room_id, match pkt { AnyPacket::Raw(b) => ConnectionAction::SendTCPRaw(b), _ => ConnectionAction::SendTCP(pkt), }, - Some(self.id), ); } } @@ -520,6 +516,18 @@ impl ConnectionActor { return Ok(()); } + let members = self.state.rooms.get_room_members(&room_id); + for (id, sender) in members { + if id != self.id { + let _ = sender.try_send(ConnectionAction::SendTCP(AnyPacket::App( + AppPacket::RoomClosed(RoomClosedPacket { + reason: CloseReason::Closed, + }), + ))); + let _ = sender.try_send(ConnectionAction::Close); + } + } + self.state.rooms.close(&room_id); info!( "Room {} closed by connection {} (the host).", diff --git a/src/state.rs b/src/state.rs index 0a46a19..4b7abf2 100644 --- a/src/state.rs +++ b/src/state.rs @@ -8,7 +8,7 @@ use std::collections::HashMap; use std::net::SocketAddr; use std::sync::{Arc, RwLock, RwLockReadGuard}; use tokio::sync::mpsc; -use tracing::{error, info}; +use tracing::{error, info, warn}; use uuid::Uuid; #[derive(Debug, Clone)] @@ -188,6 +188,59 @@ impl Rooms { } } } + + pub fn forward_to_host(&self, room_id: &str, action: ConnectionAction) { + let rooms = match self.rooms.read() { + Ok(rooms) => rooms, + Err(e) => { + error!("Failed to acquire rooms read lock: {}", e); + return; + } + }; + + let room = match rooms.get(room_id) { + Some(room) => room, + None => { + warn!("Room {} not found for forwarding", room_id); + return; + } + }; + + let sender = match room.members.get(&room.host_connection_id) { + Some(sender) => sender, + None => { + error!( + "Host {} not found in room {}", + room.host_connection_id, room_id + ); + return; + } + }; + + if let Err(e) = sender.try_send(action) { + info!( + "Failed to forward to host {}: {}", + room.host_connection_id, e + ); + } + } + + pub fn get_room_members(&self, room_id: &str) -> Vec<(i32, mpsc::Sender)> { + let rooms = match self.rooms.read() { + Ok(rooms) => rooms, + Err(e) => { + error!("Failed to acquire rooms read lock: {}", e); + return Vec::new(); + } + }; + + if let Some(room) = rooms.get(room_id) { + room.members.iter().map(|(k, v)| (*k, v.clone())).collect() + } else { + warn!("Room {} not found for getting members", room_id); + Vec::new() + } + } } pub struct AppState { From 8c7c73dd97c0309ae8c69b0bd49e368ec936ce52 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sun, 18 Jan 2026 17:43:43 +0700 Subject: [PATCH 030/115] docs: add base rules for code structure and error handling --- .trae/rules/base.md | 2 ++ src/packets.rs | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 .trae/rules/base.md diff --git a/.trae/rules/base.md b/.trae/rules/base.md new file mode 100644 index 0000000..d1b69cd --- /dev/null +++ b/.trae/rules/base.md @@ -0,0 +1,2 @@ +- All branch have to be handled or logged +- Early return if possible, make use of let Some/Ok,Err to handle early return diff --git a/src/packets.rs b/src/packets.rs index 40baddf..b4995b6 100644 --- a/src/packets.rs +++ b/src/packets.rs @@ -151,6 +151,8 @@ impl AnyPacket { } } + info!("Write packet: {:?}", self); + let mut out: BytesMut = BytesMut::new(); out.put_u16(payload.len() as u16); From e581f380281fb1f97e61e2d0ff3536aa275a5f71 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sun, 18 Jan 2026 17:45:17 +0700 Subject: [PATCH 031/115] refactor: remove redundant packet logging and add connection logging - Remove debug log for packet writing in AnyPacket implementation - Add connection-specific logging for various actions in ConnectionActor --- src/packets.rs | 2 -- src/proxy_server.rs | 8 +++++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/packets.rs b/src/packets.rs index b4995b6..40baddf 100644 --- a/src/packets.rs +++ b/src/packets.rs @@ -151,8 +151,6 @@ impl AnyPacket { } } - info!("Write packet: {:?}", self); - let mut out: BytesMut = BytesMut::new(); out.put_u16(payload.len() as u16); diff --git a/src/proxy_server.rs b/src/proxy_server.rs index b1b4d79..cd2d514 100644 --- a/src/proxy_server.rs +++ b/src/proxy_server.rs @@ -1,6 +1,7 @@ use crate::constant::{ArcCloseReason, CloseReason, MessageType}; use crate::packets::{ - AnyPacket, AppPacket, ConnectionClosedPacket, ConnectionPacketWrapPacket, FrameworkMessage, Message2Packet, MessagePacket, RoomClosedPacket, RoomLinkPacket + AnyPacket, AppPacket, ConnectionClosedPacket, ConnectionPacketWrapPacket, FrameworkMessage, + Message2Packet, MessagePacket, RoomClosedPacket, RoomLinkPacket, }; use crate::rate::AtomicRateLimiter; use crate::state::{AppState, ConnectionAction, RoomInit, RoomUpdate}; @@ -634,19 +635,24 @@ impl ConnectionActor { match action { ConnectionAction::SendTCP(p) => { let bytes = p.to_bytes(); + info!("Send packet: {:?} to {}", p, self.id); batch.extend_from_slice(&bytes); } ConnectionAction::SendUDP(p) => { + info!("Send udp: {:?} to {}", p, self.id); self.udp_writer.send(p).await?; } ConnectionAction::SendTCPRaw(b) => { + info!("Send tcp raw: to {}", self.id); batch.extend_from_slice(&b); } ConnectionAction::SendUDPRaw(b) => { + info!("Send udpudp raw: to {}", self.id); self.udp_writer.send_raw(&b).await?; } ConnectionAction::Close => { // Return error to break loop + info!("Close connection {}", self.id); return Err(anyhow::anyhow!("Closed")); } ConnectionAction::RegisterUDP(addr) => { From 21a9bf4cd1dfbf2e35285dd960fbbb09b401469c Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sun, 18 Jan 2026 17:50:31 +0700 Subject: [PATCH 032/115] s --- src/proxy_server.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/proxy_server.rs b/src/proxy_server.rs index cd2d514..8046817 100644 --- a/src/proxy_server.rs +++ b/src/proxy_server.rs @@ -54,7 +54,7 @@ fn spawn_udp_listener(state: Arc, socket: Arc) { match AnyPacket::read(&mut cursor) { Ok(packet) => { - info!("Received UDP packet: {:?}", packet); + info!("Received UDP packet: {:?} from {:?}", packet, addr); if let AnyPacket::Framework(FrameworkMessage::RegisterUDP { connection_id, @@ -244,7 +244,7 @@ impl ConnectionActor { let packet = AnyPacket::read(&mut cursor)?; - info!("Received TCP packet: {:?}", packet); + info!("Received TCP packet: {:?} from {}", packet, self.id); // Handle packet self.handle_packet(packet).await?; } From d3f41dfb75ea9f7201f93c5f8d2a4944ad2ecd0d Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sun, 18 Jan 2026 18:15:51 +0700 Subject: [PATCH 033/115] refactor(proxy_server): improve packet handling and logging - Move UDP packet logging after registration check to reduce noise - Change packet_queue to store Bytes instead of AnyPacket for efficiency - Consolidate raw packet handling into ConnectionPacketWrap format - Add queue length check and logging for raw packets --- src/proxy_server.rs | 43 ++++++++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/src/proxy_server.rs b/src/proxy_server.rs index 8046817..b753cf3 100644 --- a/src/proxy_server.rs +++ b/src/proxy_server.rs @@ -7,7 +7,7 @@ use crate::rate::AtomicRateLimiter; use crate::state::{AppState, ConnectionAction, RoomInit, RoomUpdate}; use crate::utils::current_time_millis; use anyhow::anyhow; -use bytes::{Buf, BytesMut}; +use bytes::{Buf, Bytes, BytesMut}; use std::io::Cursor; use std::net::SocketAddr; use std::sync::atomic::{AtomicI32, Ordering}; @@ -54,8 +54,6 @@ fn spawn_udp_listener(state: Arc, socket: Arc) { match AnyPacket::read(&mut cursor) { Ok(packet) => { - info!("Received UDP packet: {:?} from {:?}", packet, addr); - if let AnyPacket::Framework(FrameworkMessage::RegisterUDP { connection_id, }) = packet @@ -64,6 +62,8 @@ fn spawn_udp_listener(state: Arc, socket: Arc) { handle_register_udp(&state, connection_id, addr).await; } else { // Normal packet, route it + info!("Received UDP packet: {:?} from {:?}", packet, addr); + if let Some((sender, limiter)) = state.get_route(&addr) { if limiter.check() { if let Err(e) = @@ -158,7 +158,7 @@ struct ConnectionActor { udp_writer: UdpWriter, limiter: Arc, last_read: Instant, - packet_queue: Vec, + packet_queue: Vec, } impl ConnectionActor { @@ -243,8 +243,6 @@ impl ConnectionActor { let mut cursor = Cursor::new(&payload[..]); let packet = AnyPacket::read(&mut cursor)?; - - info!("Received TCP packet: {:?} from {}", packet, self.id); // Handle packet self.handle_packet(packet).await?; } @@ -253,7 +251,10 @@ impl ConnectionActor { async fn handle_packet(&mut self, packet: AnyPacket) -> anyhow::Result<()> { let is_framework = matches!(packet, AnyPacket::Framework(_)); + if !is_framework { + info!("Received TCP packet: {:?} from {}", packet, self.id); + let room_id_opt = self.state.rooms.find_connection_room_id(self.id); let is_host = if let Some(ref room_id) = room_id_opt { if let Some(rooms) = self.state.rooms.read() { @@ -299,12 +300,20 @@ impl ConnectionActor { AnyPacket::App(a) => self.handle_app(a).await?, AnyPacket::Raw(bytes) => { if let Some(room_id) = self.state.rooms.find_connection_room_id(self.id) { - self.state - .rooms - .forward_to_host(&room_id, ConnectionAction::SendTCPRaw(bytes)); + self.state.rooms.forward_to_host( + &room_id, + ConnectionAction::SendTCP(AnyPacket::App(AppPacket::ConnectionPacketWrap( + ConnectionPacketWrapPacket { + connection_id: self.id, + is_tcp: false, + buffer: bytes, + }, + ))), + ); } else { if self.packet_queue.len() < 16 { - self.packet_queue.push(AnyPacket::Raw(bytes)); + self.packet_queue.push(bytes); + info!("Queued raw packet for connection {}", self.id); } else { warn!( "Connection {} packet queue full, dropping raw packet", @@ -438,15 +447,19 @@ impl ConnectionActor { if let Some(sender) = self.state.get_sender(self.id) { self.state.rooms.join(self.id, &p.room_id, sender)?; + info!("Connection {} joined the room {}.", self.id, p.room_id); - for pkt in self.packet_queue.drain(..) { + for bytes in self.packet_queue.drain(..) { self.state.rooms.forward_to_host( &p.room_id, - match pkt { - AnyPacket::Raw(b) => ConnectionAction::SendTCPRaw(b), - _ => ConnectionAction::SendTCP(pkt), - }, + ConnectionAction::SendTCP(AnyPacket::App( + AppPacket::ConnectionPacketWrap(ConnectionPacketWrapPacket { + connection_id: self.id, + is_tcp: false, + buffer: bytes, + }), + )), ); } } From f861d7421e7021e70f77cf3251202684f6e9bd6a Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sun, 18 Jan 2026 18:59:18 +0700 Subject: [PATCH 034/115] feat(state): notify host about member join/leave events Add logic to notify room host when members join or leave by sending ConnectionJoinPacket and ConnectionClosedPacket respectively. This keeps the host informed about room membership changes in real-time. --- src/state.rs | 58 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/src/state.rs b/src/state.rs index 4b7abf2..79f91d3 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,4 +1,5 @@ -use crate::packets::AnyPacket; +use crate::constant::ArcCloseReason; +use crate::packets::{AnyPacket, ConnectionClosedPacket, ConnectionJoinPacket}; use crate::rate::AtomicRateLimiter; use crate::utils::current_time_millis; use anyhow::anyhow; @@ -101,14 +102,40 @@ impl Rooms { if let Some(room) = rooms.get_mut(room_id) { room.members.insert(connection_id, sender); - Ok(()) + + let sender = match room.members.get(&room.host_connection_id) { + Some(sender) => sender, + None => { + error!( + "Host {} not found in room {}", + room.host_connection_id, room_id + ); + return Ok(()); + } + }; + let packet = AnyPacket::App(crate::packets::AppPacket::ConnectionJoin( + ConnectionJoinPacket { + connection_id, + room_id: room_id.clone(), + }, + )); + + if let Err(e) = sender.try_send(ConnectionAction::SendTCP(packet)) { + info!( + "Failed to forward to host {}: {}", + room.host_connection_id, e + ); + } } else { - Err(anyhow!("Room not found")) + return Err(anyhow!("Room not found")); } + + Ok(()) } pub fn leave(&self, connection_id: i32) -> Option { let mut rooms = self.rooms.write().ok()?; + let room_id = rooms .iter() .find(|(_, room)| room.members.contains_key(&connection_id)) @@ -116,6 +143,31 @@ impl Rooms { if let Some(room) = rooms.get_mut(&room_id) { room.members.remove(&connection_id); + + let sender = match room.members.get(&room.host_connection_id) { + Some(sender) => sender, + None => { + error!( + "Host {} not found in room {}", + room.host_connection_id, room_id + ); + return None; + } + }; + + let packet = AnyPacket::App(crate::packets::AppPacket::ConnectionClosed( + ConnectionClosedPacket { + connection_id, + reason: ArcCloseReason::Closed, + }, + )); + + if let Err(e) = sender.try_send(ConnectionAction::SendTCP(packet)) { + info!( + "Failed to forward to host {}: {}", + room.host_connection_id, e + ); + } } Some(room_id) From 15703684c0bbe382ef4eae93e9b4e33bd4e0b392 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sun, 18 Jan 2026 19:21:08 +0700 Subject: [PATCH 035/115] feat: add packet processing action and handle UDP packets differently - Introduce new ConnectionAction::ProcessPacket to handle packets with TCP/UDP flag - Modify UDP packet handling to skip rate limiting and use ProcessPacket action - Update handle_packet method to accept is_tcp parameter for proper packet routing --- src/proxy_server.rs | 21 +++++++++++---------- src/state.rs | 1 + 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/proxy_server.rs b/src/proxy_server.rs index b753cf3..b48e54b 100644 --- a/src/proxy_server.rs +++ b/src/proxy_server.rs @@ -64,13 +64,11 @@ fn spawn_udp_listener(state: Arc, socket: Arc) { // Normal packet, route it info!("Received UDP packet: {:?} from {:?}", packet, addr); - if let Some((sender, limiter)) = state.get_route(&addr) { - if limiter.check() { - if let Err(e) = - sender.try_send(ConnectionAction::SendTCP(packet)) - { - info!("Failed to forward UDP packet: {}", e); - } + if let Some((sender, _)) = state.get_route(&addr) { + if let Err(e) = sender + .try_send(ConnectionAction::ProcessPacket(packet, false)) + { + info!("Failed to forward UDP packet: {}", e); } } else { // Unknown UDP sender, ignore @@ -244,12 +242,12 @@ impl ConnectionActor { let packet = AnyPacket::read(&mut cursor)?; // Handle packet - self.handle_packet(packet).await?; + self.handle_packet(packet, true).await?; } Ok(()) } - async fn handle_packet(&mut self, packet: AnyPacket) -> anyhow::Result<()> { + async fn handle_packet(&mut self, packet: AnyPacket, is_tcp: bool) -> anyhow::Result<()> { let is_framework = matches!(packet, AnyPacket::Framework(_)); if !is_framework { @@ -305,7 +303,7 @@ impl ConnectionActor { ConnectionAction::SendTCP(AnyPacket::App(AppPacket::ConnectionPacketWrap( ConnectionPacketWrapPacket { connection_id: self.id, - is_tcp: false, + is_tcp, buffer: bytes, }, ))), @@ -692,6 +690,9 @@ impl ConnectionActor { })) .await?; } + ConnectionAction::ProcessPacket(packet, is_tcp) => { + self.handle_packet(packet, is_tcp).await?; + } } Ok(()) } diff --git a/src/state.rs b/src/state.rs index 79f91d3..05172d7 100644 --- a/src/state.rs +++ b/src/state.rs @@ -20,6 +20,7 @@ pub enum ConnectionAction { SendUDPRaw(Bytes), Close, RegisterUDP(SocketAddr), + ProcessPacket(AnyPacket, bool), } #[derive(Debug, Clone)] From a0b4ba9605e2f269f63e9c7880b4d3c14b780a21 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sun, 18 Jan 2026 19:37:31 +0700 Subject: [PATCH 036/115] fix: increase connection timeout and improve room error handling - Increase CONNECTION_TIME_OUT_MS from 10s to 30s to handle slower connections - Add more descriptive error messages for room operations - Fix room cleanup logic when host leaves - Add info log when room is closed --- src/proxy_server.rs | 2 +- src/state.rs | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/proxy_server.rs b/src/proxy_server.rs index b48e54b..68346e0 100644 --- a/src/proxy_server.rs +++ b/src/proxy_server.rs @@ -23,7 +23,7 @@ static NEXT_CONNECTION_ID: AtomicI32 = AtomicI32::new(1); const UDP_BUFFER_SIZE: usize = 4096; const TCP_BUFFER_SIZE: usize = 32768; const CHANNEL_CAPACITY: usize = 100; -const CONNECTION_TIME_OUT_MS: u64 = 10000; +const CONNECTION_TIME_OUT_MS: u64 = 30000; const KEEP_ALIVE_INTERVAL_MS: u64 = 2000; const PACKET_LENGTH_LENGTH: usize = 2; const TICK_INTERVAL_SECS: u64 = 1; diff --git a/src/state.rs b/src/state.rs index 05172d7..334da17 100644 --- a/src/state.rs +++ b/src/state.rs @@ -108,7 +108,7 @@ impl Rooms { Some(sender) => sender, None => { error!( - "Host {} not found in room {}", + "Host {} not found in room {} while joining", room.host_connection_id, room_id ); return Ok(()); @@ -149,10 +149,10 @@ impl Rooms { Some(sender) => sender, None => { error!( - "Host {} not found in room {}", + "Host {} not found in room {} while leaving", room.host_connection_id, room_id ); - return None; + return Some(room_id); } }; @@ -221,6 +221,8 @@ impl Rooms { }; if removed.is_some() { + info!("Room closed {}", room_id); + if let Err(err) = self.tx.send(RoomUpdate::Remove(room_id.clone())) { error!("Failed to send remove room event: {}", err); }; @@ -263,7 +265,7 @@ impl Rooms { Some(sender) => sender, None => { error!( - "Host {} not found in room {}", + "Host {} not found in room {} while forward", room.host_connection_id, room_id ); return; @@ -372,6 +374,7 @@ impl AppState { // Handle room logic let room_id_opt = self.rooms.leave(connection_id); + if let Some(room_id) = room_id_opt { // Check if host let should_close = { From dcb9a9f33868c1085c4a891e3e4880e6b74ee289 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sun, 18 Jan 2026 19:44:31 +0700 Subject: [PATCH 037/115] fix(proxy_server): handle missing connection gracefully instead of erroring When a connection is not found, log a warning and return Ok instead of returning an error. This prevents unnecessary error propagation for cases where the connection might have been closed normally. --- src/proxy_server.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/proxy_server.rs b/src/proxy_server.rs index 68346e0..65e0ebc 100644 --- a/src/proxy_server.rs +++ b/src/proxy_server.rs @@ -620,7 +620,9 @@ impl ConnectionActor { }; let Some(sender) = self.state.get_sender(connection_id) else { - return Err(anyhow!("Connection not found: {}", connection_id)); + warn!("Connection not found: {}", connection_id); + + return Ok(()); }; if let Err(e) = sender.try_send(action) { From b217e1a9c2a223ab5322411d4cd4647e99dcb5c2 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sun, 18 Jan 2026 20:00:36 +0700 Subject: [PATCH 038/115] fix: correct udp log message and clean up packet buffer handling - Fix typo in UDP log message from "udpudp" to "udp" - Remove outdated comments and directly write packet buffer without commented-out code --- src/packets.rs | 11 +---------- src/proxy_server.rs | 2 +- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/packets.rs b/src/packets.rs index 40baddf..760bbd4 100644 --- a/src/packets.rs +++ b/src/packets.rs @@ -214,14 +214,6 @@ impl AppPacket { connection_id: buf.get_i32(), is_tcp: buf.get_u8() == 1, buffer: { - let len = buf.get_i32() as usize; // Buffer length? - // Actually java side: buffer is object. - // If it's just raw bytes, we need length. - // Let's assume remaining or prefixed length. - // Usually `write(buffer, object)` writes generic object. - // If it's ByteBuffer, it writes raw bytes? - // NetworkProxy: ((Packets.ConnectionPacketWrapPacket) packet).buffer = (ByteBuffer) ((ByteBuffer) last.get().clear()).put(buffer).flip(); - // It seems it captures the rest of the buffer? let mut b = BytesMut::new(); while buf.has_remaining() { b.put_u8(buf.get_u8()); @@ -282,8 +274,7 @@ impl AppPacket { buf.put_u8(0); buf.put_i32(p.connection_id); buf.put_u8(if p.is_tcp { 1 } else { 0 }); - // Write buffer? - // buf.put_slice(&p.buffer); + buf.put_slice(&p.buffer); } AppPacket::ConnectionClosed(p) => { buf.put_u8(1); diff --git a/src/proxy_server.rs b/src/proxy_server.rs index 65e0ebc..2406ea6 100644 --- a/src/proxy_server.rs +++ b/src/proxy_server.rs @@ -660,7 +660,7 @@ impl ConnectionActor { batch.extend_from_slice(&b); } ConnectionAction::SendUDPRaw(b) => { - info!("Send udpudp raw: to {}", self.id); + info!("Send udp raw: to {}", self.id); self.udp_writer.send_raw(&b).await?; } ConnectionAction::Close => { From 57181e031652d58f72bee916fe32bcd198b8a687 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sun, 18 Jan 2026 20:10:49 +0700 Subject: [PATCH 039/115] a --- src/packets.rs | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/src/packets.rs b/src/packets.rs index 760bbd4..854c1d3 100644 --- a/src/packets.rs +++ b/src/packets.rs @@ -167,7 +167,7 @@ impl FrameworkMessage { match fid { 0 => Ok(FrameworkMessage::Ping { id: buf.get_i32(), - is_reply: buf.get_u8() == 1, + is_reply: buf.get_u8() != 0, }), 1 => Ok(FrameworkMessage::DiscoverHost), 2 => Ok(FrameworkMessage::KeepAlive), @@ -209,19 +209,22 @@ impl AppPacket { let pid = buf.get_u8(); match pid { - 0 => Ok(AppPacket::ConnectionPacketWrap( - ConnectionPacketWrapPacket { - connection_id: buf.get_i32(), - is_tcp: buf.get_u8() == 1, - buffer: { - let mut b = BytesMut::new(); - while buf.has_remaining() { - b.put_u8(buf.get_u8()); - } - b.freeze() + 0 => { + let remaining = buf.remaining(); + let start = buf.position() as usize; + let end = start + remaining; + + let buffer = Bytes::copy_from_slice(&buf.get_ref()[start..end]); + buf.set_position(end as u64); + + Ok(AppPacket::ConnectionPacketWrap( + ConnectionPacketWrapPacket { + connection_id: buf.get_i32(), + is_tcp: buf.get_u8() != 0, + buffer, }, - }, - )), + )) + } 1 => Ok(AppPacket::ConnectionClosed(ConnectionClosedPacket { connection_id: buf.get_i32(), reason: ArcCloseReason::from(buf.get_u8()), // DcReason ordinal @@ -274,7 +277,7 @@ impl AppPacket { buf.put_u8(0); buf.put_i32(p.connection_id); buf.put_u8(if p.is_tcp { 1 } else { 0 }); - buf.put_slice(&p.buffer); + buf.extend_from_slice(&p.buffer); } AppPacket::ConnectionClosed(p) => { buf.put_u8(1); From 7dbb2fd8eeb3fc7fdf4812ea090b881e3be637b2 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sun, 18 Jan 2026 20:12:23 +0700 Subject: [PATCH 040/115] a --- src/proxy_server.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/proxy_server.rs b/src/proxy_server.rs index 2406ea6..0847645 100644 --- a/src/proxy_server.rs +++ b/src/proxy_server.rs @@ -656,11 +656,11 @@ impl ConnectionActor { self.udp_writer.send(p).await?; } ConnectionAction::SendTCPRaw(b) => { - info!("Send tcp raw: to {}", self.id); + info!("Send tcp {} bytes to {}", b.len(), self.id); batch.extend_from_slice(&b); } ConnectionAction::SendUDPRaw(b) => { - info!("Send udp raw: to {}", self.id); + info!("Send udp {} bytes to {}", b.len(), self.id); self.udp_writer.send_raw(&b).await?; } ConnectionAction::Close => { From 9adbfc1db145167efedc9f29790d6e98c6b0aa61 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sun, 18 Jan 2026 20:37:32 +0700 Subject: [PATCH 041/115] fix(proxy_server): handle packet reading errors gracefully Add error handling when reading packets to prevent connection termination on malformed packets. Log errors and continue processing subsequent packets. --- src/proxy_server.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/proxy_server.rs b/src/proxy_server.rs index 0847645..9e7f54c 100644 --- a/src/proxy_server.rs +++ b/src/proxy_server.rs @@ -229,20 +229,31 @@ impl ConnectionActor { if buf.len() < PACKET_LENGTH_LENGTH { break; } + let len = { let mut cur = Cursor::new(&buf[..]); cur.get_u16() as usize }; + if buf.len() < PACKET_LENGTH_LENGTH + len { break; } + buf.advance(PACKET_LENGTH_LENGTH); + let payload = buf.split_to(len); let mut cursor = Cursor::new(&payload[..]); - let packet = AnyPacket::read(&mut cursor)?; + match AnyPacket::read(&mut cursor) { + Ok(packet) => { + self.handle_packet(packet, true).await?; + } + Err(e) => { + error!("Error reading packet: {:?}", e); + continue; + } + } // Handle packet - self.handle_packet(packet, true).await?; } Ok(()) } From 7e661be6b0fcaa7f2c9e7a3e349a0ed486d4813d Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sun, 18 Jan 2026 20:45:27 +0700 Subject: [PATCH 042/115] feat(constant): add PackingSpamming variant to CloseReason enum Add new enum variant to handle cases where clients are detected as spamming packets --- src/constant.rs | 1 + src/state.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/constant.rs b/src/constant.rs index 181200b..c712343 100644 --- a/src/constant.rs +++ b/src/constant.rs @@ -7,6 +7,7 @@ pub enum CloseReason { ObsoleteClient = 1, OutdatedVersion = 2, ServerClosed = 3, + PackingSpamming = 4, } #[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr, PartialEq)] diff --git a/src/state.rs b/src/state.rs index 334da17..e516b66 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,4 +1,4 @@ -use crate::constant::ArcCloseReason; +use crate::constant::{ArcCloseReason, CloseReason}; use crate::packets::{AnyPacket, ConnectionClosedPacket, ConnectionJoinPacket}; use crate::rate::AtomicRateLimiter; use crate::utils::current_time_millis; From 0d33991d55a6c6751b6bf9af2baf2ea07f8c4abd Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sun, 18 Jan 2026 20:54:53 +0700 Subject: [PATCH 043/115] refactor(packets): extract connection id and is_tcp before buffer read The variables connection_id and is_tcp are now read from the buffer before processing the remaining bytes. This improves code clarity by separating the header fields from the buffer data. --- src/packets.rs | 33 ++++++++------------------------- 1 file changed, 8 insertions(+), 25 deletions(-) diff --git a/src/packets.rs b/src/packets.rs index 854c1d3..0271616 100644 --- a/src/packets.rs +++ b/src/packets.rs @@ -210,6 +210,9 @@ impl AppPacket { match pid { 0 => { + let connection_id = buf.get_i32(); + let is_tcp = buf.get_u8() != 0; + let remaining = buf.remaining(); let start = buf.position() as usize; let end = start + remaining; @@ -219,8 +222,8 @@ impl AppPacket { Ok(AppPacket::ConnectionPacketWrap( ConnectionPacketWrapPacket { - connection_id: buf.get_i32(), - is_tcp: buf.get_u8() != 0, + connection_id, + is_tcp, buffer, }, )) @@ -293,11 +296,8 @@ impl AppPacket { buf.put_u8(3); buf.put_i32(p.connection_id); } - AppPacket::RoomCreationRequest(p) => { - buf.put_u8(4); - write_string(buf, &p.version); - write_string(buf, &p.password); - write_stats(buf, &p.data); + AppPacket::RoomCreationRequest(_) => { + panic!("Client only") } AppPacket::RoomClosureRequest(_) => { buf.put_u8(5); @@ -328,9 +328,7 @@ impl AppPacket { write_string(buf, &p.message); } AppPacket::Stats(p) => { - buf.put_u8(12); - write_string(buf, &p.room_id); - write_stats(buf, &p.data); + panic!("Client only") } } } @@ -379,18 +377,3 @@ pub fn read_stats(buf: &mut Cursor<&[u8]>) -> anyhow::Result { } } } - -pub fn write_stats(buf: &mut BytesMut, stats: &Stats) { - buf.put_u8(stats.players.len() as u8); - for p in &stats.players { - write_string(buf, &p.name); - write_string(buf, &p.locale); - } - write_string(buf, &stats.map_name); - write_string(buf, &stats.name); - write_string(buf, &stats.gamemode); - // write_string(buf, &stats.mods); // TODO - write_string(buf, &stats.locale); - write_string(buf, &stats.version); - buf.put_u128(stats.created_at); -} From 7471cdfacb7accc601cfdf1fdfceb1e713489faa Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sun, 18 Jan 2026 21:57:15 +0700 Subject: [PATCH 044/115] feat(connection): add idle timeout handling for connections - Add IDLE_TIMEOUT_MS constant and implement idle connection detection - Notify room host when client connections become idle - Reset idle state on connection activity - Improve error handling for socket operations and message sending --- .trae/rules/base.md | 1 + src/proxy_server.rs | 27 ++++++++++++---- src/state.rs | 75 +++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 95 insertions(+), 8 deletions(-) diff --git a/.trae/rules/base.md b/.trae/rules/base.md index d1b69cd..52b5887 100644 --- a/.trae/rules/base.md +++ b/.trae/rules/base.md @@ -1,2 +1,3 @@ - All branch have to be handled or logged - Early return if possible, make use of let Some/Ok,Err to handle early return +- Do not ignore Result, log or handle it diff --git a/src/proxy_server.rs b/src/proxy_server.rs index 9e7f54c..bb6a126 100644 --- a/src/proxy_server.rs +++ b/src/proxy_server.rs @@ -24,6 +24,7 @@ const UDP_BUFFER_SIZE: usize = 4096; const TCP_BUFFER_SIZE: usize = 32768; const CHANNEL_CAPACITY: usize = 100; const CONNECTION_TIME_OUT_MS: u64 = 30000; +const IDLE_TIMEOUT_MS: u64 = 5000; const KEEP_ALIVE_INTERVAL_MS: u64 = 2000; const PACKET_LENGTH_LENGTH: usize = 2; const TICK_INTERVAL_SECS: u64 = 1; @@ -110,7 +111,9 @@ async fn accept_tcp_connection( let udp_socket = udp_socket.clone(); tokio::spawn(async move { - let _ = socket.set_nodelay(true); + if let Err(e) = socket.set_nodelay(true) { + warn!("Failed to set nodelay for connection: {}", e); + } let id = NEXT_CONNECTION_ID.fetch_add(1, Ordering::Relaxed); let (tx, rx) = mpsc::channel(CHANNEL_CAPACITY); @@ -181,6 +184,7 @@ impl ConnectionActor { Ok(0) => break, // EOF Ok(n) => { self.last_read = Instant::now(); + self.state.reset_idle(self.id); buf.extend_from_slice(&tmp_buf[..n]); self.process_tcp_buffer(&mut buf).await?; @@ -214,6 +218,10 @@ impl ConnectionActor { break; } + if self.last_read.elapsed() > Duration::from_millis(IDLE_TIMEOUT_MS) { + self.state.idle(self.id); + } + if self.tcp_writer.last_write.elapsed() > Duration::from_millis(KEEP_ALIVE_INTERVAL_MS) { self.write_packet(AnyPacket::Framework(FrameworkMessage::KeepAlive)).await?; } @@ -542,12 +550,16 @@ impl ConnectionActor { let members = self.state.rooms.get_room_members(&room_id); for (id, sender) in members { if id != self.id { - let _ = sender.try_send(ConnectionAction::SendTCP(AnyPacket::App( - AppPacket::RoomClosed(RoomClosedPacket { + if let Err(e) = sender.try_send(ConnectionAction::SendTCP( + AnyPacket::App(AppPacket::RoomClosed(RoomClosedPacket { reason: CloseReason::Closed, - }), - ))); - let _ = sender.try_send(ConnectionAction::Close); + })), + )) { + info!("Failed to send room closed packet to {}: {}", id, e); + } + if let Err(e) = sender.try_send(ConnectionAction::Close) { + info!("Failed to send close action to {}: {}", id, e); + } } } @@ -624,6 +636,8 @@ impl ConnectionActor { return Err(anyhow!("Not room owner")); } + self.state.reset_idle(connection_id); + let action = if is_tcp { ConnectionAction::SendTCPRaw(buffer) } else { @@ -704,6 +718,7 @@ impl ConnectionActor { .await?; } ConnectionAction::ProcessPacket(packet, is_tcp) => { + self.state.reset_idle(self.id); self.handle_packet(packet, is_tcp).await?; } } diff --git a/src/state.rs b/src/state.rs index e516b66..73e0d84 100644 --- a/src/state.rs +++ b/src/state.rs @@ -5,7 +5,7 @@ use crate::utils::current_time_millis; use anyhow::anyhow; use bytes::Bytes; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::net::SocketAddr; use std::sync::{Arc, RwLock, RwLockReadGuard}; use tokio::sync::mpsc; @@ -296,6 +296,38 @@ impl Rooms { Vec::new() } } + + pub fn idle(&self, room_id: &str, connection_id: i32) { + let rooms = match self.rooms.read() { + Ok(rooms) => rooms, + Err(_) => return, + }; + + if let Some(room) = rooms.get(room_id) { + // Don't process host idle + if room.host_connection_id == connection_id { + return; + } + + // Check if client is in room + if !room.members.contains_key(&connection_id) { + return; + } + + // Send to host + if let Some(sender) = room.members.get(&room.host_connection_id) { + let packet = AnyPacket::App(crate::packets::AppPacket::ConnectionIdling( + crate::packets::ConnectionIdlingPacket { connection_id }, + )); + if let Err(e) = sender.try_send(ConnectionAction::SendTCP(packet)) { + info!( + "Failed to forward idle packet to host {}: {}", + room.host_connection_id, e + ); + } + } + } + } } pub struct AppState { @@ -303,6 +335,7 @@ pub struct AppState { pub connections: RwLock, Arc)>>, pub udp_routes: RwLock, Arc)>>, + pub notified_idle: RwLock>, } impl AppState { @@ -316,6 +349,7 @@ impl AppState { }, connections: RwLock::new(HashMap::new()), udp_routes: RwLock::new(HashMap::new()), + notified_idle: RwLock::new(HashSet::new()), } } @@ -367,11 +401,46 @@ impl AppState { self.udp_routes.read().ok()?.get(addr).cloned() } + pub fn idle(&self, connection_id: i32) { + // Only process valid connections + if let Ok(conns) = self.connections.read() { + if !conns.contains_key(&connection_id) { + return; + } + } else { + return; + } + + // Check and set notified_idle + let should_notify = if let Ok(mut idle_set) = self.notified_idle.write() { + idle_set.insert(connection_id) + } else { + false + }; + + if !should_notify { + return; + } + + // Find room and notify host + if let Some(room_id) = self.rooms.find_connection_room_id(connection_id) { + self.rooms.idle(&room_id, connection_id); + } + } + + pub fn reset_idle(&self, connection_id: i32) { + if let Ok(mut idle_set) = self.notified_idle.write() { + idle_set.remove(&connection_id); + } + } + pub fn remove_connection(&self, connection_id: i32) { if let Ok(mut conns) = self.connections.write() { conns.remove(&connection_id); } + self.reset_idle(connection_id); + // Handle room logic let room_id_opt = self.rooms.leave(connection_id); @@ -404,7 +473,9 @@ impl AppState { }; for sender in members { - let _ = sender.try_send(ConnectionAction::Close); + if let Err(e) = sender.try_send(ConnectionAction::Close) { + info!("Failed to send close action to member: {}", e); + } } self.rooms.close(&room_id); From ea97e57d9f42c2a44aa21afbb33af19df4812e3d Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Mon, 19 Jan 2026 21:30:37 +0700 Subject: [PATCH 045/115] refactor(packets): use Bytes instead of slices for packet reading Modify packet reading functions to use Bytes type instead of byte slices for better memory efficiency and ownership handling. This change simplifies buffer management and avoids unnecessary copies when working with network data. --- src/packets.rs | 24 +++++++++++++++--------- src/proxy_server.rs | 6 +++--- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/packets.rs b/src/packets.rs index 0271616..5e34254 100644 --- a/src/packets.rs +++ b/src/packets.rs @@ -115,7 +115,7 @@ pub struct StatsPacket { } impl AnyPacket { - pub fn read(buf: &mut Cursor<&[u8]>) -> anyhow::Result { + pub fn read(buf: &mut Cursor) -> anyhow::Result { if !buf.has_remaining() { return Err(anyhow!("Empty packet")); } @@ -128,10 +128,13 @@ impl AnyPacket { _ => { buf.set_position(buf.position() - 1); let remaining = buf.remaining(); - let mut bytes = BytesMut::with_capacity(remaining); - bytes.put(buf); + let start = buf.position() as usize; + let end = start + remaining; - Ok(AnyPacket::Raw(bytes.freeze())) + let bytes = buf.get_ref().slice(start..end); + buf.advance(remaining); + + Ok(AnyPacket::Raw(bytes)) } } } @@ -161,7 +164,7 @@ impl AnyPacket { } impl FrameworkMessage { - pub fn read(buf: &mut Cursor<&[u8]>) -> anyhow::Result { + pub fn read(buf: &mut Cursor) -> anyhow::Result { let fid = buf.get_u8(); match fid { @@ -205,19 +208,22 @@ impl FrameworkMessage { } impl AppPacket { - pub fn read(buf: &mut Cursor<&[u8]>) -> anyhow::Result { + pub fn read(buf: &mut Cursor) -> anyhow::Result { let pid = buf.get_u8(); match pid { 0 => { + let start_pos = buf.position(); let connection_id = buf.get_i32(); let is_tcp = buf.get_u8() != 0; + buf.set_position(start_pos); + let remaining = buf.remaining(); let start = buf.position() as usize; let end = start + remaining; - let buffer = Bytes::copy_from_slice(&buf.get_ref()[start..end]); + let buffer = buf.get_ref().slice(start..end); buf.set_position(end as u64); Ok(AppPacket::ConnectionPacketWrap( @@ -335,7 +341,7 @@ impl AppPacket { } // Helper to read/write strings -pub fn read_string(buf: &mut Cursor<&[u8]>) -> anyhow::Result { +pub fn read_string(buf: &mut Cursor) -> anyhow::Result { if buf.remaining() < 2 { return Err(anyhow!( "Not enough bytes for string length: {}", @@ -366,7 +372,7 @@ pub fn write_string(buf: &mut BytesMut, s: &str) { buf.put_slice(bytes); } -pub fn read_stats(buf: &mut Cursor<&[u8]>) -> anyhow::Result { +pub fn read_stats(buf: &mut Cursor) -> anyhow::Result { let json = read_string(buf)?; match serde_json::from_str::(&json) { diff --git a/src/proxy_server.rs b/src/proxy_server.rs index bb6a126..8f4a437 100644 --- a/src/proxy_server.rs +++ b/src/proxy_server.rs @@ -51,7 +51,7 @@ fn spawn_udp_listener(state: Arc, socket: Arc) { match socket.recv_from(&mut buf).await { Ok((len, addr)) => { let data = &buf[..len]; - let mut cursor = Cursor::new(data); + let mut cursor = Cursor::new(Bytes::copy_from_slice(data)); match AnyPacket::read(&mut cursor) { Ok(packet) => { @@ -249,8 +249,8 @@ impl ConnectionActor { buf.advance(PACKET_LENGTH_LENGTH); - let payload = buf.split_to(len); - let mut cursor = Cursor::new(&payload[..]); + let payload = buf.split_to(len).freeze(); + let mut cursor = Cursor::new(payload); match AnyPacket::read(&mut cursor) { Ok(packet) => { From 6e2557bb6c0b75dac625f89562d79365588d4d60 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Mon, 19 Jan 2026 22:04:36 +0700 Subject: [PATCH 046/115] refactor(networking): replace Bytes with BytesMut for packet handling Change packet handling to use BytesMut instead of Bytes for better performance in buffer manipulation. This allows for more efficient zero-copy operations when building and modifying packets. Add prepend_len helper method to centralize length prefixing logic for both TCP and UDP packets. --- src/packets.rs | 21 ++++++++++++--------- src/proxy_server.rs | 6 +++--- src/state.rs | 6 +++--- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/packets.rs b/src/packets.rs index 5e34254..74783b2 100644 --- a/src/packets.rs +++ b/src/packets.rs @@ -14,7 +14,7 @@ pub const FRAMEWORK_PACKET_ID: i8 = -2; pub enum AnyPacket { Framework(FrameworkMessage), App(AppPacket), - Raw(Bytes), + Raw(BytesMut), } #[derive(Debug, Clone, Copy)] @@ -47,7 +47,7 @@ pub enum AppPacket { pub struct ConnectionPacketWrapPacket { pub connection_id: i32, pub is_tcp: bool, - pub buffer: Bytes, + pub buffer: BytesMut, } #[derive(Debug, Clone)] @@ -134,12 +134,12 @@ impl AnyPacket { let bytes = buf.get_ref().slice(start..end); buf.advance(remaining); - Ok(AnyPacket::Raw(bytes)) + Ok(AnyPacket::Raw(BytesMut::from(bytes))) } } } - pub fn to_bytes(&self) -> Bytes { + pub fn to_bytes(&self) -> BytesMut { let mut payload = BytesMut::new(); match self { @@ -154,12 +154,15 @@ impl AnyPacket { } } - let mut out: BytesMut = BytesMut::new(); + AnyPacket::prepend_len(payload) + } - out.put_u16(payload.len() as u16); - out.extend_from_slice(&payload); + pub fn prepend_len(payload: BytesMut) -> BytesMut { + let mut header = BytesMut::with_capacity(2); + header.put_u16(payload.len() as u16); - out.freeze() + header.unsplit(payload); + header } } @@ -230,7 +233,7 @@ impl AppPacket { ConnectionPacketWrapPacket { connection_id, is_tcp, - buffer, + buffer: BytesMut::from(buffer), }, )) } diff --git a/src/proxy_server.rs b/src/proxy_server.rs index 8f4a437..50084b4 100644 --- a/src/proxy_server.rs +++ b/src/proxy_server.rs @@ -159,7 +159,7 @@ struct ConnectionActor { udp_writer: UdpWriter, limiter: Arc, last_read: Instant, - packet_queue: Vec, + packet_queue: Vec, } impl ConnectionActor { @@ -682,11 +682,11 @@ impl ConnectionActor { } ConnectionAction::SendTCPRaw(b) => { info!("Send tcp {} bytes to {}", b.len(), self.id); - batch.extend_from_slice(&b); + batch.extend_from_slice(&AnyPacket::prepend_len(b)); } ConnectionAction::SendUDPRaw(b) => { info!("Send udp {} bytes to {}", b.len(), self.id); - self.udp_writer.send_raw(&b).await?; + self.udp_writer.send_raw(&AnyPacket::prepend_len(b)).await?; } ConnectionAction::Close => { // Return error to break loop diff --git a/src/state.rs b/src/state.rs index 73e0d84..7794c06 100644 --- a/src/state.rs +++ b/src/state.rs @@ -3,7 +3,7 @@ use crate::packets::{AnyPacket, ConnectionClosedPacket, ConnectionJoinPacket}; use crate::rate::AtomicRateLimiter; use crate::utils::current_time_millis; use anyhow::anyhow; -use bytes::Bytes; +use bytes::{Bytes, BytesMut}; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; use std::net::SocketAddr; @@ -16,8 +16,8 @@ use uuid::Uuid; pub enum ConnectionAction { SendTCP(AnyPacket), SendUDP(AnyPacket), - SendTCPRaw(Bytes), - SendUDPRaw(Bytes), + SendTCPRaw(BytesMut), + SendUDPRaw(BytesMut), Close, RegisterUDP(SocketAddr), ProcessPacket(AnyPacket, bool), From e311efa48164d7f60a4f317477bbacb7480dd3a7 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Mon, 19 Jan 2026 22:19:10 +0700 Subject: [PATCH 047/115] refactor(proxy): remove SendUDP action and simplify UDP raw sending Remove unused SendUDP variant from ConnectionAction enum and simplify UDP raw packet handling by sending bytes directly without prepending length. This improves code maintainability by removing unused functionality and simplifying the UDP sending logic. --- src/packets.rs | 4 ++-- src/proxy_server.rs | 6 +----- src/state.rs | 1 - 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/packets.rs b/src/packets.rs index 74783b2..ef8ddea 100644 --- a/src/packets.rs +++ b/src/packets.rs @@ -158,10 +158,10 @@ impl AnyPacket { } pub fn prepend_len(payload: BytesMut) -> BytesMut { - let mut header = BytesMut::with_capacity(2); + let mut header = BytesMut::with_capacity(2 + payload.len()); header.put_u16(payload.len() as u16); - header.unsplit(payload); + header.extend_from_slice(&payload); header } } diff --git a/src/proxy_server.rs b/src/proxy_server.rs index 50084b4..9016b58 100644 --- a/src/proxy_server.rs +++ b/src/proxy_server.rs @@ -676,17 +676,13 @@ impl ConnectionActor { info!("Send packet: {:?} to {}", p, self.id); batch.extend_from_slice(&bytes); } - ConnectionAction::SendUDP(p) => { - info!("Send udp: {:?} to {}", p, self.id); - self.udp_writer.send(p).await?; - } ConnectionAction::SendTCPRaw(b) => { info!("Send tcp {} bytes to {}", b.len(), self.id); batch.extend_from_slice(&AnyPacket::prepend_len(b)); } ConnectionAction::SendUDPRaw(b) => { info!("Send udp {} bytes to {}", b.len(), self.id); - self.udp_writer.send_raw(&AnyPacket::prepend_len(b)).await?; + self.udp_writer.send_raw(&b).await?; } ConnectionAction::Close => { // Return error to break loop diff --git a/src/state.rs b/src/state.rs index 7794c06..efa5316 100644 --- a/src/state.rs +++ b/src/state.rs @@ -15,7 +15,6 @@ use uuid::Uuid; #[derive(Debug, Clone)] pub enum ConnectionAction { SendTCP(AnyPacket), - SendUDP(AnyPacket), SendTCPRaw(BytesMut), SendUDPRaw(BytesMut), Close, From 64f4584a2bf677007610e6bfec0c47009e81498b Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Mon, 19 Jan 2026 22:31:42 +0700 Subject: [PATCH 048/115] fix(proxy_server): handle connection sender check before action Move sender existence check before creating connection action to avoid potential panics. Also remove unnecessary packet length prepending for TCP packets. --- src/proxy_server.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/proxy_server.rs b/src/proxy_server.rs index 9016b58..c230c4a 100644 --- a/src/proxy_server.rs +++ b/src/proxy_server.rs @@ -636,6 +636,12 @@ impl ConnectionActor { return Err(anyhow!("Not room owner")); } + let Some(sender) = self.state.get_sender(connection_id) else { + warn!("Connection not found: {}", connection_id); + + return Ok(()); + }; + self.state.reset_idle(connection_id); let action = if is_tcp { @@ -644,12 +650,6 @@ impl ConnectionActor { ConnectionAction::SendUDPRaw(buffer) }; - let Some(sender) = self.state.get_sender(connection_id) else { - warn!("Connection not found: {}", connection_id); - - return Ok(()); - }; - if let Err(e) = sender.try_send(action) { warn!("Failed to forward packet to {}: {}", connection_id, e); } @@ -678,7 +678,7 @@ impl ConnectionActor { } ConnectionAction::SendTCPRaw(b) => { info!("Send tcp {} bytes to {}", b.len(), self.id); - batch.extend_from_slice(&AnyPacket::prepend_len(b)); + batch.extend_from_slice(&b); } ConnectionAction::SendUDPRaw(b) => { info!("Send udp {} bytes to {}", b.len(), self.id); From 3430e9b64ee31139e55b8c68c503b40c6cd480f4 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Mon, 19 Jan 2026 22:52:13 +0700 Subject: [PATCH 049/115] refactor(packets): move prepend_len method to ConnectionActor The prepend_len functionality is more logically placed in ConnectionActor since it's primarily used there. This change improves code organization by keeping packet length handling with the connection logic that uses it. --- src/packets.rs | 10 +--------- src/proxy_server.rs | 18 +++++++++++------- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/src/packets.rs b/src/packets.rs index ef8ddea..6f91f45 100644 --- a/src/packets.rs +++ b/src/packets.rs @@ -154,15 +154,7 @@ impl AnyPacket { } } - AnyPacket::prepend_len(payload) - } - - pub fn prepend_len(payload: BytesMut) -> BytesMut { - let mut header = BytesMut::with_capacity(2 + payload.len()); - header.put_u16(payload.len() as u16); - - header.extend_from_slice(&payload); - header + payload } } diff --git a/src/proxy_server.rs b/src/proxy_server.rs index c230c4a..89514f8 100644 --- a/src/proxy_server.rs +++ b/src/proxy_server.rs @@ -7,7 +7,7 @@ use crate::rate::AtomicRateLimiter; use crate::state::{AppState, ConnectionAction, RoomInit, RoomUpdate}; use crate::utils::current_time_millis; use anyhow::anyhow; -use bytes::{Buf, Bytes, BytesMut}; +use bytes::{Buf, BufMut, Bytes, BytesMut}; use std::io::Cursor; use std::net::SocketAddr; use std::sync::atomic::{AtomicI32, Ordering}; @@ -674,11 +674,11 @@ impl ConnectionActor { ConnectionAction::SendTCP(p) => { let bytes = p.to_bytes(); info!("Send packet: {:?} to {}", p, self.id); - batch.extend_from_slice(&bytes); + batch.extend_from_slice(&ConnectionActor::prepend_len(bytes)); } ConnectionAction::SendTCPRaw(b) => { info!("Send tcp {} bytes to {}", b.len(), self.id); - batch.extend_from_slice(&b); + batch.extend_from_slice(&ConnectionActor::prepend_len(b)); } ConnectionAction::SendUDPRaw(b) => { info!("Send udp {} bytes to {}", b.len(), self.id); @@ -721,6 +721,14 @@ impl ConnectionActor { Ok(()) } + pub fn prepend_len(payload: BytesMut) -> BytesMut { + let mut header = BytesMut::with_capacity(2 + payload.len()); + header.put_u16(payload.len() as u16); + + header.extend_from_slice(&payload); + header + } + async fn write_packet(&mut self, packet: AnyPacket) -> anyhow::Result<()> { self.tcp_writer.write_packet(packet).await } @@ -765,10 +773,6 @@ impl UdpWriter { self.addr = Some(addr); } - async fn send(&self, packet: AnyPacket) -> anyhow::Result<()> { - return self.send_raw(&packet.to_bytes()).await; - } - async fn send_raw(&self, bytes: &[u8]) -> anyhow::Result<()> { if let Some(addr) = self.addr { self.socket.send_to(bytes, addr).await?; From 9f9121c34a12a3e7e3c4d4458f6877171f74e595 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Mon, 19 Jan 2026 23:03:05 +0700 Subject: [PATCH 050/115] refactor: clean up unused imports and simplify packet handling - Remove unused imports in state.rs - Simplify StatsPacket by removing unused room_id field - Clean up packet serialization logic - Improve prepend_len method readability in proxy_server - Add send method to UdpWriter for better packet handling --- src/packets.rs | 4 +--- src/proxy_server.rs | 9 +++++---- src/state.rs | 4 ++-- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/packets.rs b/src/packets.rs index 6f91f45..4e5e072 100644 --- a/src/packets.rs +++ b/src/packets.rs @@ -110,7 +110,6 @@ pub struct Message2Packet { #[derive(Debug, Clone)] pub struct StatsPacket { - pub room_id: String, pub data: Stats, } @@ -266,7 +265,6 @@ impl AppPacket { message: read_string(buf)?, })), 12 => Ok(AppPacket::Stats(StatsPacket { - room_id: read_string(buf)?, data: read_stats(buf)?, })), _ => Err(anyhow!("Unknown App Packet ID: {}", pid)), @@ -328,7 +326,7 @@ impl AppPacket { buf.put_u8(11); write_string(buf, &p.message); } - AppPacket::Stats(p) => { + AppPacket::Stats(_) => { panic!("Client only") } } diff --git a/src/proxy_server.rs b/src/proxy_server.rs index 89514f8..546b9b1 100644 --- a/src/proxy_server.rs +++ b/src/proxy_server.rs @@ -722,11 +722,12 @@ impl ConnectionActor { } pub fn prepend_len(payload: BytesMut) -> BytesMut { - let mut header = BytesMut::with_capacity(2 + payload.len()); - header.put_u16(payload.len() as u16); + let mut out: BytesMut = BytesMut::with_capacity(2 + payload.len()); - header.extend_from_slice(&payload); - header + out.put_u16(payload.len() as u16); + out.extend_from_slice(&payload); + + out } async fn write_packet(&mut self, packet: AnyPacket) -> anyhow::Result<()> { diff --git a/src/state.rs b/src/state.rs index efa5316..3d5a932 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,9 +1,9 @@ -use crate::constant::{ArcCloseReason, CloseReason}; +use crate::constant::ArcCloseReason; use crate::packets::{AnyPacket, ConnectionClosedPacket, ConnectionJoinPacket}; use crate::rate::AtomicRateLimiter; use crate::utils::current_time_millis; use anyhow::anyhow; -use bytes::{Bytes, BytesMut}; +use bytes::BytesMut; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; use std::net::SocketAddr; From 0bc4ad31fb096231bf19ece55448e084967175e4 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Mon, 19 Jan 2026 23:15:44 +0700 Subject: [PATCH 051/115] fix: remove unnecessary length prepend in TCP raw send The ConnectionActor was incorrectly prepending length to TCP raw data before sending. Since the data is already properly formatted, we can send it directly without modification. --- src/proxy_server.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/proxy_server.rs b/src/proxy_server.rs index 546b9b1..cda0ef4 100644 --- a/src/proxy_server.rs +++ b/src/proxy_server.rs @@ -678,7 +678,7 @@ impl ConnectionActor { } ConnectionAction::SendTCPRaw(b) => { info!("Send tcp {} bytes to {}", b.len(), self.id); - batch.extend_from_slice(&ConnectionActor::prepend_len(b)); + batch.extend_from_slice(&b); } ConnectionAction::SendUDPRaw(b) => { info!("Send udp {} bytes to {}", b.len(), self.id); From e440a1ed0f9c899728a07262c0a181164491b21b Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Mon, 19 Jan 2026 23:22:59 +0700 Subject: [PATCH 052/115] refactor(proxy_server): modify packet writing to prepend length The changes consolidate packet writing logic by always prepending length information before sending TCP packets. This removes the separate write_packet method in TcpWriter and ensures consistent behavior across all packet writes. --- src/proxy_server.rs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/proxy_server.rs b/src/proxy_server.rs index cda0ef4..7f0d137 100644 --- a/src/proxy_server.rs +++ b/src/proxy_server.rs @@ -678,7 +678,7 @@ impl ConnectionActor { } ConnectionAction::SendTCPRaw(b) => { info!("Send tcp {} bytes to {}", b.len(), self.id); - batch.extend_from_slice(&b); + batch.extend_from_slice(&ConnectionActor::prepend_len(b)); } ConnectionAction::SendUDPRaw(b) => { info!("Send udp {} bytes to {}", b.len(), self.id); @@ -726,12 +726,14 @@ impl ConnectionActor { out.put_u16(payload.len() as u16); out.extend_from_slice(&payload); - + out } async fn write_packet(&mut self, packet: AnyPacket) -> anyhow::Result<()> { - self.tcp_writer.write_packet(packet).await + self.tcp_writer + .write(&ConnectionActor::prepend_len(packet.to_bytes())) + .await } } @@ -753,11 +755,6 @@ impl TcpWriter { self.last_write = Instant::now(); Ok(()) } - - async fn write_packet(&mut self, packet: AnyPacket) -> anyhow::Result<()> { - let bytes = packet.to_bytes(); - self.write(&bytes).await - } } struct UdpWriter { From da16fabaef42cde1c186ab1a556c81cae63b3423 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Mon, 19 Jan 2026 23:33:01 +0700 Subject: [PATCH 053/115] perf(proxy_server): write TCP packets directly instead of batching The change removes intermediate batching of TCP packets and writes them directly to the TCP writer. This reduces memory usage and latency by avoiding unnecessary buffering. --- src/proxy_server.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/proxy_server.rs b/src/proxy_server.rs index 7f0d137..9539b29 100644 --- a/src/proxy_server.rs +++ b/src/proxy_server.rs @@ -674,11 +674,11 @@ impl ConnectionActor { ConnectionAction::SendTCP(p) => { let bytes = p.to_bytes(); info!("Send packet: {:?} to {}", p, self.id); - batch.extend_from_slice(&ConnectionActor::prepend_len(bytes)); + self.tcp_writer.write(&ConnectionActor::prepend_len(bytes)).await?; } ConnectionAction::SendTCPRaw(b) => { info!("Send tcp {} bytes to {}", b.len(), self.id); - batch.extend_from_slice(&ConnectionActor::prepend_len(b)); + self.tcp_writer.write(&ConnectionActor::prepend_len(b)).await?; } ConnectionAction::SendUDPRaw(b) => { info!("Send udp {} bytes to {}", b.len(), self.id); From 2e55d1adc5512a8f64da802dbba1c2a46c9060b1 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Mon, 19 Jan 2026 23:48:25 +0700 Subject: [PATCH 054/115] perf(proxy_server): batch TCP writes instead of immediate writes Replace individual TCP writes with batched writes to improve network performance by reducing system calls. The change accumulates packets in a buffer before sending. --- src/proxy_server.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/proxy_server.rs b/src/proxy_server.rs index 9539b29..7f0d137 100644 --- a/src/proxy_server.rs +++ b/src/proxy_server.rs @@ -674,11 +674,11 @@ impl ConnectionActor { ConnectionAction::SendTCP(p) => { let bytes = p.to_bytes(); info!("Send packet: {:?} to {}", p, self.id); - self.tcp_writer.write(&ConnectionActor::prepend_len(bytes)).await?; + batch.extend_from_slice(&ConnectionActor::prepend_len(bytes)); } ConnectionAction::SendTCPRaw(b) => { info!("Send tcp {} bytes to {}", b.len(), self.id); - self.tcp_writer.write(&ConnectionActor::prepend_len(b)).await?; + batch.extend_from_slice(&ConnectionActor::prepend_len(b)); } ConnectionAction::SendUDPRaw(b) => { info!("Send udp {} bytes to {}", b.len(), self.id); From 120cdb9a738cc96f044f2257bb335ed97cc64010 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Tue, 20 Jan 2026 00:37:46 +0700 Subject: [PATCH 055/115] refactor(packets): rename data to raw for clarity and use extend_from_slice refactor(proxy_server): improve packet handling and add connection closed notification - Rename variable 'data' to 'raw' in AnyPacket::Raw for better clarity - Replace put_slice with extend_from_slice for consistency - Simplify packet forwarding logic in ConnectionActor - Add connection closed notification when connection is not found --- src/packets.rs | 4 ++-- src/proxy_server.rs | 31 +++++++++++++++++++++---------- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/packets.rs b/src/packets.rs index 4e5e072..5b0b81b 100644 --- a/src/packets.rs +++ b/src/packets.rs @@ -148,8 +148,8 @@ impl AnyPacket { AnyPacket::App(package) => { package.write(&mut payload); } - AnyPacket::Raw(data) => { - payload.put_slice(data); + AnyPacket::Raw(raw) => { + payload.extend_from_slice(raw); } } diff --git a/src/proxy_server.rs b/src/proxy_server.rs index 7f0d137..03daaba 100644 --- a/src/proxy_server.rs +++ b/src/proxy_server.rs @@ -317,16 +317,17 @@ impl ConnectionActor { AnyPacket::App(a) => self.handle_app(a).await?, AnyPacket::Raw(bytes) => { if let Some(room_id) = self.state.rooms.find_connection_room_id(self.id) { - self.state.rooms.forward_to_host( - &room_id, - ConnectionAction::SendTCP(AnyPacket::App(AppPacket::ConnectionPacketWrap( - ConnectionPacketWrapPacket { - connection_id: self.id, - is_tcp, - buffer: bytes, - }, - ))), - ); + let packet = AnyPacket::App(AppPacket::ConnectionPacketWrap( + ConnectionPacketWrapPacket { + connection_id: self.id, + is_tcp: true, + buffer: bytes, + }, + )); + + self.state + .rooms + .forward_to_host(&room_id, ConnectionAction::SendTCP(packet)); } else { if self.packet_queue.len() < 16 { self.packet_queue.push(bytes); @@ -639,6 +640,16 @@ impl ConnectionActor { let Some(sender) = self.state.get_sender(connection_id) else { warn!("Connection not found: {}", connection_id); + self.state.rooms.forward_to_host( + &room_id, + ConnectionAction::SendTCP(AnyPacket::App( + AppPacket::ConnectionClosed(ConnectionClosedPacket { + connection_id, + reason: ArcCloseReason::Closed, + }), + )), + ); + return Ok(()); }; From f66343c8a16cfe6188db340e76e2e1fe96aa66b8 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Tue, 20 Jan 2026 00:40:39 +0700 Subject: [PATCH 056/115] fix(proxy_server): change default connection type to UDP The connection type was incorrectly set to TCP by default. This change sets it to UDP which is the intended default behavior for this connection. --- src/proxy_server.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/proxy_server.rs b/src/proxy_server.rs index 03daaba..a212c01 100644 --- a/src/proxy_server.rs +++ b/src/proxy_server.rs @@ -320,7 +320,7 @@ impl ConnectionActor { let packet = AnyPacket::App(AppPacket::ConnectionPacketWrap( ConnectionPacketWrapPacket { connection_id: self.id, - is_tcp: true, + is_tcp: false, buffer: bytes, }, )); From 833e3b7640060fa8758866c8ed22b83d17e6db19 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Tue, 20 Jan 2026 01:13:01 +0700 Subject: [PATCH 057/115] a --- src/packets.rs | 8 ++------ src/proxy_server.rs | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/packets.rs b/src/packets.rs index 5b0b81b..230dbd4 100644 --- a/src/packets.rs +++ b/src/packets.rs @@ -207,17 +207,13 @@ impl AppPacket { match pid { 0 => { - let start_pos = buf.position(); let connection_id = buf.get_i32(); let is_tcp = buf.get_u8() != 0; - buf.set_position(start_pos); - - let remaining = buf.remaining(); let start = buf.position() as usize; - let end = start + remaining; - + let end = start + buf.remaining(); let buffer = buf.get_ref().slice(start..end); + buf.set_position(end as u64); Ok(AppPacket::ConnectionPacketWrap( diff --git a/src/proxy_server.rs b/src/proxy_server.rs index a212c01..3ba5051 100644 --- a/src/proxy_server.rs +++ b/src/proxy_server.rs @@ -320,7 +320,7 @@ impl ConnectionActor { let packet = AnyPacket::App(AppPacket::ConnectionPacketWrap( ConnectionPacketWrapPacket { connection_id: self.id, - is_tcp: false, + is_tcp, buffer: bytes, }, )); From 469d3c867b79adbe5ce97b2a7f7f83bc35038a81 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Tue, 20 Jan 2026 15:22:11 +0700 Subject: [PATCH 058/115] refactor(packet): restructure packet handling and error management feat(error): add AppError enum for better error handling refactor(constant): implement TryFrom for enum conversions refactor(state): use strong types for ConnectionId and RoomId feat(models): add Stats and related structs for room management refactor(http_server): update to use new models and strong types feat(writer): add TcpWriter and UdpWriter for connection handling feat(connection): implement ConnectionActor for TCP/UDP management chore(rules): update project rules and guidelines --- .trae/rules/base.md | 116 +++++- src/config.rs | 1 + src/connection.rs | 623 +++++++++++++++++++++++++++++++ src/constant.rs | 55 ++- src/error.rs | 28 ++ src/http_server.rs | 21 +- src/main.rs | 29 +- src/models.rs | 54 +++ src/{packets.rs => packet.rs} | 129 ++++--- src/proxy_server.rs | 670 +--------------------------------- src/rate.rs | 2 +- src/state.rs | 219 +++++------ src/writer.rs | 51 +++ 13 files changed, 1121 insertions(+), 877 deletions(-) create mode 100644 src/connection.rs create mode 100644 src/error.rs create mode 100644 src/models.rs rename src/{packets.rs => packet.rs} (72%) create mode 100644 src/writer.rs diff --git a/.trae/rules/base.md b/.trae/rules/base.md index 52b5887..744f66f 100644 --- a/.trae/rules/base.md +++ b/.trae/rules/base.md @@ -1,3 +1,113 @@ -- All branch have to be handled or logged -- Early return if possible, make use of let Some/Ok,Err to handle early return -- Do not ignore Result, log or handle it +1. Project & File Structure Rules + +One responsibility per module + +Each mod should have a single clear purpose. + +Avoid “god modules”. + +Predictable layout + +src/ main.rs error.rs config.rs + +Public API at the top + +pub struct, pub enum, pub fn first + +Helpers and private impls later + +2. Naming Rules (Very Important for AI) + +Use descriptive names, never single letters ❌ x, tmp, r, v ✅ request, buffer, user_id, packet_type + +Avoid abbreviations ❌ cfg, mgr, svc ✅ config, manager, service + +Types > comments + +Prefer expressive type names over comments + +Boolean names must answer yes/no + +is_connected has_permission should_retry + +3. Type System Rules + +Prefer strong types over primitives + +struct UserId(u64); struct Port(u16); + +Never use String when an enum fits + +enum Protocol { Tcp, Udp, } + +Avoid Option for required data + +Use constructor validation instead + +Avoid Vec without context + +struct PacketBytes(Vec); + +4. Error Handling Rules + +Never use unwrap() or expect() in library code + +OK only in tests or binaries + +Use a single error enum + +enum AppError { Io(std::io::Error), InvalidPacket, Timeout, } + +Errors must be meaningful ❌ Err(AppError::Invalid) ✅ Err(AppError::InvalidPacketHeader) + +Implement Display for errors + +AI tools rely on readable messages + +5. Function Design Rules + +Functions should fit on one screen + +~20–40 lines max + +One logical action per function + +Avoid hidden side effects + +No silent global mutation + +Explicit input > implicit state + +fn encode(packet: &Packet, buffer: &mut BytesMut) + +Prefer returning values over mutating inputs + +6. Ownership & Borrowing Rules + +Prefer borrowing over cloning + +fn process(data: &Data) + +Clone only at API boundaries + +Avoid complex lifetime annotations + +If lifetimes get hard, redesign + +Use Arc only for shared ownership + +Never “just in case” + +7. Async & Concurrency Rules + +Never block in async code ❌ std::thread::sleep ✅ tokio::time::sleep + +Name async functions clearly + +async fn fetch_user() + +One async runtime + +Do not mix Tokio + async-std + +Use channels instead of shared mutable state diff --git a/src/config.rs b/src/config.rs index d382e4f..8f3688f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -2,6 +2,7 @@ use anyhow::Result; use dotenvy::dotenv; use std::env; +#[derive(Clone, Debug)] pub struct Config { pub player_connect_port: u16, pub player_connect_http_port: u16, diff --git a/src/connection.rs b/src/connection.rs new file mode 100644 index 0000000..e93ec25 --- /dev/null +++ b/src/connection.rs @@ -0,0 +1,623 @@ +use crate::constant::{ArcCloseReason, CloseReason, MessageType}; +use crate::packet::{ + AnyPacket, AppPacket, ConnectionClosedPacket, ConnectionId, ConnectionPacketWrapPacket, + FrameworkMessage, Message2Packet, MessagePacket, RoomClosedPacket, RoomLinkPacket, +}; +use crate::rate::AtomicRateLimiter; +use crate::state::{AppState, ConnectionAction, RoomInit, RoomUpdate}; +use crate::utils::current_time_millis; +use crate::writer::{TcpWriter, UdpWriter}; +use anyhow::anyhow; +use bytes::{Buf, BufMut, BytesMut}; +use std::io::Cursor; +use std::sync::Arc; +use std::time::{Duration, Instant}; +use tokio::io::AsyncReadExt; +use tokio::sync::mpsc; +use tracing::{error, info, warn}; + +const TCP_BUFFER_SIZE: usize = 32768; +const CONNECTION_TIME_OUT_MS: u64 = 30000; +const IDLE_TIMEOUT_MS: u64 = 5000; +const KEEP_ALIVE_INTERVAL_MS: u64 = 2000; +const PACKET_LENGTH_LENGTH: usize = 2; +const TICK_INTERVAL_SECS: u64 = 1; + +pub struct ConnectionActor { + pub id: ConnectionId, + pub state: Arc, + pub rx: mpsc::Receiver, + pub tcp_writer: TcpWriter, + pub udp_writer: UdpWriter, + pub limiter: Arc, + pub last_read: Instant, + pub packet_queue: Vec, +} + +impl ConnectionActor { + pub async fn run(&mut self, mut reader: tokio::net::tcp::OwnedReadHalf) -> anyhow::Result<()> { + let register_packet = AnyPacket::Framework(FrameworkMessage::RegisterTCP { + connection_id: self.id, + }); + + self.write_packet(register_packet).await?; + + let mut buf = BytesMut::with_capacity(TCP_BUFFER_SIZE); + let mut tmp_buf = [0u8; TCP_BUFFER_SIZE]; + let mut tick_interval = tokio::time::interval(Duration::from_secs(TICK_INTERVAL_SECS)); + + loop { + let mut batch = BytesMut::new(); + + tokio::select! { + // TCP Read + read_result = reader.read(&mut tmp_buf) => { + match read_result { + Ok(0) => break, // EOF + Ok(n) => { + self.last_read = Instant::now(); + self.state.reset_idle(self.id); + + buf.extend_from_slice(&tmp_buf[..n]); + self.process_tcp_buffer(&mut buf).await?; + } + Err(e) => return Err(e.into()), + } + } + + // Channel Read + action = self.rx.recv() => { + if let Some(action) = action { + self.handle_action(action, &mut batch).await?; + + while let Ok(action) = self.rx.try_recv() { + self.handle_action(action, &mut batch).await?; + } + // Flush batch + if !batch.is_empty() { + self.tcp_writer.write(&batch).await?; + } + } else { + // Channel closed + break; + } + } + + // Tick + _ = tick_interval.tick() => { + if self.last_read.elapsed() > Duration::from_millis(CONNECTION_TIME_OUT_MS) { + info!("Connection {} timed out", self.id); + break; + } + + if self.last_read.elapsed() > Duration::from_millis(IDLE_TIMEOUT_MS) { + self.state.idle(self.id); + } + + if self.tcp_writer.last_write.elapsed() > Duration::from_millis(KEEP_ALIVE_INTERVAL_MS) { + self.write_packet(AnyPacket::Framework(FrameworkMessage::KeepAlive)).await?; + } + } + } + } + + Ok(()) + } + + async fn process_tcp_buffer(&mut self, buf: &mut BytesMut) -> anyhow::Result<()> { + loop { + if buf.len() < PACKET_LENGTH_LENGTH { + break; + } + + let len = { + let mut cur = Cursor::new(&buf[..]); + cur.get_u16() as usize + }; + + if buf.len() < PACKET_LENGTH_LENGTH + len { + break; + } + + buf.advance(PACKET_LENGTH_LENGTH); + + let payload = buf.split_to(len).freeze(); + let mut cursor = Cursor::new(payload); + + match AnyPacket::read(&mut cursor) { + Ok(packet) => { + self.handle_packet(packet, true).await?; + } + Err(e) => { + error!("Error reading packet: {:?}", e); + continue; + } + } + // Handle packet + } + Ok(()) + } + + async fn handle_packet(&mut self, packet: AnyPacket, is_tcp: bool) -> anyhow::Result<()> { + let is_framework = matches!(packet, AnyPacket::Framework(_)); + + if !is_framework { + info!("Received TCP packet: {:?} from {}", packet, self.id); + + let room_id_opt = self.state.rooms.find_connection_room_id(self.id); + let is_host = if let Some(ref room_id) = room_id_opt { + if let Some(rooms) = self.state.rooms.read() { + rooms + .get(room_id) + .map(|r| r.host_connection_id == self.id) + .unwrap_or(false) + } else { + false + } + } else { + false + }; + + if !is_host && !self.limiter.check() { + if let Some(ref room_id) = room_id_opt { + self.state.rooms.broadcast( + room_id, + ConnectionAction::SendTCP(AnyPacket::App(AppPacket::Message2( + Message2Packet { + message: MessageType::PacketSpamming, + }, + ))), + None, + ); + } + + self.write_packet(AnyPacket::App(AppPacket::ConnectionClosed( + ConnectionClosedPacket { + connection_id: self.id, + reason: ArcCloseReason::Closed, + }, + ))) + .await?; + + warn!("Connection {} disconnected for packet spamming.", self.id); + return Err(anyhow!("Packet Spamming")); + } + } + + match packet { + AnyPacket::Framework(f) => self.handle_framework(f).await?, + AnyPacket::App(a) => self.handle_app(a).await?, + AnyPacket::Raw(bytes) => { + if let Some(room_id) = self.state.rooms.find_connection_room_id(self.id) { + let packet = AnyPacket::App(AppPacket::ConnectionPacketWrap( + ConnectionPacketWrapPacket { + connection_id: self.id, + is_tcp, + buffer: bytes, + }, + )); + + self.state + .rooms + .forward_to_host(&room_id, ConnectionAction::SendTCP(packet)); + } else { + if self.packet_queue.len() < 16 { + self.packet_queue.push(bytes); + info!("Queued raw packet for connection {}", self.id); + } else { + warn!( + "Connection {} packet queue full, dropping raw packet", + self.id + ); + } + } + } + } + Ok(()) + } + + async fn handle_framework(&mut self, packet: FrameworkMessage) -> anyhow::Result<()> { + match packet { + FrameworkMessage::Ping { id, is_reply } => { + if !is_reply { + self.write_packet(AnyPacket::Framework(FrameworkMessage::Ping { + id, + is_reply: true, + })) + .await?; + } + } + FrameworkMessage::KeepAlive => { + // Handled by activity update + } + FrameworkMessage::RegisterUDP { .. } => { + // Should not happen via TCP? + // But if it does, ignore? + } + _ => { + warn!("Unhandled Framework Packet: {:?}", packet); + } + } + Ok(()) + } + + async fn handle_app(&mut self, packet: AppPacket) -> anyhow::Result<()> { + match packet { + AppPacket::Stats(p) => { + if let Some(room_id) = self.state.rooms.find_connection_room_id(self.id) { + if let Ok(mut rooms) = self.state.rooms.rooms.write() { + if let Some(room) = rooms.get_mut(&room_id) { + let sent_at = p.data.created_at; + + room.stats = p.data; + room.updated_at = current_time_millis(); + room.ping = current_time_millis() - sent_at; + + if let Err(err) = + self.state.rooms.broadcast_sender.send(RoomUpdate::Update { + id: room.id.clone(), + data: room.clone(), + }) + { + info!("Fail to broadcast room update {}", err); + } + } + } + } + } + AppPacket::RoomJoin(p) => { + if let Some(current_room_id) = self.state.rooms.find_connection_room_id(self.id) { + let is_host = if let Some(rooms) = self.state.rooms.read() { + rooms + .get(¤t_room_id) + .map(|r| r.host_connection_id == self.id) + .unwrap_or(false) + } else { + false + }; + + if is_host { + self.write_packet(AnyPacket::App(AppPacket::Message2(Message2Packet { + message: MessageType::AlreadyHosting, + }))) + .await?; + + warn!( + "Connection {} tried to join room {} but is already hosting {}", + self.id, p.room_id, current_room_id + ); + return Ok(()); + } + + info!( + "Connection {} left room {} to join {}", + self.id, current_room_id, p.room_id + ); + self.state.rooms.leave(self.id); + } + + let (can_join, wrong_password) = (|| { + let rooms = self.state.rooms.read()?; + let room = rooms.get(&p.room_id)?; + + if let Some(ref pass) = room.password { + if pass != &p.password { + return Some((false, true)); + } + } + + Some((true, false)) + })() + .unwrap_or((false, false)); + + if wrong_password { + info!( + "Connection {} tried to join room {} with wrong password.", + self.id, p.room_id + ); + self.write_packet(AnyPacket::App(AppPacket::Message(MessagePacket { + message: "Wrong password".to_string(), + }))) + .await?; + return Ok(()); + } + + if !can_join { + info!( + "Connection {} tried to join a non-existent room {}.", + self.id, p.room_id + ); + self.write_packet(AnyPacket::App(AppPacket::ConnectionClosed( + ConnectionClosedPacket { + connection_id: self.id, + reason: ArcCloseReason::Error, + }, + ))) + .await?; + return Ok(()); + } + + if let Some(sender) = self.state.get_sender(self.id) { + self.state.rooms.join(self.id, &p.room_id, sender)?; + + info!("Connection {} joined the room {}.", self.id, p.room_id); + + for bytes in self.packet_queue.drain(..) { + self.state.rooms.forward_to_host( + &p.room_id, + ConnectionAction::SendTCP(AnyPacket::App( + AppPacket::ConnectionPacketWrap(ConnectionPacketWrapPacket { + connection_id: self.id, + is_tcp: false, + buffer: bytes, + }), + )), + ); + } + } + } + AppPacket::RoomCreationRequest(p) => { + if let Some(current_room_id) = self.state.rooms.find_connection_room_id(self.id) { + self.write_packet(AnyPacket::App(AppPacket::Message2(Message2Packet { + message: MessageType::AlreadyHosting, + }))) + .await?; + warn!( + "Connection {} tried to create a room but is already hosting/in the room {}.", + self.id, current_room_id + ); + return Ok(()); + } + + if let Some(sender) = self.state.get_sender(self.id) { + let room_id = self.state.rooms.create(RoomInit { + connection_id: self.id, + password: p.password, + stats: p.data, + sender, + }); + self.write_packet(AnyPacket::App(AppPacket::RoomLink(RoomLinkPacket { + room_id: room_id.clone(), + }))) + .await?; + + let Some(rooms) = self.state.rooms.read() else { + return Err(anyhow!("Can not read rooms")); + }; + + let Some(room) = rooms.get(&room_id) else { + return Err(anyhow!("Can not find room {}", room_id)); + }; + + if let Err(err) = self.state.rooms.broadcast_sender.send(RoomUpdate::Update { + id: room.id.clone(), + data: room.clone(), + }) { + info!("Fail to broadcast room update {}", err); + } + + info!("Room {} created by connection {}.", room_id, self.id); + } + } + AppPacket::RoomClosureRequest(_) => { + if let Some(room_id) = self.state.rooms.find_connection_room_id(self.id) { + let is_host = if let Some(rooms) = self.state.rooms.read() { + rooms + .get(&room_id) + .map(|r| r.host_connection_id == self.id) + .unwrap_or(false) + } else { + false + }; + + if !is_host { + self.write_packet(AnyPacket::App(AppPacket::Message2(Message2Packet { + message: MessageType::RoomClosureDenied, + }))) + .await?; + warn!( + "Connection {} tried to close the room {} but is not the host.", + self.id, room_id + ); + return Ok(()); + } + + let members = self.state.rooms.get_room_members(&room_id); + for (id, sender) in members { + if id != self.id { + if let Err(e) = sender.try_send(ConnectionAction::SendTCP( + AnyPacket::App(AppPacket::RoomClosed(RoomClosedPacket { + reason: CloseReason::Closed, + })), + )) { + info!("Failed to send room closed packet to {}: {}", id, e); + } + if let Err(e) = sender.try_send(ConnectionAction::Close) { + info!("Failed to send close action to {}: {}", id, e); + } + } + } + + self.state.rooms.close(&room_id); + info!( + "Room {} closed by connection {} (the host).", + room_id, self.id + ); + } + } + AppPacket::ConnectionClosed(p) => { + if let Some(room_id) = self.state.rooms.find_connection_room_id(self.id) { + let is_host = if let Some(rooms) = self.state.rooms.read() { + rooms + .get(&room_id) + .map(|r| r.host_connection_id == self.id) + .unwrap_or(false) + } else { + false + }; + + if !is_host { + self.write_packet(AnyPacket::App(AppPacket::Message2(Message2Packet { + message: MessageType::ConClosureDenied, + }))) + .await?; + warn!("Connection {} tried to close the connection {} but is not the host of room {}.", self.id, p.connection_id, room_id); + return Ok(()); + } + + if let Some(sender) = self.state.get_sender(p.connection_id) { + let target_room = self.state.rooms.find_connection_room_id(p.connection_id); + if target_room.as_ref() == Some(&room_id) { + info!( + "Connection {} (room {}) closed the connection {}.", + self.id, room_id, p.connection_id + ); + + if let Err(e) = + sender.try_send(ConnectionAction::SendTCP(AnyPacket::App( + AppPacket::ConnectionClosed(ConnectionClosedPacket { + connection_id: p.connection_id, + reason: p.reason, + }), + ))) + { + info!( + "Failed to send connection closed packet to {}: {}", + p.connection_id, e + ); + } + if let Err(e) = sender.try_send(ConnectionAction::Close) { + info!("Failed to send close action to {}: {}", p.connection_id, e); + } + } else { + warn!("Connection {} (room {}) tried to close a connection from another room.", self.id, room_id); + } + } + } + } + AppPacket::ConnectionPacketWrap(ConnectionPacketWrapPacket { + connection_id, + is_tcp, + buffer, + }) => { + if let Some(room_id) = self.state.rooms.find_connection_room_id(self.id) { + if let Some(rooms) = self.state.rooms.read() { + let is_owner = rooms + .get(&room_id) + .map(|r| r.host_connection_id == self.id) + .unwrap_or(false); + + if !is_owner { + return Err(anyhow!("Not room owner")); + } + + let Some(sender) = self.state.get_sender(connection_id) else { + warn!("Connection not found: {}", connection_id); + + self.state.rooms.forward_to_host( + &room_id, + ConnectionAction::SendTCP(AnyPacket::App( + AppPacket::ConnectionClosed(ConnectionClosedPacket { + connection_id, + reason: ArcCloseReason::Closed, + }), + )), + ); + + return Ok(()); + }; + + self.state.reset_idle(connection_id); + + let action = if is_tcp { + ConnectionAction::SendTCPRaw(buffer) + } else { + ConnectionAction::SendUDPRaw(buffer) + }; + + if let Err(e) = sender.try_send(action) { + warn!("Failed to forward packet to {}: {}", connection_id, e); + } + } + } else { + info!("No room found for connection {}", self.id); + } + } + _ => { + warn!("Unhandled App Packet: {:?}", packet); + } + } + Ok(()) + } + + async fn handle_action( + &mut self, + action: ConnectionAction, + batch: &mut BytesMut, + ) -> anyhow::Result<()> { + match action { + ConnectionAction::SendTCP(p) => { + let bytes = p.to_bytes(); + info!("Send packet: {:?} to {}", p, self.id); + batch.extend_from_slice(&ConnectionActor::prepend_len(bytes)); + } + ConnectionAction::SendTCPRaw(b) => { + info!("Send tcp {} bytes to {}", b.len(), self.id); + batch.extend_from_slice(&ConnectionActor::prepend_len(b)); + } + ConnectionAction::SendUDPRaw(b) => { + info!("Send udp {} bytes to {}", b.len(), self.id); + self.udp_writer.send_raw(&b).await?; + } + ConnectionAction::Close => { + // Return error to break loop + info!("Close connection {}", self.id); + return Err(anyhow::anyhow!("Closed")); + } + ConnectionAction::RegisterUDP(addr) => { + if self.udp_writer.addr.is_some() { + return Ok(()); + } + + self.udp_writer.set_addr(addr); + + info!("New connection {} from {}", self.id, addr); + + // Register in state + if let Some(sender) = self.state.get_sender(self.id) { + self.state.register_udp(addr, sender, self.limiter.clone()); + } else { + return Err(anyhow::anyhow!( + "No sender found for connection {}", + self.id + )); + } + // Send reply + self.write_packet(AnyPacket::Framework(FrameworkMessage::RegisterUDP { + connection_id: self.id, + })) + .await?; + } + ConnectionAction::ProcessPacket(packet, is_tcp) => { + self.state.reset_idle(self.id); + self.handle_packet(packet, is_tcp).await?; + } + } + Ok(()) + } + + pub fn prepend_len(payload: BytesMut) -> BytesMut { + let mut out: BytesMut = BytesMut::with_capacity(2 + payload.len()); + + out.put_u16(payload.len() as u16); + out.extend_from_slice(&payload); + + out + } + + async fn write_packet(&mut self, packet: AnyPacket) -> anyhow::Result<()> { + self.tcp_writer + .write(&ConnectionActor::prepend_len(packet.to_bytes())) + .await + } +} diff --git a/src/constant.rs b/src/constant.rs index c712343..e829ab6 100644 --- a/src/constant.rs +++ b/src/constant.rs @@ -1,4 +1,6 @@ +use crate::error::AppError; use serde_repr::{Deserialize_repr, Serialize_repr}; +use std::convert::TryFrom; #[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr, PartialEq)] #[repr(u8)] @@ -10,6 +12,24 @@ pub enum CloseReason { PackingSpamming = 4, } +impl TryFrom for CloseReason { + type Error = AppError; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(CloseReason::Closed), + 1 => Ok(CloseReason::ObsoleteClient), + 2 => Ok(CloseReason::OutdatedVersion), + 3 => Ok(CloseReason::ServerClosed), + 4 => Ok(CloseReason::PackingSpamming), + _ => Err(AppError::PacketParsing(format!( + "Invalid CloseReason: {}", + value + ))), + } + } +} + #[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr, PartialEq)] #[repr(u8)] pub enum ArcCloseReason { @@ -18,13 +38,18 @@ pub enum ArcCloseReason { Error = 2, } -impl ArcCloseReason { - pub fn from(value: u8) -> Self { +impl TryFrom for ArcCloseReason { + type Error = AppError; + + fn try_from(value: u8) -> Result { match value { - 0 => ArcCloseReason::Closed, - 1 => ArcCloseReason::Timeout, - 2 => ArcCloseReason::Error, - _ => panic!("Invalid ArcCloseReason number"), + 0 => Ok(ArcCloseReason::Closed), + 1 => Ok(ArcCloseReason::Timeout), + 2 => Ok(ArcCloseReason::Error), + _ => Err(AppError::PacketParsing(format!( + "Invalid ArcCloseReason: {}", + value + ))), } } } @@ -38,3 +63,21 @@ pub enum MessageType { RoomClosureDenied = 3, ConClosureDenied = 4, } + +impl TryFrom for MessageType { + type Error = AppError; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(MessageType::ServerClosing), + 1 => Ok(MessageType::PacketSpamming), + 2 => Ok(MessageType::AlreadyHosting), + 3 => Ok(MessageType::RoomClosureDenied), + 4 => Ok(MessageType::ConClosureDenied), + _ => Err(AppError::PacketParsing(format!( + "Invalid MessageType: {}", + value + ))), + } + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..0508fbc --- /dev/null +++ b/src/error.rs @@ -0,0 +1,28 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum AppError { + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + + #[error("Packet parsing error: {0}")] + PacketParsing(String), + + #[error("Room not found: {0}")] + RoomNotFound(String), + + #[error("Authentication failed: {0}")] + AuthFailed(String), + + #[error("Packet spamming detected")] + PacketSpamming, + + #[error("Lock poison error")] + LockPoison, + + #[error("Invalid state: {0}")] + InvalidState(String), + + #[error("Unknown error: {0}")] + Unknown(String), +} diff --git a/src/http_server.rs b/src/http_server.rs index 710ee4f..de6e2b5 100644 --- a/src/http_server.rs +++ b/src/http_server.rs @@ -1,4 +1,6 @@ -use crate::state::{AppState, RemoveRemoveEvent, RoomUpdate, RoomUpdateEvent, RoomView}; +use crate::models::{RemoveRemoveEvent, RoomUpdateEvent, RoomView}; +use crate::packet::RoomId; +use crate::state::{AppState, RoomUpdate}; use axum::{ extract::{Path, State}, http::header, @@ -36,7 +38,7 @@ async fn ping() -> impl IntoResponse { } async fn rooms_sse(State(state): State>) -> impl IntoResponse { - let rx = state.rooms.tx.subscribe(); + let rx = state.rooms.broadcast_sender.subscribe(); let stream = BroadcastStream::new(rx); let initial_rooms: Vec = { @@ -46,7 +48,7 @@ async fn rooms_sse(State(state): State>) -> impl IntoResponse { rooms .iter() .map(|(key, room)| RoomUpdateEvent { - room_id: key.clone(), + room_id: key.0.clone(), data: RoomView::from(room), }) .collect() @@ -68,13 +70,13 @@ async fn rooms_sse(State(state): State>) -> impl IntoResponse { RoomUpdate::Update { id, data } => Event::default() .event("update") .json_data(vec![RoomUpdateEvent { - room_id: id.clone(), + room_id: id.0, data: RoomView::from(&data), }]) .map_err(axum::BoxError::from), RoomUpdate::Remove(id) => Event::default() .event("remove") - .json_data(RemoveRemoveEvent { room_id: id }) + .json_data(RemoveRemoveEvent { room_id: id.0 }) .map_err(axum::BoxError::from), }; @@ -101,7 +103,7 @@ async fn room_page( ) -> impl IntoResponse { let stats = { if let Some(rooms) = state.rooms.read() { - rooms.get(&room_id).map(|r| r.stats.clone()) + rooms.get(&RoomId(room_id)).map(|r| r.stats.clone()) } else { None } @@ -167,9 +169,6 @@ async fn room_page( } } -async fn room_port(State(_state): State>) -> impl IntoResponse { - // Return the TCP port (which is in config, maybe we should store it in state or config) - // For now hardcode or get from config if we passed it. - // Ideally pass config to state. - "11010" // Using default, should be dynamic +async fn room_port(State(state): State>) -> impl IntoResponse { + state.config.player_connect_port.to_string() } diff --git a/src/main.rs b/src/main.rs index 86f0385..fb242f3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,18 @@ -mod config; -mod constant; -mod packets; -mod state; -mod proxy_server; -mod http_server; -mod utils; -mod rate; +pub mod config; +pub mod connection; +pub mod constant; +pub mod error; +pub mod http_server; +pub mod models; +pub mod packet; +pub mod proxy_server; +pub mod rate; +pub mod state; +pub mod utils; +pub mod writer; -use crate::config::Config; -use crate::state::AppState; +use config::Config; +use state::AppState; use std::sync::Arc; use tracing::{info, Level}; use tracing_subscriber::FmtSubscriber; @@ -19,8 +23,7 @@ async fn main() -> anyhow::Result<()> { let subscriber = FmtSubscriber::builder() .with_max_level(Level::INFO) .finish(); - tracing::subscriber::set_global_default(subscriber) - .expect("setting default subscriber failed"); + tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); // Load config let config = Config::from_env()?; @@ -29,7 +32,7 @@ async fn main() -> anyhow::Result<()> { info!("HTTP Port: {}", config.player_connect_http_port); // Initialize state - let state = Arc::new(AppState::new()); + let state = Arc::new(AppState::new(config.clone())); // Start Proxy Server let proxy_state = state.clone(); diff --git a/src/models.rs b/src/models.rs new file mode 100644 index 0000000..ce1cdae --- /dev/null +++ b/src/models.rs @@ -0,0 +1,54 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Stats { + pub players: Vec, + #[serde(rename = "mapName")] + pub map_name: String, + pub name: String, + pub gamemode: String, + pub mods: Vec, + pub locale: String, + pub version: String, + #[serde(rename = "createdAt")] + pub created_at: u128, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Player { + pub name: String, + pub locale: String, +} + +#[derive(Clone, Debug, Serialize)] +pub struct RoomView { + pub name: String, + pub status: String, + #[serde(rename = "isPrivate")] + pub is_private: bool, + #[serde(rename = "isSecured")] + pub is_secured: bool, + pub players: Vec, + #[serde(rename = "mapName")] + pub map_name: String, + pub gamemode: String, + pub mods: Vec, + pub locale: String, + pub version: String, + #[serde(rename = "createdAt")] + pub created_at: u128, + pub ping: u128, +} + +#[derive(Clone, Debug, Serialize)] +pub struct RemoveRemoveEvent { + #[serde(rename = "roomId")] + pub room_id: String, +} + +#[derive(Clone, Debug, Serialize)] +pub struct RoomUpdateEvent { + #[serde(rename = "roomId")] + pub room_id: String, + pub data: RoomView, +} diff --git a/src/packets.rs b/src/packet.rs similarity index 72% rename from src/packets.rs rename to src/packet.rs index 230dbd4..78e3a82 100644 --- a/src/packets.rs +++ b/src/packet.rs @@ -1,15 +1,31 @@ -use crate::{ - constant::{ArcCloseReason, CloseReason, MessageType}, - state::Stats, -}; -use anyhow::anyhow; +use crate::constant::{ArcCloseReason, CloseReason, MessageType}; +use crate::models::Stats; +use crate::error::AppError; use bytes::{Buf, BufMut, Bytes, BytesMut}; +use std::convert::TryFrom; use std::io::Cursor; -use tracing::info; pub const APP_PACKET_ID: i8 = -4; pub const FRAMEWORK_PACKET_ID: i8 = -2; +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ConnectionId(pub i32); + +impl std::fmt::Display for ConnectionId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct RoomId(pub String); + +impl std::fmt::Display for RoomId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + #[derive(Debug, Clone)] pub enum AnyPacket { Framework(FrameworkMessage), @@ -22,8 +38,8 @@ pub enum FrameworkMessage { Ping { id: i32, is_reply: bool }, DiscoverHost, KeepAlive, - RegisterUDP { connection_id: i32 }, - RegisterTCP { connection_id: i32 }, + RegisterUDP { connection_id: ConnectionId }, + RegisterTCP { connection_id: ConnectionId }, } #[derive(Debug, Clone)] @@ -45,36 +61,36 @@ pub enum AppPacket { #[derive(Debug, Clone)] pub struct ConnectionPacketWrapPacket { - pub connection_id: i32, + pub connection_id: ConnectionId, pub is_tcp: bool, pub buffer: BytesMut, } #[derive(Debug, Clone)] pub struct ConnectionClosedPacket { - pub connection_id: i32, + pub connection_id: ConnectionId, pub reason: ArcCloseReason, } #[derive(Debug, Clone)] pub struct ConnectionJoinPacket { - pub connection_id: i32, - pub room_id: String, + pub connection_id: ConnectionId, + pub room_id: RoomId, } #[derive(Debug, Clone)] pub struct ConnectionIdlingPacket { - pub connection_id: i32, + pub connection_id: ConnectionId, } #[derive(Debug, Clone)] pub struct RoomLinkPacket { - pub room_id: String, + pub room_id: RoomId, } #[derive(Debug, Clone)] pub struct RoomJoinPacket { - pub room_id: String, + pub room_id: RoomId, pub password: String, } @@ -114,9 +130,9 @@ pub struct StatsPacket { } impl AnyPacket { - pub fn read(buf: &mut Cursor) -> anyhow::Result { + pub fn read(buf: &mut Cursor) -> Result { if !buf.has_remaining() { - return Err(anyhow!("Empty packet")); + return Err(AppError::PacketParsing("Empty packet".to_string())); } let id = buf.get_i8(); @@ -152,13 +168,12 @@ impl AnyPacket { payload.extend_from_slice(raw); } } - payload } } impl FrameworkMessage { - pub fn read(buf: &mut Cursor) -> anyhow::Result { + pub fn read(buf: &mut Cursor) -> Result { let fid = buf.get_u8(); match fid { @@ -169,12 +184,15 @@ impl FrameworkMessage { 1 => Ok(FrameworkMessage::DiscoverHost), 2 => Ok(FrameworkMessage::KeepAlive), 3 => Ok(FrameworkMessage::RegisterUDP { - connection_id: buf.get_i32(), + connection_id: ConnectionId(buf.get_i32()), }), 4 => Ok(FrameworkMessage::RegisterTCP { - connection_id: buf.get_i32(), + connection_id: ConnectionId(buf.get_i32()), }), - _ => Err(anyhow!("Unknown Framework ID: {}", fid)), + _ => Err(AppError::PacketParsing(format!( + "Unknown Framework ID: {}", + fid + ))), } } @@ -191,23 +209,23 @@ impl FrameworkMessage { FrameworkMessage::KeepAlive => buf.put_u8(2), FrameworkMessage::RegisterUDP { connection_id } => { buf.put_u8(3); - buf.put_i32(*connection_id); + buf.put_i32(connection_id.0); } FrameworkMessage::RegisterTCP { connection_id } => { buf.put_u8(4); - buf.put_i32(*connection_id); + buf.put_i32(connection_id.0); } } } } impl AppPacket { - pub fn read(buf: &mut Cursor) -> anyhow::Result { + pub fn read(buf: &mut Cursor) -> Result { let pid = buf.get_u8(); match pid { 0 => { - let connection_id = buf.get_i32(); + let connection_id = ConnectionId(buf.get_i32()); let is_tcp = buf.get_u8() != 0; let start = buf.position() as usize; @@ -225,15 +243,15 @@ impl AppPacket { )) } 1 => Ok(AppPacket::ConnectionClosed(ConnectionClosedPacket { - connection_id: buf.get_i32(), - reason: ArcCloseReason::from(buf.get_u8()), // DcReason ordinal + connection_id: ConnectionId(buf.get_i32()), + reason: ArcCloseReason::try_from(buf.get_u8())?, })), 2 => Ok(AppPacket::ConnectionJoin(ConnectionJoinPacket { - connection_id: buf.get_i32(), - room_id: read_string(buf)?, + connection_id: ConnectionId(buf.get_i32()), + room_id: RoomId(read_string(buf)?), })), 3 => Ok(AppPacket::ConnectionIdling(ConnectionIdlingPacket { - connection_id: buf.get_i32(), + connection_id: ConnectionId(buf.get_i32()), })), 4 => Ok(AppPacket::RoomCreationRequest(RoomCreationRequestPacket { version: read_string(buf)?, @@ -242,20 +260,20 @@ impl AppPacket { })), 5 => Ok(AppPacket::RoomClosureRequest(RoomClosureRequestPacket)), 6 => Ok(AppPacket::RoomClosed(RoomClosedPacket { - reason: unsafe { std::mem::transmute(buf.get_u8()) }, // Unsafe or impl TryFrom + reason: CloseReason::try_from(buf.get_u8())?, })), 7 => Ok(AppPacket::RoomLink(RoomLinkPacket { - room_id: read_string(buf)?, + room_id: RoomId(read_string(buf)?), })), 8 => Ok(AppPacket::RoomJoin(RoomJoinPacket { - room_id: read_string(buf)?, + room_id: RoomId(read_string(buf)?), password: read_string(buf)?, })), 9 => Ok(AppPacket::Message(MessagePacket { message: read_string(buf)?, })), 10 => Ok(AppPacket::Message2(Message2Packet { - message: unsafe { std::mem::transmute(buf.get_u8()) }, + message: MessageType::try_from(buf.get_u8())?, })), 11 => Ok(AppPacket::Popup(PopupPacket { message: read_string(buf)?, @@ -263,7 +281,10 @@ impl AppPacket { 12 => Ok(AppPacket::Stats(StatsPacket { data: read_stats(buf)?, })), - _ => Err(anyhow!("Unknown App Packet ID: {}", pid)), + _ => Err(AppError::PacketParsing(format!( + "Unknown App Packet ID: {}", + pid + ))), } } @@ -273,23 +294,23 @@ impl AppPacket { match self { AppPacket::ConnectionPacketWrap(p) => { buf.put_u8(0); - buf.put_i32(p.connection_id); + buf.put_i32(p.connection_id.0); buf.put_u8(if p.is_tcp { 1 } else { 0 }); buf.extend_from_slice(&p.buffer); } AppPacket::ConnectionClosed(p) => { buf.put_u8(1); - buf.put_i32(p.connection_id); + buf.put_i32(p.connection_id.0); buf.put_u8(p.reason as u8); } AppPacket::ConnectionJoin(p) => { buf.put_u8(2); - buf.put_i32(p.connection_id); - write_string(buf, &p.room_id); + buf.put_i32(p.connection_id.0); + write_string(buf, &p.room_id.0); } AppPacket::ConnectionIdling(p) => { buf.put_u8(3); - buf.put_i32(p.connection_id); + buf.put_i32(p.connection_id.0); } AppPacket::RoomCreationRequest(_) => { panic!("Client only") @@ -303,11 +324,11 @@ impl AppPacket { } AppPacket::RoomLink(p) => { buf.put_u8(7); - write_string(buf, &p.room_id); + write_string(buf, &p.room_id.0); } AppPacket::RoomJoin(p) => { buf.put_u8(8); - write_string(buf, &p.room_id); + write_string(buf, &p.room_id.0); write_string(buf, &p.password); } AppPacket::Message(p) => { @@ -329,30 +350,29 @@ impl AppPacket { } } -// Helper to read/write strings -pub fn read_string(buf: &mut Cursor) -> anyhow::Result { +pub fn read_string(buf: &mut Cursor) -> Result { if buf.remaining() < 2 { - return Err(anyhow!( + return Err(AppError::PacketParsing(format!( "Not enough bytes for string length: {}", buf.remaining() - )); + ))); } let len = buf.get_u16() as usize; if buf.remaining() < len { - return Err(anyhow!( + return Err(AppError::PacketParsing(format!( "Not enough bytes for string content, expected {}, got {}", len, buf.remaining() - )); + ))); } let mut bytes = vec![0u8; len]; buf.copy_to_slice(&mut bytes); - String::from_utf8(bytes).map_err(|e| anyhow!(e)) + String::from_utf8(bytes).map_err(|e| AppError::PacketParsing(e.to_string())) } pub fn write_string(buf: &mut BytesMut, s: &str) { @@ -361,14 +381,17 @@ pub fn write_string(buf: &mut BytesMut, s: &str) { buf.put_slice(bytes); } -pub fn read_stats(buf: &mut Cursor) -> anyhow::Result { +pub fn read_stats(buf: &mut Cursor) -> Result { let json = read_string(buf)?; match serde_json::from_str::(&json) { Ok(data) => Ok(data), Err(e) => { - info!("Failed to parse stats: {}", json); - Err(anyhow!(e)) + // Log? No, we return error. + Err(AppError::PacketParsing(format!( + "Failed to parse stats: {}. JSON: {}", + e, json + ))) } } } diff --git a/src/proxy_server.rs b/src/proxy_server.rs index 3ba5051..a237aaf 100644 --- a/src/proxy_server.rs +++ b/src/proxy_server.rs @@ -1,34 +1,24 @@ -use crate::constant::{ArcCloseReason, CloseReason, MessageType}; -use crate::packets::{ - AnyPacket, AppPacket, ConnectionClosedPacket, ConnectionPacketWrapPacket, FrameworkMessage, - Message2Packet, MessagePacket, RoomClosedPacket, RoomLinkPacket, -}; +use crate::connection::ConnectionActor; +use crate::packet::{AnyPacket, FrameworkMessage}; use crate::rate::AtomicRateLimiter; -use crate::state::{AppState, ConnectionAction, RoomInit, RoomUpdate}; -use crate::utils::current_time_millis; -use anyhow::anyhow; -use bytes::{Buf, BufMut, Bytes, BytesMut}; +use crate::state::{AppState, ConnectionAction}; +use crate::writer::{TcpWriter, UdpWriter}; +use bytes::Bytes; use std::io::Cursor; use std::net::SocketAddr; use std::sync::atomic::{AtomicI32, Ordering}; use std::sync::Arc; use std::time::{Duration, Instant}; -use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::{TcpListener, UdpSocket}; use tokio::sync::mpsc; use tracing::{error, info, warn}; +use crate::packet::ConnectionId; + static NEXT_CONNECTION_ID: AtomicI32 = AtomicI32::new(1); const UDP_BUFFER_SIZE: usize = 4096; -const TCP_BUFFER_SIZE: usize = 32768; const CHANNEL_CAPACITY: usize = 100; -const CONNECTION_TIME_OUT_MS: u64 = 30000; -const IDLE_TIMEOUT_MS: u64 = 5000; -const KEEP_ALIVE_INTERVAL_MS: u64 = 2000; -const PACKET_LENGTH_LENGTH: usize = 2; -const TICK_INTERVAL_SECS: u64 = 1; - const PACKET_RATE_LIMIT_WINDOW: Duration = Duration::from_millis(3000); const PACKET_RATE_LIMIT: u32 = 300; @@ -88,7 +78,7 @@ fn spawn_udp_listener(state: Arc, socket: Arc) { }); } -async fn handle_register_udp(state: &Arc, connection_id: i32, addr: SocketAddr) { +async fn handle_register_udp(state: &Arc, connection_id: ConnectionId, addr: SocketAddr) { if let Some(sender) = state.get_sender(connection_id) { if let Err(e) = sender.try_send(ConnectionAction::RegisterUDP(addr)) { info!( @@ -114,7 +104,7 @@ async fn accept_tcp_connection( if let Err(e) = socket.set_nodelay(true) { warn!("Failed to set nodelay for connection: {}", e); } - let id = NEXT_CONNECTION_ID.fetch_add(1, Ordering::Relaxed); + let id = ConnectionId(NEXT_CONNECTION_ID.fetch_add(1, Ordering::Relaxed)); let (tx, rx) = mpsc::channel(CHANNEL_CAPACITY); let limiter = Arc::new(AtomicRateLimiter::new( @@ -150,645 +140,3 @@ async fn accept_tcp_connection( }); } } - -struct ConnectionActor { - id: i32, - state: Arc, - rx: mpsc::Receiver, - tcp_writer: TcpWriter, - udp_writer: UdpWriter, - limiter: Arc, - last_read: Instant, - packet_queue: Vec, -} - -impl ConnectionActor { - async fn run(&mut self, mut reader: tokio::net::tcp::OwnedReadHalf) -> anyhow::Result<()> { - let register_packet = AnyPacket::Framework(FrameworkMessage::RegisterTCP { - connection_id: self.id, - }); - - self.write_packet(register_packet).await?; - - let mut buf = BytesMut::with_capacity(TCP_BUFFER_SIZE); - let mut tmp_buf = [0u8; TCP_BUFFER_SIZE]; - let mut tick_interval = tokio::time::interval(Duration::from_secs(TICK_INTERVAL_SECS)); - - loop { - let mut batch = BytesMut::new(); - - tokio::select! { - // TCP Read - read_result = reader.read(&mut tmp_buf) => { - match read_result { - Ok(0) => break, // EOF - Ok(n) => { - self.last_read = Instant::now(); - self.state.reset_idle(self.id); - - buf.extend_from_slice(&tmp_buf[..n]); - self.process_tcp_buffer(&mut buf).await?; - } - Err(e) => return Err(e.into()), - } - } - - // Channel Read - action = self.rx.recv() => { - if let Some(action) = action { - self.handle_action(action, &mut batch).await?; - - while let Ok(action) = self.rx.try_recv() { - self.handle_action(action, &mut batch).await?; - } - // Flush batch - if !batch.is_empty() { - self.tcp_writer.write(&batch).await?; - } - } else { - // Channel closed - break; - } - } - - // Tick - _ = tick_interval.tick() => { - if self.last_read.elapsed() > Duration::from_millis(CONNECTION_TIME_OUT_MS) { - info!("Connection {} timed out", self.id); - break; - } - - if self.last_read.elapsed() > Duration::from_millis(IDLE_TIMEOUT_MS) { - self.state.idle(self.id); - } - - if self.tcp_writer.last_write.elapsed() > Duration::from_millis(KEEP_ALIVE_INTERVAL_MS) { - self.write_packet(AnyPacket::Framework(FrameworkMessage::KeepAlive)).await?; - } - } - } - } - - Ok(()) - } - - async fn process_tcp_buffer(&mut self, buf: &mut BytesMut) -> anyhow::Result<()> { - loop { - if buf.len() < PACKET_LENGTH_LENGTH { - break; - } - - let len = { - let mut cur = Cursor::new(&buf[..]); - cur.get_u16() as usize - }; - - if buf.len() < PACKET_LENGTH_LENGTH + len { - break; - } - - buf.advance(PACKET_LENGTH_LENGTH); - - let payload = buf.split_to(len).freeze(); - let mut cursor = Cursor::new(payload); - - match AnyPacket::read(&mut cursor) { - Ok(packet) => { - self.handle_packet(packet, true).await?; - } - Err(e) => { - error!("Error reading packet: {:?}", e); - continue; - } - } - // Handle packet - } - Ok(()) - } - - async fn handle_packet(&mut self, packet: AnyPacket, is_tcp: bool) -> anyhow::Result<()> { - let is_framework = matches!(packet, AnyPacket::Framework(_)); - - if !is_framework { - info!("Received TCP packet: {:?} from {}", packet, self.id); - - let room_id_opt = self.state.rooms.find_connection_room_id(self.id); - let is_host = if let Some(ref room_id) = room_id_opt { - if let Some(rooms) = self.state.rooms.read() { - rooms - .get(room_id) - .map(|r| r.host_connection_id == self.id) - .unwrap_or(false) - } else { - false - } - } else { - false - }; - - if !is_host && !self.limiter.check() { - if let Some(ref room_id) = room_id_opt { - self.state.rooms.broadcast( - room_id, - ConnectionAction::SendTCP(AnyPacket::App(AppPacket::Message2( - Message2Packet { - message: MessageType::PacketSpamming, - }, - ))), - None, - ); - } - - self.write_packet(AnyPacket::App(AppPacket::ConnectionClosed( - ConnectionClosedPacket { - connection_id: self.id, - reason: ArcCloseReason::Closed, - }, - ))) - .await?; - - warn!("Connection {} disconnected for packet spamming.", self.id); - return Err(anyhow!("Packet Spamming")); - } - } - - match packet { - AnyPacket::Framework(f) => self.handle_framework(f).await?, - AnyPacket::App(a) => self.handle_app(a).await?, - AnyPacket::Raw(bytes) => { - if let Some(room_id) = self.state.rooms.find_connection_room_id(self.id) { - let packet = AnyPacket::App(AppPacket::ConnectionPacketWrap( - ConnectionPacketWrapPacket { - connection_id: self.id, - is_tcp, - buffer: bytes, - }, - )); - - self.state - .rooms - .forward_to_host(&room_id, ConnectionAction::SendTCP(packet)); - } else { - if self.packet_queue.len() < 16 { - self.packet_queue.push(bytes); - info!("Queued raw packet for connection {}", self.id); - } else { - warn!( - "Connection {} packet queue full, dropping raw packet", - self.id - ); - } - } - } - } - Ok(()) - } - - async fn handle_framework(&mut self, packet: FrameworkMessage) -> anyhow::Result<()> { - match packet { - FrameworkMessage::Ping { id, is_reply } => { - if !is_reply { - self.write_packet(AnyPacket::Framework(FrameworkMessage::Ping { - id, - is_reply: true, - })) - .await?; - } - } - FrameworkMessage::KeepAlive => { - // Handled by activity update - } - FrameworkMessage::RegisterUDP { .. } => { - // Should not happen via TCP? - // But if it does, ignore? - } - _ => { - warn!("Unhandled Framework Packet: {:?}", packet); - } - } - Ok(()) - } - - async fn handle_app(&mut self, packet: AppPacket) -> anyhow::Result<()> { - match packet { - AppPacket::Stats(p) => { - if let Some(room_id) = self.state.rooms.find_connection_room_id(self.id) { - if let Ok(mut rooms) = self.state.rooms.rooms.write() { - if let Some(room) = rooms.get_mut(&room_id) { - let sent_at = p.data.created_at; - - room.stats = p.data; - room.updated_at = current_time_millis(); - room.ping = current_time_millis() - sent_at; - - if let Err(err) = self.state.rooms.tx.send(RoomUpdate::Update { - id: room.id.clone(), - data: room.clone(), - }) { - info!("Fail to broadcast room update {}", err); - } - } - } - } - } - AppPacket::RoomJoin(p) => { - if let Some(current_room_id) = self.state.rooms.find_connection_room_id(self.id) { - let is_host = if let Some(rooms) = self.state.rooms.read() { - rooms - .get(¤t_room_id) - .map(|r| r.host_connection_id == self.id) - .unwrap_or(false) - } else { - false - }; - - if is_host { - self.write_packet(AnyPacket::App(AppPacket::Message2(Message2Packet { - message: MessageType::AlreadyHosting, - }))) - .await?; - - warn!( - "Connection {} tried to join room {} but is already hosting {}", - self.id, p.room_id, current_room_id - ); - return Ok(()); - } - - info!( - "Connection {} left room {} to join {}", - self.id, current_room_id, p.room_id - ); - self.state.rooms.leave(self.id); - } - - let (can_join, wrong_password) = (|| { - let rooms = self.state.rooms.read()?; - let room = rooms.get(&p.room_id)?; - - if let Some(ref pass) = room.password { - if pass != &p.password { - return Some((false, true)); - } - } - - Some((true, false)) - })() - .unwrap_or((false, false)); - - if wrong_password { - info!( - "Connection {} tried to join room {} with wrong password.", - self.id, p.room_id - ); - self.write_packet(AnyPacket::App(AppPacket::Message(MessagePacket { - message: "Wrong password".to_string(), - }))) - .await?; - return Ok(()); - } - - if !can_join { - info!( - "Connection {} tried to join a non-existent room {}.", - self.id, p.room_id - ); - self.write_packet(AnyPacket::App(AppPacket::ConnectionClosed( - ConnectionClosedPacket { - connection_id: self.id, - reason: ArcCloseReason::Error, - }, - ))) - .await?; - return Ok(()); - } - - if let Some(sender) = self.state.get_sender(self.id) { - self.state.rooms.join(self.id, &p.room_id, sender)?; - - info!("Connection {} joined the room {}.", self.id, p.room_id); - - for bytes in self.packet_queue.drain(..) { - self.state.rooms.forward_to_host( - &p.room_id, - ConnectionAction::SendTCP(AnyPacket::App( - AppPacket::ConnectionPacketWrap(ConnectionPacketWrapPacket { - connection_id: self.id, - is_tcp: false, - buffer: bytes, - }), - )), - ); - } - } - } - AppPacket::RoomCreationRequest(p) => { - if let Some(current_room_id) = self.state.rooms.find_connection_room_id(self.id) { - self.write_packet(AnyPacket::App(AppPacket::Message2(Message2Packet { - message: MessageType::AlreadyHosting, - }))) - .await?; - warn!( - "Connection {} tried to create a room but is already hosting/in the room {}.", - self.id, current_room_id - ); - return Ok(()); - } - - if let Some(sender) = self.state.get_sender(self.id) { - let room_id = self.state.rooms.create(RoomInit { - connection_id: self.id, - password: p.password, - stats: p.data, - sender, - }); - self.write_packet(AnyPacket::App(AppPacket::RoomLink(RoomLinkPacket { - room_id: room_id.clone(), - }))) - .await?; - - let Some(rooms) = self.state.rooms.read() else { - return Err(anyhow!("Can not read rooms")); - }; - - let Some(room) = rooms.get(&room_id) else { - return Err(anyhow!("Can not find room {}", room_id)); - }; - - if let Err(err) = self.state.rooms.tx.send(RoomUpdate::Update { - id: room.id.clone(), - data: room.clone(), - }) { - info!("Fail to broadcast room update {}", err); - } - - info!("Room {} created by connection {}.", room_id, self.id); - } - } - AppPacket::RoomClosureRequest(_) => { - if let Some(room_id) = self.state.rooms.find_connection_room_id(self.id) { - let is_host = if let Some(rooms) = self.state.rooms.read() { - rooms - .get(&room_id) - .map(|r| r.host_connection_id == self.id) - .unwrap_or(false) - } else { - false - }; - - if !is_host { - self.write_packet(AnyPacket::App(AppPacket::Message2(Message2Packet { - message: MessageType::RoomClosureDenied, - }))) - .await?; - warn!( - "Connection {} tried to close the room {} but is not the host.", - self.id, room_id - ); - return Ok(()); - } - - let members = self.state.rooms.get_room_members(&room_id); - for (id, sender) in members { - if id != self.id { - if let Err(e) = sender.try_send(ConnectionAction::SendTCP( - AnyPacket::App(AppPacket::RoomClosed(RoomClosedPacket { - reason: CloseReason::Closed, - })), - )) { - info!("Failed to send room closed packet to {}: {}", id, e); - } - if let Err(e) = sender.try_send(ConnectionAction::Close) { - info!("Failed to send close action to {}: {}", id, e); - } - } - } - - self.state.rooms.close(&room_id); - info!( - "Room {} closed by connection {} (the host).", - room_id, self.id - ); - } - } - AppPacket::ConnectionClosed(p) => { - if let Some(room_id) = self.state.rooms.find_connection_room_id(self.id) { - let is_host = if let Some(rooms) = self.state.rooms.read() { - rooms - .get(&room_id) - .map(|r| r.host_connection_id == self.id) - .unwrap_or(false) - } else { - false - }; - - if !is_host { - self.write_packet(AnyPacket::App(AppPacket::Message2(Message2Packet { - message: MessageType::ConClosureDenied, - }))) - .await?; - warn!("Connection {} tried to close the connection {} but is not the host of room {}.", self.id, p.connection_id, room_id); - return Ok(()); - } - - if let Some(sender) = self.state.get_sender(p.connection_id) { - let target_room = self.state.rooms.find_connection_room_id(p.connection_id); - if target_room.as_ref() == Some(&room_id) { - info!( - "Connection {} (room {}) closed the connection {}.", - self.id, room_id, p.connection_id - ); - - if let Err(e) = - sender.try_send(ConnectionAction::SendTCP(AnyPacket::App( - AppPacket::ConnectionClosed(ConnectionClosedPacket { - connection_id: p.connection_id, - reason: p.reason, - }), - ))) - { - info!( - "Failed to send connection closed packet to {}: {}", - p.connection_id, e - ); - } - if let Err(e) = sender.try_send(ConnectionAction::Close) { - info!("Failed to send close action to {}: {}", p.connection_id, e); - } - } else { - warn!("Connection {} (room {}) tried to close a connection from another room.", self.id, room_id); - } - } - } - } - AppPacket::ConnectionPacketWrap(ConnectionPacketWrapPacket { - connection_id, - is_tcp, - buffer, - }) => { - if let Some(room_id) = self.state.rooms.find_connection_room_id(self.id) { - if let Some(rooms) = self.state.rooms.read() { - let is_owner = rooms - .get(&room_id) - .map(|r| r.host_connection_id == self.id) - .unwrap_or(false); - - if !is_owner { - return Err(anyhow!("Not room owner")); - } - - let Some(sender) = self.state.get_sender(connection_id) else { - warn!("Connection not found: {}", connection_id); - - self.state.rooms.forward_to_host( - &room_id, - ConnectionAction::SendTCP(AnyPacket::App( - AppPacket::ConnectionClosed(ConnectionClosedPacket { - connection_id, - reason: ArcCloseReason::Closed, - }), - )), - ); - - return Ok(()); - }; - - self.state.reset_idle(connection_id); - - let action = if is_tcp { - ConnectionAction::SendTCPRaw(buffer) - } else { - ConnectionAction::SendUDPRaw(buffer) - }; - - if let Err(e) = sender.try_send(action) { - warn!("Failed to forward packet to {}: {}", connection_id, e); - } - } - } else { - info!("No room found for connection {}", self.id); - } - } - _ => { - warn!("Unhandled App Packet: {:?}", packet); - } - } - Ok(()) - } - - async fn handle_action( - &mut self, - action: ConnectionAction, - batch: &mut BytesMut, - ) -> anyhow::Result<()> { - match action { - ConnectionAction::SendTCP(p) => { - let bytes = p.to_bytes(); - info!("Send packet: {:?} to {}", p, self.id); - batch.extend_from_slice(&ConnectionActor::prepend_len(bytes)); - } - ConnectionAction::SendTCPRaw(b) => { - info!("Send tcp {} bytes to {}", b.len(), self.id); - batch.extend_from_slice(&ConnectionActor::prepend_len(b)); - } - ConnectionAction::SendUDPRaw(b) => { - info!("Send udp {} bytes to {}", b.len(), self.id); - self.udp_writer.send_raw(&b).await?; - } - ConnectionAction::Close => { - // Return error to break loop - info!("Close connection {}", self.id); - return Err(anyhow::anyhow!("Closed")); - } - ConnectionAction::RegisterUDP(addr) => { - if self.udp_writer.addr.is_some() { - return Ok(()); - } - - self.udp_writer.set_addr(addr); - - info!("New connection {} from {}", self.id, addr); - - // Register in state - if let Some(sender) = self.state.get_sender(self.id) { - self.state.register_udp(addr, sender, self.limiter.clone()); - } else { - return Err(anyhow::anyhow!( - "No sender found for connection {}", - self.id - )); - } - // Send reply - self.write_packet(AnyPacket::Framework(FrameworkMessage::RegisterUDP { - connection_id: self.id, - })) - .await?; - } - ConnectionAction::ProcessPacket(packet, is_tcp) => { - self.state.reset_idle(self.id); - self.handle_packet(packet, is_tcp).await?; - } - } - Ok(()) - } - - pub fn prepend_len(payload: BytesMut) -> BytesMut { - let mut out: BytesMut = BytesMut::with_capacity(2 + payload.len()); - - out.put_u16(payload.len() as u16); - out.extend_from_slice(&payload); - - out - } - - async fn write_packet(&mut self, packet: AnyPacket) -> anyhow::Result<()> { - self.tcp_writer - .write(&ConnectionActor::prepend_len(packet.to_bytes())) - .await - } -} - -struct TcpWriter { - writer: tokio::net::tcp::OwnedWriteHalf, - last_write: Instant, -} - -impl TcpWriter { - fn new(writer: tokio::net::tcp::OwnedWriteHalf) -> Self { - Self { - writer, - last_write: Instant::now(), - } - } - - async fn write(&mut self, data: &[u8]) -> anyhow::Result<()> { - self.writer.write_all(data).await?; - self.last_write = Instant::now(); - Ok(()) - } -} - -struct UdpWriter { - socket: Arc, - addr: Option, -} - -impl UdpWriter { - fn new(socket: Arc) -> Self { - Self { socket, addr: None } - } - - fn set_addr(&mut self, addr: SocketAddr) { - self.addr = Some(addr); - } - - async fn send_raw(&self, bytes: &[u8]) -> anyhow::Result<()> { - if let Some(addr) = self.addr { - self.socket.send_to(bytes, addr).await?; - - return Ok(()); - } - - return Err(anyhow!("UPD not registered")); - } -} diff --git a/src/rate.rs b/src/rate.rs index 4cc7875..35a3e75 100644 --- a/src/rate.rs +++ b/src/rate.rs @@ -46,6 +46,6 @@ impl AtomicRateLimiter { fn current_millis() -> u64 { SystemTime::now() .duration_since(UNIX_EPOCH) - .unwrap() + .expect("Time went backwards") .as_millis() as u64 } diff --git a/src/state.rs b/src/state.rs index 3d5a932..d53aac8 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,10 +1,14 @@ +use crate::config::Config; use crate::constant::ArcCloseReason; -use crate::packets::{AnyPacket, ConnectionClosedPacket, ConnectionJoinPacket}; +use crate::models::{RoomView, Stats}; +use crate::packet::{ + AnyPacket, AppPacket, ConnectionClosedPacket, ConnectionId, ConnectionIdlingPacket, + ConnectionJoinPacket, RoomId, +}; +use crate::error::AppError; use crate::rate::AtomicRateLimiter; use crate::utils::current_time_millis; -use anyhow::anyhow; use bytes::BytesMut; -use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; use std::net::SocketAddr; use std::sync::{Arc, RwLock, RwLockReadGuard}; @@ -24,45 +28,31 @@ pub enum ConnectionAction { #[derive(Debug, Clone)] pub struct Room { - pub id: String, - pub host_connection_id: i32, + pub id: RoomId, + pub host_connection_id: ConnectionId, pub password: Option, pub created_at: u128, pub updated_at: u128, - pub members: HashMap>, + pub members: HashMap>, pub stats: Stats, pub ping: u128, } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Stats { - pub players: Vec, - #[serde(rename = "mapName")] - pub map_name: String, - pub name: String, - pub gamemode: String, - pub mods: Vec, - pub locale: String, - pub version: String, - #[serde(rename = "createdAt")] - pub created_at: u128, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Player { - pub name: String, - pub locale: String, +#[derive(Clone, Debug)] +pub enum RoomUpdate { + Update { id: RoomId, data: Room }, + Remove(RoomId), } pub struct Rooms { - pub rooms: RwLock>, - pub tx: tokio::sync::broadcast::Sender, + pub rooms: RwLock>, + pub broadcast_sender: tokio::sync::broadcast::Sender, // Keep a receiver to prevent the channel from closing when all clients disconnect - pub _rx: tokio::sync::broadcast::Receiver, + pub _broadcast_receiver: tokio::sync::broadcast::Receiver, } pub struct RoomInit { - pub connection_id: i32, + pub connection_id: ConnectionId, pub password: String, pub stats: Stats, pub sender: mpsc::Sender, @@ -71,15 +61,15 @@ pub struct RoomInit { impl Rooms { pub fn get_sender( &self, - room_id: &str, - connection_id: i32, + room_id: &RoomId, + connection_id: ConnectionId, ) -> Option> { let rooms = self.rooms.read().ok()?; rooms.get(room_id)?.members.get(&connection_id).cloned() } - pub fn find_connection_room_id(&self, connection_id: i32) -> Option { + pub fn find_connection_room_id(&self, connection_id: ConnectionId) -> Option { let rooms = self.rooms.read().ok()?; rooms @@ -88,17 +78,17 @@ impl Rooms { .map(|(id, _)| id.clone()) } - pub fn read(&self) -> Option>> { + pub fn read(&self) -> Option>> { self.rooms.read().ok() } pub fn join( &self, - connection_id: i32, - room_id: &String, + connection_id: ConnectionId, + room_id: &RoomId, sender: mpsc::Sender, - ) -> anyhow::Result<()> { - let mut rooms = self.rooms.write().map_err(|_| anyhow!("Lock poison"))?; + ) -> Result<(), AppError> { + let mut rooms = self.rooms.write().map_err(|_| AppError::LockPoison)?; if let Some(room) = rooms.get_mut(room_id) { room.members.insert(connection_id, sender); @@ -113,12 +103,10 @@ impl Rooms { return Ok(()); } }; - let packet = AnyPacket::App(crate::packets::AppPacket::ConnectionJoin( - ConnectionJoinPacket { - connection_id, - room_id: room_id.clone(), - }, - )); + let packet = AnyPacket::App(AppPacket::ConnectionJoin(ConnectionJoinPacket { + connection_id, + room_id: room_id.clone(), + })); if let Err(e) = sender.try_send(ConnectionAction::SendTCP(packet)) { info!( @@ -127,13 +115,13 @@ impl Rooms { ); } } else { - return Err(anyhow!("Room not found")); + return Err(AppError::RoomNotFound(room_id.to_string())); } Ok(()) } - pub fn leave(&self, connection_id: i32) -> Option { + pub fn leave(&self, connection_id: ConnectionId) -> Option { let mut rooms = self.rooms.write().ok()?; let room_id = rooms @@ -155,12 +143,10 @@ impl Rooms { } }; - let packet = AnyPacket::App(crate::packets::AppPacket::ConnectionClosed( - ConnectionClosedPacket { - connection_id, - reason: ArcCloseReason::Closed, - }, - )); + let packet = AnyPacket::App(AppPacket::ConnectionClosed(ConnectionClosedPacket { + connection_id, + reason: ArcCloseReason::Closed, + })); if let Err(e) = sender.try_send(ConnectionAction::SendTCP(packet)) { info!( @@ -173,7 +159,7 @@ impl Rooms { Some(room_id) } - pub fn create(&self, init: RoomInit) -> String { + pub fn create(&self, init: RoomInit) -> RoomId { let RoomInit { password, connection_id, @@ -187,7 +173,7 @@ impl Rooms { Some(password) }; - let room_id = Uuid::now_v7().to_string(); + let room_id = RoomId(Uuid::now_v7().to_string()); let mut members = HashMap::new(); members.insert(connection_id, sender); @@ -210,7 +196,7 @@ impl Rooms { room_id } - pub fn close(&self, room_id: &String) { + pub fn close(&self, room_id: &RoomId) { let removed = { if let Ok(mut rooms) = self.rooms.write() { rooms.remove(room_id) @@ -222,13 +208,21 @@ impl Rooms { if removed.is_some() { info!("Room closed {}", room_id); - if let Err(err) = self.tx.send(RoomUpdate::Remove(room_id.clone())) { + if let Err(err) = self + .broadcast_sender + .send(RoomUpdate::Remove(room_id.clone())) + { error!("Failed to send remove room event: {}", err); }; } } - pub fn broadcast(&self, room_id: &str, action: ConnectionAction, exclude_id: Option) { + pub fn broadcast( + &self, + room_id: &RoomId, + action: ConnectionAction, + exclude_id: Option, + ) { if let Ok(rooms) = self.rooms.read() { if let Some(room) = rooms.get(room_id) { for (id, sender) in &room.members { @@ -243,7 +237,7 @@ impl Rooms { } } - pub fn forward_to_host(&self, room_id: &str, action: ConnectionAction) { + pub fn forward_to_host(&self, room_id: &RoomId, action: ConnectionAction) { let rooms = match self.rooms.read() { Ok(rooms) => rooms, Err(e) => { @@ -279,7 +273,10 @@ impl Rooms { } } - pub fn get_room_members(&self, room_id: &str) -> Vec<(i32, mpsc::Sender)> { + pub fn get_room_members( + &self, + room_id: &RoomId, + ) -> Vec<(ConnectionId, mpsc::Sender)> { let rooms = match self.rooms.read() { Ok(rooms) => rooms, Err(e) => { @@ -296,7 +293,7 @@ impl Rooms { } } - pub fn idle(&self, room_id: &str, connection_id: i32) { + pub fn idle(&self, room_id: &RoomId, connection_id: ConnectionId) { let rooms = match self.rooms.read() { Ok(rooms) => rooms, Err(_) => return, @@ -315,9 +312,9 @@ impl Rooms { // Send to host if let Some(sender) = room.members.get(&room.host_connection_id) { - let packet = AnyPacket::App(crate::packets::AppPacket::ConnectionIdling( - crate::packets::ConnectionIdlingPacket { connection_id }, - )); + let packet = AnyPacket::App(AppPacket::ConnectionIdling(ConnectionIdlingPacket { + connection_id, + })); if let Err(e) = sender.try_send(ConnectionAction::SendTCP(packet)) { info!( "Failed to forward idle packet to host {}: {}", @@ -329,22 +326,44 @@ impl Rooms { } } +impl From<&Room> for RoomView { + fn from(room: &Room) -> Self { + Self { + name: room.stats.name.clone(), + status: "UP".to_string(), + is_private: false, + is_secured: room.password.is_some(), + players: room.stats.players.clone(), + map_name: room.stats.map_name.clone(), + gamemode: room.stats.gamemode.clone(), + mods: room.stats.mods.clone(), + locale: room.stats.locale.clone(), + version: room.stats.version.clone(), + created_at: room.stats.created_at, + ping: room.ping, + } + } +} + pub struct AppState { + pub config: Config, pub rooms: Rooms, - pub connections: RwLock, Arc)>>, + pub connections: + RwLock, Arc)>>, pub udp_routes: RwLock, Arc)>>, - pub notified_idle: RwLock>, + pub notified_idle: RwLock>, } impl AppState { - pub fn new() -> Self { + pub fn new(config: Config) -> Self { let (tx, rx) = tokio::sync::broadcast::channel(1024); Self { + config, rooms: Rooms { rooms: RwLock::new(HashMap::new()), - tx, - _rx: rx, + broadcast_sender: tx, + _broadcast_receiver: rx, }, connections: RwLock::new(HashMap::new()), udp_routes: RwLock::new(HashMap::new()), @@ -354,7 +373,7 @@ impl AppState { pub fn register_connection( &self, - id: i32, + id: ConnectionId, sender: mpsc::Sender, limiter: Arc, ) { @@ -385,7 +404,7 @@ impl AppState { } } - pub fn get_sender(&self, id: i32) -> Option> { + pub fn get_sender(&self, id: ConnectionId) -> Option> { self.connections .read() .ok()? @@ -400,7 +419,7 @@ impl AppState { self.udp_routes.read().ok()?.get(addr).cloned() } - pub fn idle(&self, connection_id: i32) { + pub fn idle(&self, connection_id: ConnectionId) { // Only process valid connections if let Ok(conns) = self.connections.read() { if !conns.contains_key(&connection_id) { @@ -427,13 +446,13 @@ impl AppState { } } - pub fn reset_idle(&self, connection_id: i32) { + pub fn reset_idle(&self, connection_id: ConnectionId) { if let Ok(mut idle_set) = self.notified_idle.write() { idle_set.remove(&connection_id); } } - pub fn remove_connection(&self, connection_id: i32) { + pub fn remove_connection(&self, connection_id: ConnectionId) { if let Ok(mut conns) = self.connections.write() { conns.remove(&connection_id); } @@ -482,61 +501,3 @@ impl AppState { } } } - -#[derive(Clone, Debug)] -pub enum RoomUpdate { - Update { id: String, data: Room }, - Remove(String), -} - -#[derive(Clone, Debug, Serialize)] -pub struct RemoveRemoveEvent { - #[serde(rename = "roomId")] - pub room_id: String, -} - -#[derive(Clone, Debug, Serialize)] -pub struct RoomUpdateEvent { - #[serde(rename = "roomId")] - pub room_id: String, - pub data: RoomView, -} - -#[derive(Clone, Debug, Serialize)] -pub struct RoomView { - pub name: String, - pub status: String, - #[serde(rename = "isPrivate")] - pub is_private: bool, - #[serde(rename = "isSecured")] - pub is_secured: bool, - pub players: Vec, - #[serde(rename = "mapName")] - pub map_name: String, - pub gamemode: String, - pub mods: Vec, - pub locale: String, - pub version: String, - #[serde(rename = "createdAt")] - pub created_at: u128, - pub ping: u128, -} - -impl RoomView { - pub fn from(room: &Room) -> Self { - Self { - name: room.stats.name.clone(), - status: "UP".to_string(), - is_private: false, - is_secured: room.password.is_some(), - players: room.stats.players.clone(), - map_name: room.stats.map_name.clone(), - gamemode: room.stats.gamemode.clone(), - mods: room.stats.mods.clone(), - locale: room.stats.locale.clone(), - version: room.stats.version.clone(), - created_at: room.stats.created_at, - ping: room.ping, - } - } -} diff --git a/src/writer.rs b/src/writer.rs new file mode 100644 index 0000000..bfa03be --- /dev/null +++ b/src/writer.rs @@ -0,0 +1,51 @@ +use anyhow::anyhow; +use std::net::SocketAddr; +use std::sync::Arc; +use std::time::Instant; +use tokio::io::AsyncWriteExt; +use tokio::net::UdpSocket; + +pub struct TcpWriter { + writer: tokio::net::tcp::OwnedWriteHalf, + pub last_write: Instant, +} + +impl TcpWriter { + pub fn new(writer: tokio::net::tcp::OwnedWriteHalf) -> Self { + Self { + writer, + last_write: Instant::now(), + } + } + + pub async fn write(&mut self, data: &[u8]) -> anyhow::Result<()> { + self.writer.write_all(data).await?; + self.last_write = Instant::now(); + Ok(()) + } +} + +pub struct UdpWriter { + socket: Arc, + pub addr: Option, +} + +impl UdpWriter { + pub fn new(socket: Arc) -> Self { + Self { socket, addr: None } + } + + pub fn set_addr(&mut self, addr: SocketAddr) { + self.addr = Some(addr); + } + + pub async fn send_raw(&self, bytes: &[u8]) -> anyhow::Result<()> { + if let Some(addr) = self.addr { + self.socket.send_to(bytes, addr).await?; + + return Ok(()); + } + + return Err(anyhow!("UPD not registered")); + } +} From 5cd24a3a56d88aa0ca8f6ea2ac115138187fb6a9 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Tue, 20 Jan 2026 15:32:12 +0700 Subject: [PATCH 059/115] refactor(server): rename http_server to http and proxy_server to proxy The change simplifies module names while maintaining the same functionality. Both server implementations were moved to more concise filenames without any behavioral modifications. --- src/{http_server.rs => http.rs} | 0 src/main.rs | 8 ++++---- src/{proxy_server.rs => proxy.rs} | 0 3 files changed, 4 insertions(+), 4 deletions(-) rename src/{http_server.rs => http.rs} (100%) rename src/{proxy_server.rs => proxy.rs} (100%) diff --git a/src/http_server.rs b/src/http.rs similarity index 100% rename from src/http_server.rs rename to src/http.rs diff --git a/src/main.rs b/src/main.rs index fb242f3..6b1c08a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,10 +2,10 @@ pub mod config; pub mod connection; pub mod constant; pub mod error; -pub mod http_server; +pub mod http; pub mod models; pub mod packet; -pub mod proxy_server; +pub mod proxy; pub mod rate; pub mod state; pub mod utils; @@ -38,7 +38,7 @@ async fn main() -> anyhow::Result<()> { let proxy_state = state.clone(); let proxy_port = config.player_connect_port; tokio::spawn(async move { - if let Err(e) = proxy_server::run(proxy_state, proxy_port).await { + if let Err(e) = proxy::run(proxy_state, proxy_port).await { tracing::error!("Proxy server error: {}", e); } }); @@ -46,7 +46,7 @@ async fn main() -> anyhow::Result<()> { // Start HTTP Server let http_state = state.clone(); let http_port = config.player_connect_http_port; - http_server::run(http_state, http_port).await?; + http::run(http_state, http_port).await?; Ok(()) } diff --git a/src/proxy_server.rs b/src/proxy.rs similarity index 100% rename from src/proxy_server.rs rename to src/proxy.rs From 745f6245821a5d8fd05f9f8e49f9cb307b5c202c Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Tue, 20 Jan 2026 17:07:11 +0700 Subject: [PATCH 060/115] refactor(logging): reduce info logs and promote important warnings Replace various info-level logs with warn-level for important failure cases to make them more visible. Remove redundant info logs for packet handling and connection actions to reduce log noise. --- src/connection.rs | 25 +++++++++---------------- src/proxy.rs | 5 ++--- src/state.rs | 8 ++++---- 3 files changed, 15 insertions(+), 23 deletions(-) diff --git a/src/connection.rs b/src/connection.rs index e93ec25..6be5ab5 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -142,8 +142,6 @@ impl ConnectionActor { let is_framework = matches!(packet, AnyPacket::Framework(_)); if !is_framework { - info!("Received TCP packet: {:?} from {}", packet, self.id); - let room_id_opt = self.state.rooms.find_connection_room_id(self.id); let is_host = if let Some(ref room_id) = room_id_opt { if let Some(rooms) = self.state.rooms.read() { @@ -203,7 +201,6 @@ impl ConnectionActor { } else { if self.packet_queue.len() < 16 { self.packet_queue.push(bytes); - info!("Queued raw packet for connection {}", self.id); } else { warn!( "Connection {} packet queue full, dropping raw packet", @@ -259,7 +256,7 @@ impl ConnectionActor { data: room.clone(), }) { - info!("Fail to broadcast room update {}", err); + warn!("Fail to broadcast room update {}", err); } } } @@ -311,7 +308,7 @@ impl ConnectionActor { .unwrap_or((false, false)); if wrong_password { - info!( + warn!( "Connection {} tried to join room {} with wrong password.", self.id, p.room_id ); @@ -323,7 +320,7 @@ impl ConnectionActor { } if !can_join { - info!( + warn!( "Connection {} tried to join a non-existent room {}.", self.id, p.room_id ); @@ -396,7 +393,7 @@ impl ConnectionActor { info!("Fail to broadcast room update {}", err); } - info!("Room {} created by connection {}.", room_id, self.id); + warn!("Room {} created by connection {}.", room_id, self.id); } } AppPacket::RoomClosureRequest(_) => { @@ -430,10 +427,10 @@ impl ConnectionActor { reason: CloseReason::Closed, })), )) { - info!("Failed to send room closed packet to {}: {}", id, e); + warn!("Failed to send room closed packet to {}: {}", id, e); } if let Err(e) = sender.try_send(ConnectionAction::Close) { - info!("Failed to send close action to {}: {}", id, e); + warn!("Failed to send close action to {}: {}", id, e); } } } @@ -481,13 +478,13 @@ impl ConnectionActor { }), ))) { - info!( + warn!( "Failed to send connection closed packet to {}: {}", p.connection_id, e ); } if let Err(e) = sender.try_send(ConnectionAction::Close) { - info!("Failed to send close action to {}: {}", p.connection_id, e); + warn!("Failed to send close action to {}: {}", p.connection_id, e); } } else { warn!("Connection {} (room {}) tried to close a connection from another room.", self.id, room_id); @@ -540,7 +537,7 @@ impl ConnectionActor { } } } else { - info!("No room found for connection {}", self.id); + warn!("No room found for connection {}", self.id); } } _ => { @@ -558,20 +555,16 @@ impl ConnectionActor { match action { ConnectionAction::SendTCP(p) => { let bytes = p.to_bytes(); - info!("Send packet: {:?} to {}", p, self.id); batch.extend_from_slice(&ConnectionActor::prepend_len(bytes)); } ConnectionAction::SendTCPRaw(b) => { - info!("Send tcp {} bytes to {}", b.len(), self.id); batch.extend_from_slice(&ConnectionActor::prepend_len(b)); } ConnectionAction::SendUDPRaw(b) => { - info!("Send udp {} bytes to {}", b.len(), self.id); self.udp_writer.send_raw(&b).await?; } ConnectionAction::Close => { // Return error to break loop - info!("Close connection {}", self.id); return Err(anyhow::anyhow!("Closed")); } ConnectionAction::RegisterUDP(addr) => { diff --git a/src/proxy.rs b/src/proxy.rs index a237aaf..d9adaf2 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -53,13 +53,12 @@ fn spawn_udp_listener(state: Arc, socket: Arc) { handle_register_udp(&state, connection_id, addr).await; } else { // Normal packet, route it - info!("Received UDP packet: {:?} from {:?}", packet, addr); - + if let Some((sender, _)) = state.get_route(&addr) { if let Err(e) = sender .try_send(ConnectionAction::ProcessPacket(packet, false)) { - info!("Failed to forward UDP packet: {}", e); + warn!("Failed to forward UDP packet: {}", e); } } else { // Unknown UDP sender, ignore diff --git a/src/state.rs b/src/state.rs index d53aac8..987db62 100644 --- a/src/state.rs +++ b/src/state.rs @@ -230,7 +230,7 @@ impl Rooms { continue; } if let Err(e) = sender.try_send(action.clone()) { - info!("Failed to broadcast to {}: {}", id, e); + warn!("Failed to broadcast to {}: {}", id, e); } } } @@ -266,7 +266,7 @@ impl Rooms { }; if let Err(e) = sender.try_send(action) { - info!( + warn!( "Failed to forward to host {}: {}", room.host_connection_id, e ); @@ -316,7 +316,7 @@ impl Rooms { connection_id, })); if let Err(e) = sender.try_send(ConnectionAction::SendTCP(packet)) { - info!( + warn!( "Failed to forward idle packet to host {}: {}", room.host_connection_id, e ); @@ -492,7 +492,7 @@ impl AppState { for sender in members { if let Err(e) = sender.try_send(ConnectionAction::Close) { - info!("Failed to send close action to member: {}", e); + warn!("Failed to send close action to member: {}", e); } } From e49a0deb36d41ce505a28259d369698838438da9 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Tue, 20 Jan 2026 17:24:46 +0700 Subject: [PATCH 061/115] feat(connection): use random connection IDs instead of sequential Replace sequential connection ID generation with random IDs to prevent potential conflicts and improve security. Also added the rand dependency and a helper method to check for existing connection IDs. --- Cargo.lock | 1 + Cargo.toml | 1 + src/proxy.rs | 13 +++++++------ src/state.rs | 10 +++++++++- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2d9b3ad..ae8ff26 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -689,6 +689,7 @@ dependencies = [ "dashmap", "dotenvy", "futures", + "rand", "serde", "serde_json", "serde_repr", diff --git a/Cargo.toml b/Cargo.toml index ca12559..6e6c3eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,3 +29,4 @@ tower-http = { version = "0.5", features = ["cors", "trace"] } futures = "0.3" thiserror = "1.0" anyhow = "1.0" +rand = "0.9.2" diff --git a/src/proxy.rs b/src/proxy.rs index d9adaf2..d82c5cb 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -6,7 +6,6 @@ use crate::writer::{TcpWriter, UdpWriter}; use bytes::Bytes; use std::io::Cursor; use std::net::SocketAddr; -use std::sync::atomic::{AtomicI32, Ordering}; use std::sync::Arc; use std::time::{Duration, Instant}; use tokio::net::{TcpListener, UdpSocket}; @@ -14,9 +13,6 @@ use tokio::sync::mpsc; use tracing::{error, info, warn}; use crate::packet::ConnectionId; - -static NEXT_CONNECTION_ID: AtomicI32 = AtomicI32::new(1); - const UDP_BUFFER_SIZE: usize = 4096; const CHANNEL_CAPACITY: usize = 100; const PACKET_RATE_LIMIT_WINDOW: Duration = Duration::from_millis(3000); @@ -53,7 +49,7 @@ fn spawn_udp_listener(state: Arc, socket: Arc) { handle_register_udp(&state, connection_id, addr).await; } else { // Normal packet, route it - + if let Some((sender, _)) = state.get_route(&addr) { if let Err(e) = sender .try_send(ConnectionAction::ProcessPacket(packet, false)) @@ -103,7 +99,12 @@ async fn accept_tcp_connection( if let Err(e) = socket.set_nodelay(true) { warn!("Failed to set nodelay for connection: {}", e); } - let id = ConnectionId(NEXT_CONNECTION_ID.fetch_add(1, Ordering::Relaxed)); + let id = loop { + let id = ConnectionId(rand::random()); + if !state.has_connection_id(id) { + break id; + } + }; let (tx, rx) = mpsc::channel(CHANNEL_CAPACITY); let limiter = Arc::new(AtomicRateLimiter::new( diff --git a/src/state.rs b/src/state.rs index 987db62..6bb6a13 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,11 +1,11 @@ use crate::config::Config; use crate::constant::ArcCloseReason; +use crate::error::AppError; use crate::models::{RoomView, Stats}; use crate::packet::{ AnyPacket, AppPacket, ConnectionClosedPacket, ConnectionId, ConnectionIdlingPacket, ConnectionJoinPacket, RoomId, }; -use crate::error::AppError; use crate::rate::AtomicRateLimiter; use crate::utils::current_time_millis; use bytes::BytesMut; @@ -387,6 +387,14 @@ impl AppState { } } + pub fn has_connection_id(&self, id: ConnectionId) -> bool { + self.connections + .read() + .ok() + .map(|conns| conns.contains_key(&id)) + .unwrap_or(false) + } + pub fn register_udp( &self, addr: SocketAddr, From 6bb484cc349705a46455da3a4f8a0c679b25558e Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Tue, 20 Jan 2026 18:34:05 +0700 Subject: [PATCH 062/115] refactor(packet): change connection IDs from i32 to u32 The change from signed to unsigned integers for connection IDs ensures these values are always positive and better represents their usage as identifiers. This affects the ConnectionId struct and all related packet handling code. --- src/packet.rs | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/packet.rs b/src/packet.rs index 78e3a82..2993b62 100644 --- a/src/packet.rs +++ b/src/packet.rs @@ -9,7 +9,7 @@ pub const APP_PACKET_ID: i8 = -4; pub const FRAMEWORK_PACKET_ID: i8 = -2; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct ConnectionId(pub i32); +pub struct ConnectionId(pub u32); impl std::fmt::Display for ConnectionId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -35,7 +35,7 @@ pub enum AnyPacket { #[derive(Debug, Clone, Copy)] pub enum FrameworkMessage { - Ping { id: i32, is_reply: bool }, + Ping { id: u32, is_reply: bool }, DiscoverHost, KeepAlive, RegisterUDP { connection_id: ConnectionId }, @@ -178,16 +178,16 @@ impl FrameworkMessage { match fid { 0 => Ok(FrameworkMessage::Ping { - id: buf.get_i32(), + id: buf.get_u32(), is_reply: buf.get_u8() != 0, }), 1 => Ok(FrameworkMessage::DiscoverHost), 2 => Ok(FrameworkMessage::KeepAlive), 3 => Ok(FrameworkMessage::RegisterUDP { - connection_id: ConnectionId(buf.get_i32()), + connection_id: ConnectionId(buf.get_u32()), }), 4 => Ok(FrameworkMessage::RegisterTCP { - connection_id: ConnectionId(buf.get_i32()), + connection_id: ConnectionId(buf.get_u32()), }), _ => Err(AppError::PacketParsing(format!( "Unknown Framework ID: {}", @@ -202,18 +202,18 @@ impl FrameworkMessage { match self { FrameworkMessage::Ping { id, is_reply } => { buf.put_u8(0); - buf.put_i32(*id); + buf.put_u32(*id); buf.put_u8(if *is_reply { 1 } else { 0 }); } FrameworkMessage::DiscoverHost => buf.put_u8(1), FrameworkMessage::KeepAlive => buf.put_u8(2), FrameworkMessage::RegisterUDP { connection_id } => { buf.put_u8(3); - buf.put_i32(connection_id.0); + buf.put_u32(connection_id.0); } FrameworkMessage::RegisterTCP { connection_id } => { buf.put_u8(4); - buf.put_i32(connection_id.0); + buf.put_u32(connection_id.0); } } } @@ -225,7 +225,7 @@ impl AppPacket { match pid { 0 => { - let connection_id = ConnectionId(buf.get_i32()); + let connection_id = ConnectionId(buf.get_u32()); let is_tcp = buf.get_u8() != 0; let start = buf.position() as usize; @@ -243,15 +243,15 @@ impl AppPacket { )) } 1 => Ok(AppPacket::ConnectionClosed(ConnectionClosedPacket { - connection_id: ConnectionId(buf.get_i32()), + connection_id: ConnectionId(buf.get_u32()), reason: ArcCloseReason::try_from(buf.get_u8())?, })), 2 => Ok(AppPacket::ConnectionJoin(ConnectionJoinPacket { - connection_id: ConnectionId(buf.get_i32()), + connection_id: ConnectionId(buf.get_u32()), room_id: RoomId(read_string(buf)?), })), 3 => Ok(AppPacket::ConnectionIdling(ConnectionIdlingPacket { - connection_id: ConnectionId(buf.get_i32()), + connection_id: ConnectionId(buf.get_u32()), })), 4 => Ok(AppPacket::RoomCreationRequest(RoomCreationRequestPacket { version: read_string(buf)?, @@ -294,23 +294,23 @@ impl AppPacket { match self { AppPacket::ConnectionPacketWrap(p) => { buf.put_u8(0); - buf.put_i32(p.connection_id.0); + buf.put_u32(p.connection_id.0); buf.put_u8(if p.is_tcp { 1 } else { 0 }); buf.extend_from_slice(&p.buffer); } AppPacket::ConnectionClosed(p) => { buf.put_u8(1); - buf.put_i32(p.connection_id.0); + buf.put_u32(p.connection_id.0); buf.put_u8(p.reason as u8); } AppPacket::ConnectionJoin(p) => { buf.put_u8(2); - buf.put_i32(p.connection_id.0); + buf.put_u32(p.connection_id.0); write_string(buf, &p.room_id.0); } AppPacket::ConnectionIdling(p) => { buf.put_u8(3); - buf.put_i32(p.connection_id.0); + buf.put_u32(p.connection_id.0); } AppPacket::RoomCreationRequest(_) => { panic!("Client only") From 1c87d708b23a56f1d3eb8750fc3c672f8ac08a6e Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Wed, 21 Jan 2026 22:55:00 +0700 Subject: [PATCH 063/115] refactor(networking): improve packet handling and connection throughput - Replace BytesMut with Bytes for more efficient memory usage in packet handling - Increase channel capacity and packet rate limit for better performance - Add multiple UDP listeners to improve throughput - Simplify packet serialization by using Bytes directly --- src/connection.rs | 10 +++++----- src/packet.rs | 10 +++++----- src/proxy.rs | 10 +++++++--- src/state.rs | 6 +++--- 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/connection.rs b/src/connection.rs index 6be5ab5..bb5e6e1 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -8,7 +8,7 @@ use crate::state::{AppState, ConnectionAction, RoomInit, RoomUpdate}; use crate::utils::current_time_millis; use crate::writer::{TcpWriter, UdpWriter}; use anyhow::anyhow; -use bytes::{Buf, BufMut, BytesMut}; +use bytes::{Buf, BufMut, Bytes, BytesMut}; use std::io::Cursor; use std::sync::Arc; use std::time::{Duration, Instant}; @@ -31,7 +31,7 @@ pub struct ConnectionActor { pub udp_writer: UdpWriter, pub limiter: Arc, pub last_read: Instant, - pub packet_queue: Vec, + pub packet_queue: Vec, } impl ConnectionActor { @@ -555,7 +555,7 @@ impl ConnectionActor { match action { ConnectionAction::SendTCP(p) => { let bytes = p.to_bytes(); - batch.extend_from_slice(&ConnectionActor::prepend_len(bytes)); + batch.extend_from_slice(&ConnectionActor::prepend_len(bytes.freeze())); } ConnectionAction::SendTCPRaw(b) => { batch.extend_from_slice(&ConnectionActor::prepend_len(b)); @@ -599,7 +599,7 @@ impl ConnectionActor { Ok(()) } - pub fn prepend_len(payload: BytesMut) -> BytesMut { + pub fn prepend_len(payload: Bytes) -> BytesMut { let mut out: BytesMut = BytesMut::with_capacity(2 + payload.len()); out.put_u16(payload.len() as u16); @@ -610,7 +610,7 @@ impl ConnectionActor { async fn write_packet(&mut self, packet: AnyPacket) -> anyhow::Result<()> { self.tcp_writer - .write(&ConnectionActor::prepend_len(packet.to_bytes())) + .write(&ConnectionActor::prepend_len(packet.to_bytes().freeze())) .await } } diff --git a/src/packet.rs b/src/packet.rs index 2993b62..1860faf 100644 --- a/src/packet.rs +++ b/src/packet.rs @@ -1,6 +1,6 @@ use crate::constant::{ArcCloseReason, CloseReason, MessageType}; -use crate::models::Stats; use crate::error::AppError; +use crate::models::Stats; use bytes::{Buf, BufMut, Bytes, BytesMut}; use std::convert::TryFrom; use std::io::Cursor; @@ -30,7 +30,7 @@ impl std::fmt::Display for RoomId { pub enum AnyPacket { Framework(FrameworkMessage), App(AppPacket), - Raw(BytesMut), + Raw(Bytes), } #[derive(Debug, Clone, Copy)] @@ -63,7 +63,7 @@ pub enum AppPacket { pub struct ConnectionPacketWrapPacket { pub connection_id: ConnectionId, pub is_tcp: bool, - pub buffer: BytesMut, + pub buffer: Bytes, } #[derive(Debug, Clone)] @@ -149,7 +149,7 @@ impl AnyPacket { let bytes = buf.get_ref().slice(start..end); buf.advance(remaining); - Ok(AnyPacket::Raw(BytesMut::from(bytes))) + Ok(AnyPacket::Raw(bytes)) } } } @@ -238,7 +238,7 @@ impl AppPacket { ConnectionPacketWrapPacket { connection_id, is_tcp, - buffer: BytesMut::from(buffer), + buffer, }, )) } diff --git a/src/proxy.rs b/src/proxy.rs index d82c5cb..e117d04 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -14,9 +14,9 @@ use tracing::{error, info, warn}; use crate::packet::ConnectionId; const UDP_BUFFER_SIZE: usize = 4096; -const CHANNEL_CAPACITY: usize = 100; +const CHANNEL_CAPACITY: usize = 1024; const PACKET_RATE_LIMIT_WINDOW: Duration = Duration::from_millis(3000); -const PACKET_RATE_LIMIT: u32 = 300; +const PACKET_RATE_LIMIT: u32 = 1000; pub async fn run(state: Arc, port: u16) -> anyhow::Result<()> { let address = format!("0.0.0.0:{}", port); @@ -25,7 +25,11 @@ pub async fn run(state: Arc, port: u16) -> anyhow::Result<()> { info!("Proxy Server listening on TCP/UDP {}", port); - spawn_udp_listener(state.clone(), udp_socket.clone()); + // Spawn multiple UDP listeners for better throughput + for _ in 0..4 { + spawn_udp_listener(state.clone(), udp_socket.clone()); + } + accept_tcp_connection(state, tcp_listener, udp_socket).await } diff --git a/src/state.rs b/src/state.rs index 6bb6a13..a1d0c3e 100644 --- a/src/state.rs +++ b/src/state.rs @@ -8,7 +8,7 @@ use crate::packet::{ }; use crate::rate::AtomicRateLimiter; use crate::utils::current_time_millis; -use bytes::BytesMut; +use bytes::Bytes; use std::collections::{HashMap, HashSet}; use std::net::SocketAddr; use std::sync::{Arc, RwLock, RwLockReadGuard}; @@ -19,8 +19,8 @@ use uuid::Uuid; #[derive(Debug, Clone)] pub enum ConnectionAction { SendTCP(AnyPacket), - SendTCPRaw(BytesMut), - SendUDPRaw(BytesMut), + SendTCPRaw(Bytes), + SendUDPRaw(Bytes), Close, RegisterUDP(SocketAddr), ProcessPacket(AnyPacket, bool), From 8d1484306aace92c3ed2d33f1f912efe377515aa Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Wed, 21 Jan 2026 23:21:40 +0700 Subject: [PATCH 064/115] perf: add performance monitoring for network operations Add timing measurements and logging for critical network operations including: - Room member broadcasting - TCP read/write operations - Channel message processing - Packet writing Log warnings when operations exceed threshold durations to help identify performance bottlenecks --- src/connection.rs | 61 ++++++++++++++++++++++++++++++++++------------- src/state.rs | 9 +++++++ 2 files changed, 53 insertions(+), 17 deletions(-) diff --git a/src/connection.rs b/src/connection.rs index bb5e6e1..0735051 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -50,39 +50,58 @@ impl ConnectionActor { let mut batch = BytesMut::new(); tokio::select! { - // TCP Read - read_result = reader.read(&mut tmp_buf) => { - match read_result { - Ok(0) => break, // EOF - Ok(n) => { - self.last_read = Instant::now(); - self.state.reset_idle(self.id); - - buf.extend_from_slice(&tmp_buf[..n]); - self.process_tcp_buffer(&mut buf).await?; - } - Err(e) => return Err(e.into()), - } - } - // Channel Read action = self.rx.recv() => { if let Some(action) = action { + let start = Instant::now(); + // info!("Connection {} received action from channel: {:?}", self.id, action); self.handle_action(action, &mut batch).await?; while let Ok(action) = self.rx.try_recv() { + // info!("Connection {} received extra action from channel: {:?}", self.id, action); self.handle_action(action, &mut batch).await?; } // Flush batch if !batch.is_empty() { + let batch_len = batch.len(); self.tcp_writer.write(&batch).await?; + info!("Connection {} flushed {} bytes to TCP", self.id, batch_len); + } + let elapsed = start.elapsed(); + if elapsed > Duration::from_millis(100) { + warn!("Connection {} channel batch processing took {:?}", self.id, elapsed); } } else { // Channel closed + info!("Connection {} channel closed", self.id); break; } } + // TCP Read + read_result = reader.read(&mut tmp_buf) => { + match read_result { + Ok(0) => break, // EOF + Ok(n) => { + self.last_read = Instant::now(); + self.state.reset_idle(self.id); + + info!("Connection {} received {} bytes from TCP", self.id, n); + + buf.extend_from_slice(&tmp_buf[..n]); + + let start = Instant::now(); + self.process_tcp_buffer(&mut buf).await?; + let elapsed = start.elapsed(); + if elapsed > Duration::from_millis(100) { + warn!("Connection {} process_tcp_buffer took {:?}", self.id, elapsed); + } + } + Err(e) => return Err(e.into()), + } + } + + // Tick _ = tick_interval.tick() => { if self.last_read.elapsed() > Duration::from_millis(CONNECTION_TIME_OUT_MS) { @@ -609,8 +628,16 @@ impl ConnectionActor { } async fn write_packet(&mut self, packet: AnyPacket) -> anyhow::Result<()> { - self.tcp_writer + let start = Instant::now(); + let res = self + .tcp_writer .write(&ConnectionActor::prepend_len(packet.to_bytes().freeze())) - .await + .await; + + let elapsed = start.elapsed(); + if elapsed > Duration::from_millis(100) { + warn!("Connection {} write_packet took {:?}", self.id, elapsed); + } + res } } diff --git a/src/state.rs b/src/state.rs index a1d0c3e..b122bdb 100644 --- a/src/state.rs +++ b/src/state.rs @@ -225,14 +225,23 @@ impl Rooms { ) { if let Ok(rooms) = self.rooms.read() { if let Some(room) = rooms.get(room_id) { + // info!("Broadcasting to room {} (members: {})", room_id, room.members.len()); + let start = std::time::Instant::now(); + let mut count = 0; for (id, sender) in &room.members { if Some(*id) == exclude_id { continue; } if let Err(e) = sender.try_send(action.clone()) { warn!("Failed to broadcast to {}: {}", id, e); + } else { + count += 1; } } + let elapsed = start.elapsed(); + if elapsed > std::time::Duration::from_millis(10) { + info!("Broadcast to {} members in room {} took {:?}", count, room_id, elapsed); + } } } } From dbe61d9c449e286d4c03fb32925ace10afd7795c Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Wed, 21 Jan 2026 23:32:21 +0700 Subject: [PATCH 065/115] feat(logging): add selective packet logging and improve broadcast timing Add debug logging for TCP packets while filtering out noisy data packets Improve broadcast timing log format and add comment for future debugging --- src/connection.rs | 6 ++++++ src/state.rs | 6 +++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/connection.rs b/src/connection.rs index 0735051..458a057 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -571,8 +571,14 @@ impl ConnectionActor { action: ConnectionAction, batch: &mut BytesMut, ) -> anyhow::Result<()> { + // info!("Connection {} handling action: {:?}", self.id, action); match action { ConnectionAction::SendTCP(p) => { + if let AnyPacket::App(AppPacket::ConnectionPacketWrap(_)) = &p { + // Reduce noise for data packets + } else { + info!("Connection {} sending TCP packet: {:?}", self.id, p); + } let bytes = p.to_bytes(); batch.extend_from_slice(&ConnectionActor::prepend_len(bytes.freeze())); } diff --git a/src/state.rs b/src/state.rs index b122bdb..d37f4c6 100644 --- a/src/state.rs +++ b/src/state.rs @@ -226,6 +226,7 @@ impl Rooms { if let Ok(rooms) = self.rooms.read() { if let Some(room) = rooms.get(room_id) { // info!("Broadcasting to room {} (members: {})", room_id, room.members.len()); + // info!("Broadcasting action: {:?}", action); let start = std::time::Instant::now(); let mut count = 0; for (id, sender) in &room.members { @@ -240,7 +241,10 @@ impl Rooms { } let elapsed = start.elapsed(); if elapsed > std::time::Duration::from_millis(10) { - info!("Broadcast to {} members in room {} took {:?}", count, room_id, elapsed); + info!( + "Broadcast to {} members in room {} took {:?}", + count, room_id, elapsed + ); } } } From df9e7e9ab7f5e00c6d78b716de60ccf9297326f3 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Wed, 21 Jan 2026 23:42:06 +0700 Subject: [PATCH 066/115] fix: reduce idle timeout from 5000ms to 1000ms to improve connection cleanup --- src/connection.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/connection.rs b/src/connection.rs index 458a057..33d7900 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -18,7 +18,7 @@ use tracing::{error, info, warn}; const TCP_BUFFER_SIZE: usize = 32768; const CONNECTION_TIME_OUT_MS: u64 = 30000; -const IDLE_TIMEOUT_MS: u64 = 5000; +const IDLE_TIMEOUT_MS: u64 = 1000; const KEEP_ALIVE_INTERVAL_MS: u64 = 2000; const PACKET_LENGTH_LENGTH: usize = 2; const TICK_INTERVAL_SECS: u64 = 1; From 9f75266d97e01ac46cfa35d32430b76bdaa78b0e Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Wed, 21 Jan 2026 23:56:34 +0700 Subject: [PATCH 067/115] perf: remove excessive logging and timing measurements The changes remove redundant debug logging and performance timing measurements that were cluttering the code. This improves code cleanliness and reduces overhead from unnecessary instrumentation, particularly in hot paths like message broadcasting and TCP operations. --- src/connection.rs | 65 +++++++++++++++-------------------------------- src/state.rs | 13 ---------- 2 files changed, 21 insertions(+), 57 deletions(-) diff --git a/src/connection.rs b/src/connection.rs index 33d7900..8e36edc 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -18,7 +18,6 @@ use tracing::{error, info, warn}; const TCP_BUFFER_SIZE: usize = 32768; const CONNECTION_TIME_OUT_MS: u64 = 30000; -const IDLE_TIMEOUT_MS: u64 = 1000; const KEEP_ALIVE_INTERVAL_MS: u64 = 2000; const PACKET_LENGTH_LENGTH: usize = 2; const TICK_INTERVAL_SECS: u64 = 1; @@ -50,66 +49,48 @@ impl ConnectionActor { let mut batch = BytesMut::new(); tokio::select! { - // Channel Read + action = self.rx.recv() => { if let Some(action) = action { - let start = Instant::now(); - // info!("Connection {} received action from channel: {:?}", self.id, action); self.handle_action(action, &mut batch).await?; while let Ok(action) = self.rx.try_recv() { - // info!("Connection {} received extra action from channel: {:?}", self.id, action); self.handle_action(action, &mut batch).await?; } - // Flush batch + if !batch.is_empty() { - let batch_len = batch.len(); self.tcp_writer.write(&batch).await?; - info!("Connection {} flushed {} bytes to TCP", self.id, batch_len); - } - let elapsed = start.elapsed(); - if elapsed > Duration::from_millis(100) { - warn!("Connection {} channel batch processing took {:?}", self.id, elapsed); } } else { - // Channel closed - info!("Connection {} channel closed", self.id); + break; } } - // TCP Read + read_result = reader.read(&mut tmp_buf) => { match read_result { - Ok(0) => break, // EOF + Ok(0) => break, Ok(n) => { self.last_read = Instant::now(); self.state.reset_idle(self.id); - info!("Connection {} received {} bytes from TCP", self.id, n); - buf.extend_from_slice(&tmp_buf[..n]); - - let start = Instant::now(); self.process_tcp_buffer(&mut buf).await?; - let elapsed = start.elapsed(); - if elapsed > Duration::from_millis(100) { - warn!("Connection {} process_tcp_buffer took {:?}", self.id, elapsed); - } } Err(e) => return Err(e.into()), } } - // Tick + _ = tick_interval.tick() => { if self.last_read.elapsed() > Duration::from_millis(CONNECTION_TIME_OUT_MS) { info!("Connection {} timed out", self.id); break; } - if self.last_read.elapsed() > Duration::from_millis(IDLE_TIMEOUT_MS) { + if self.is_idle() { self.state.idle(self.id); } @@ -152,7 +133,7 @@ impl ConnectionActor { continue; } } - // Handle packet + } Ok(()) } @@ -244,11 +225,11 @@ impl ConnectionActor { } } FrameworkMessage::KeepAlive => { - // Handled by activity update + } FrameworkMessage::RegisterUDP { .. } => { - // Should not happen via TCP? - // But if it does, ignore? + + } _ => { warn!("Unhandled Framework Packet: {:?}", packet); @@ -571,11 +552,11 @@ impl ConnectionActor { action: ConnectionAction, batch: &mut BytesMut, ) -> anyhow::Result<()> { - // info!("Connection {} handling action: {:?}", self.id, action); + match action { ConnectionAction::SendTCP(p) => { if let AnyPacket::App(AppPacket::ConnectionPacketWrap(_)) = &p { - // Reduce noise for data packets + } else { info!("Connection {} sending TCP packet: {:?}", self.id, p); } @@ -589,7 +570,7 @@ impl ConnectionActor { self.udp_writer.send_raw(&b).await?; } ConnectionAction::Close => { - // Return error to break loop + return Err(anyhow::anyhow!("Closed")); } ConnectionAction::RegisterUDP(addr) => { @@ -601,7 +582,7 @@ impl ConnectionActor { info!("New connection {} from {}", self.id, addr); - // Register in state + if let Some(sender) = self.state.get_sender(self.id) { self.state.register_udp(addr, sender, self.limiter.clone()); } else { @@ -610,7 +591,7 @@ impl ConnectionActor { self.id )); } - // Send reply + self.write_packet(AnyPacket::Framework(FrameworkMessage::RegisterUDP { connection_id: self.id, })) @@ -634,16 +615,12 @@ impl ConnectionActor { } async fn write_packet(&mut self, packet: AnyPacket) -> anyhow::Result<()> { - let start = Instant::now(); - let res = self - .tcp_writer + self.tcp_writer .write(&ConnectionActor::prepend_len(packet.to_bytes().freeze())) - .await; + .await + } - let elapsed = start.elapsed(); - if elapsed > Duration::from_millis(100) { - warn!("Connection {} write_packet took {:?}", self.id, elapsed); - } - res + fn is_idle(&self) -> bool { + self.packet_queue.is_empty() } } diff --git a/src/state.rs b/src/state.rs index d37f4c6..a1d0c3e 100644 --- a/src/state.rs +++ b/src/state.rs @@ -225,27 +225,14 @@ impl Rooms { ) { if let Ok(rooms) = self.rooms.read() { if let Some(room) = rooms.get(room_id) { - // info!("Broadcasting to room {} (members: {})", room_id, room.members.len()); - // info!("Broadcasting action: {:?}", action); - let start = std::time::Instant::now(); - let mut count = 0; for (id, sender) in &room.members { if Some(*id) == exclude_id { continue; } if let Err(e) = sender.try_send(action.clone()) { warn!("Failed to broadcast to {}: {}", id, e); - } else { - count += 1; } } - let elapsed = start.elapsed(); - if elapsed > std::time::Duration::from_millis(10) { - info!( - "Broadcast to {} members in room {} took {:?}", - count, room_id, elapsed - ); - } } } } From 1b2279eeec0453cdcfe59f1aaeba3925da1fcfcb Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Thu, 22 Jan 2026 00:41:50 +0700 Subject: [PATCH 068/115] refactor(connection): move idle notification logic to connection actor Simplify idle connection handling by moving the notification state from AppState to ConnectionActor. This makes the logic more self-contained and removes the need for shared state management through AppState's notified_idle field. --- src/connection.rs | 48 ++++++++++++++++++++++++----------------------- src/proxy.rs | 1 + src/state.rs | 25 +----------------------- 3 files changed, 27 insertions(+), 47 deletions(-) diff --git a/src/connection.rs b/src/connection.rs index 8e36edc..ffa5813 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -31,6 +31,7 @@ pub struct ConnectionActor { pub limiter: Arc, pub last_read: Instant, pub packet_queue: Vec, + pub notified_idle: bool, } impl ConnectionActor { @@ -41,6 +42,9 @@ impl ConnectionActor { self.write_packet(register_packet).await?; + + self.notify_idle(); + let mut buf = BytesMut::with_capacity(TCP_BUFFER_SIZE); let mut tmp_buf = [0u8; TCP_BUFFER_SIZE]; let mut tick_interval = tokio::time::interval(Duration::from_secs(TICK_INTERVAL_SECS)); @@ -49,7 +53,7 @@ impl ConnectionActor { let mut batch = BytesMut::new(); tokio::select! { - + action = self.rx.recv() => { if let Some(action) = action { self.handle_action(action, &mut batch).await?; @@ -57,23 +61,26 @@ impl ConnectionActor { while let Ok(action) = self.rx.try_recv() { self.handle_action(action, &mut batch).await?; } - + if !batch.is_empty() { self.tcp_writer.write(&batch).await?; } } else { - + break; } } + read_result = reader.read(&mut tmp_buf) => { match read_result { Ok(0) => break, Ok(n) => { self.last_read = Instant::now(); - self.state.reset_idle(self.id); + + + self.notified_idle = false; buf.extend_from_slice(&tmp_buf[..n]); self.process_tcp_buffer(&mut buf).await?; @@ -83,6 +90,7 @@ impl ConnectionActor { } + _ = tick_interval.tick() => { if self.last_read.elapsed() > Duration::from_millis(CONNECTION_TIME_OUT_MS) { @@ -90,8 +98,10 @@ impl ConnectionActor { break; } - if self.is_idle() { - self.state.idle(self.id); + + + if self.is_idle() && !self.notified_idle { + self.notify_idle(); } if self.tcp_writer.last_write.elapsed() > Duration::from_millis(KEEP_ALIVE_INTERVAL_MS) { @@ -133,7 +143,6 @@ impl ConnectionActor { continue; } } - } Ok(()) } @@ -224,13 +233,8 @@ impl ConnectionActor { .await?; } } - FrameworkMessage::KeepAlive => { - - } - FrameworkMessage::RegisterUDP { .. } => { - - - } + FrameworkMessage::KeepAlive => {} + FrameworkMessage::RegisterUDP { .. } => {} _ => { warn!("Unhandled Framework Packet: {:?}", packet); } @@ -524,8 +528,6 @@ impl ConnectionActor { return Ok(()); }; - self.state.reset_idle(connection_id); - let action = if is_tcp { ConnectionAction::SendTCPRaw(buffer) } else { @@ -552,11 +554,9 @@ impl ConnectionActor { action: ConnectionAction, batch: &mut BytesMut, ) -> anyhow::Result<()> { - match action { ConnectionAction::SendTCP(p) => { if let AnyPacket::App(AppPacket::ConnectionPacketWrap(_)) = &p { - } else { info!("Connection {} sending TCP packet: {:?}", self.id, p); } @@ -570,7 +570,6 @@ impl ConnectionActor { self.udp_writer.send_raw(&b).await?; } ConnectionAction::Close => { - return Err(anyhow::anyhow!("Closed")); } ConnectionAction::RegisterUDP(addr) => { @@ -582,7 +581,6 @@ impl ConnectionActor { info!("New connection {} from {}", self.id, addr); - if let Some(sender) = self.state.get_sender(self.id) { self.state.register_udp(addr, sender, self.limiter.clone()); } else { @@ -591,14 +589,13 @@ impl ConnectionActor { self.id )); } - + self.write_packet(AnyPacket::Framework(FrameworkMessage::RegisterUDP { connection_id: self.id, })) .await?; } ConnectionAction::ProcessPacket(packet, is_tcp) => { - self.state.reset_idle(self.id); self.handle_packet(packet, is_tcp).await?; } } @@ -621,6 +618,11 @@ impl ConnectionActor { } fn is_idle(&self) -> bool { - self.packet_queue.is_empty() + self.rx.len() < 5 + } + + fn notify_idle(&mut self) { + self.state.idle(self.id); + self.notified_idle = true; } } diff --git a/src/proxy.rs b/src/proxy.rs index e117d04..0858ba5 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -129,6 +129,7 @@ async fn accept_tcp_connection( limiter, last_read: Instant::now(), packet_queue: Vec::new(), + notified_idle: false, }; if let Err(e) = actor.run(reader).await { diff --git a/src/state.rs b/src/state.rs index a1d0c3e..6924e58 100644 --- a/src/state.rs +++ b/src/state.rs @@ -9,7 +9,7 @@ use crate::packet::{ use crate::rate::AtomicRateLimiter; use crate::utils::current_time_millis; use bytes::Bytes; -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use std::net::SocketAddr; use std::sync::{Arc, RwLock, RwLockReadGuard}; use tokio::sync::mpsc; @@ -352,7 +352,6 @@ pub struct AppState { RwLock, Arc)>>, pub udp_routes: RwLock, Arc)>>, - pub notified_idle: RwLock>, } impl AppState { @@ -367,7 +366,6 @@ impl AppState { }, connections: RwLock::new(HashMap::new()), udp_routes: RwLock::new(HashMap::new()), - notified_idle: RwLock::new(HashSet::new()), } } @@ -436,37 +434,16 @@ impl AppState { } else { return; } - - // Check and set notified_idle - let should_notify = if let Ok(mut idle_set) = self.notified_idle.write() { - idle_set.insert(connection_id) - } else { - false - }; - - if !should_notify { - return; - } - // Find room and notify host if let Some(room_id) = self.rooms.find_connection_room_id(connection_id) { self.rooms.idle(&room_id, connection_id); } } - pub fn reset_idle(&self, connection_id: ConnectionId) { - if let Ok(mut idle_set) = self.notified_idle.write() { - idle_set.remove(&connection_id); - } - } - pub fn remove_connection(&self, connection_id: ConnectionId) { if let Ok(mut conns) = self.connections.write() { conns.remove(&connection_id); } - - self.reset_idle(connection_id); - // Handle room logic let room_id_opt = self.rooms.leave(connection_id); From db6079cdc063ba44a7dc23ceb72181e568fcd6b3 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Thu, 22 Jan 2026 00:59:13 +0700 Subject: [PATCH 069/115] refactor(connection): update timeout and interval constants to use Duration Replace numeric constants with Duration types for better readability and type safety. Adjust tick interval to 120Hz for smoother performance. --- src/connection.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/connection.rs b/src/connection.rs index ffa5813..d778e28 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -17,10 +17,10 @@ use tokio::sync::mpsc; use tracing::{error, info, warn}; const TCP_BUFFER_SIZE: usize = 32768; -const CONNECTION_TIME_OUT_MS: u64 = 30000; -const KEEP_ALIVE_INTERVAL_MS: u64 = 2000; +const CONNECTION_TIME_OUT_MS: Duration = Duration::from_millis(30000); +const KEEP_ALIVE_INTERVAL_MS: Duration = Duration::from_millis(5000); const PACKET_LENGTH_LENGTH: usize = 2; -const TICK_INTERVAL_SECS: u64 = 1; +const TICK_INTERVAL_MS: u64 = 1000 / 120; pub struct ConnectionActor { pub id: ConnectionId, @@ -47,7 +47,7 @@ impl ConnectionActor { let mut buf = BytesMut::with_capacity(TCP_BUFFER_SIZE); let mut tmp_buf = [0u8; TCP_BUFFER_SIZE]; - let mut tick_interval = tokio::time::interval(Duration::from_secs(TICK_INTERVAL_SECS)); + let mut tick_interval = tokio::time::interval(Duration::from_millis(TICK_INTERVAL_MS)); loop { let mut batch = BytesMut::new(); @@ -93,18 +93,16 @@ impl ConnectionActor { _ = tick_interval.tick() => { - if self.last_read.elapsed() > Duration::from_millis(CONNECTION_TIME_OUT_MS) { + if self.last_read.elapsed() > CONNECTION_TIME_OUT_MS { info!("Connection {} timed out", self.id); break; } - - if self.is_idle() && !self.notified_idle { self.notify_idle(); } - if self.tcp_writer.last_write.elapsed() > Duration::from_millis(KEEP_ALIVE_INTERVAL_MS) { + if self.tcp_writer.last_write.elapsed() > KEEP_ALIVE_INTERVAL_MS { self.write_packet(AnyPacket::Framework(FrameworkMessage::KeepAlive)).await?; } } From 04f22509d584df8be990bb23876dd5a12ad85551 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Thu, 22 Jan 2026 01:09:01 +0700 Subject: [PATCH 070/115] refactor(connection): improve connection handling and idle state management - Reorder timeout and keep-alive checks for better logical flow - Remove redundant whitespace and improve code readability - Ensure idle state is properly reset when processing packets --- src/connection.rs | 25 +++++++------------------ src/error.rs | 2 +- 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/src/connection.rs b/src/connection.rs index d778e28..5701135 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -42,7 +42,6 @@ impl ConnectionActor { self.write_packet(register_packet).await?; - self.notify_idle(); let mut buf = BytesMut::with_capacity(TCP_BUFFER_SIZE); @@ -53,7 +52,6 @@ impl ConnectionActor { let mut batch = BytesMut::new(); tokio::select! { - action = self.rx.recv() => { if let Some(action) = action { self.handle_action(action, &mut batch).await?; @@ -66,20 +64,15 @@ impl ConnectionActor { self.tcp_writer.write(&batch).await?; } } else { - break; } } - - read_result = reader.read(&mut tmp_buf) => { match read_result { - Ok(0) => break, + Ok(0) => break, Ok(n) => { self.last_read = Instant::now(); - - self.notified_idle = false; buf.extend_from_slice(&tmp_buf[..n]); @@ -89,21 +82,16 @@ impl ConnectionActor { } } - - - _ = tick_interval.tick() => { - if self.last_read.elapsed() > CONNECTION_TIME_OUT_MS { - info!("Connection {} timed out", self.id); - break; - } - if self.is_idle() && !self.notified_idle { self.notify_idle(); + } else if self.tcp_writer.last_write.elapsed() > KEEP_ALIVE_INTERVAL_MS { + self.write_packet(AnyPacket::Framework(FrameworkMessage::KeepAlive)).await?; } - if self.tcp_writer.last_write.elapsed() > KEEP_ALIVE_INTERVAL_MS { - self.write_packet(AnyPacket::Framework(FrameworkMessage::KeepAlive)).await?; + if self.last_read.elapsed() > CONNECTION_TIME_OUT_MS { + info!("Connection {} timed out", self.id); + break; } } } @@ -594,6 +582,7 @@ impl ConnectionActor { .await?; } ConnectionAction::ProcessPacket(packet, is_tcp) => { + self.notified_idle = false; self.handle_packet(packet, is_tcp).await?; } } diff --git a/src/error.rs b/src/error.rs index 0508fbc..dfe79be 100644 --- a/src/error.rs +++ b/src/error.rs @@ -19,7 +19,7 @@ pub enum AppError { #[error("Lock poison error")] LockPoison, - + #[error("Invalid state: {0}")] InvalidState(String), From 45759063a024bcb2d5d62db8912f210caa2c08a0 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Thu, 22 Jan 2026 01:20:00 +0700 Subject: [PATCH 071/115] feat(connection): add packet logging for all sent and received packets Log all packets being sent and received by ConnectionActor for better debugging visibility. Removes the special case that previously excluded ConnectionPacketWrap from logging. --- src/connection.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/connection.rs b/src/connection.rs index 5701135..da786fe 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -136,6 +136,8 @@ impl ConnectionActor { async fn handle_packet(&mut self, packet: AnyPacket, is_tcp: bool) -> anyhow::Result<()> { let is_framework = matches!(packet, AnyPacket::Framework(_)); + info!("Received packet: {:?}", packet); + if !is_framework { let room_id_opt = self.state.rooms.find_connection_room_id(self.id); let is_host = if let Some(ref room_id) = room_id_opt { @@ -542,10 +544,7 @@ impl ConnectionActor { ) -> anyhow::Result<()> { match action { ConnectionAction::SendTCP(p) => { - if let AnyPacket::App(AppPacket::ConnectionPacketWrap(_)) = &p { - } else { - info!("Connection {} sending TCP packet: {:?}", self.id, p); - } + info!("Connection {} sending TCP packet: {:?}", self.id, p); let bytes = p.to_bytes(); batch.extend_from_slice(&ConnectionActor::prepend_len(bytes.freeze())); } From e6e1b62571d8d41d4513ca100dc8a4332c75c85c Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Thu, 22 Jan 2026 02:15:42 +0700 Subject: [PATCH 072/115] fix(connection): adjust log levels and idle condition logic Change log level from warn to info for room creation and from info to warn for broadcast failures Simplify idle condition check to always return true --- src/connection.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/connection.rs b/src/connection.rs index da786fe..a22fb6d 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -382,10 +382,10 @@ impl ConnectionActor { id: room.id.clone(), data: room.clone(), }) { - info!("Fail to broadcast room update {}", err); + warn!("Fail to broadcast room update {}", err); } - warn!("Room {} created by connection {}.", room_id, self.id); + info!("Room {} created by connection {}.", room_id, self.id); } } AppPacket::RoomClosureRequest(_) => { @@ -604,7 +604,7 @@ impl ConnectionActor { } fn is_idle(&self) -> bool { - self.rx.len() < 5 + true } fn notify_idle(&mut self) { From 3330b18e6d87c3ea2672dc556e9ef4177cba9c7d Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Thu, 22 Jan 2026 02:52:00 +0700 Subject: [PATCH 073/115] fix(connection): handle idle notification more carefully Modify idle notification logic to only set notified_idle flag when the state.idle() operation succeeds. State methods now return bool to indicate success/failure, allowing better flow control. This prevents false idle notifications when the host channel is full or other errors occur. --- src/connection.rs | 7 +++++-- src/proxy.rs | 1 + src/state.rs | 37 ++++++++++++++++++++++++------------- 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/src/connection.rs b/src/connection.rs index a22fb6d..c476f59 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -558,6 +558,8 @@ impl ConnectionActor { return Err(anyhow::anyhow!("Closed")); } ConnectionAction::RegisterUDP(addr) => { + self.notified_idle = false; + if self.udp_writer.addr.is_some() { return Ok(()); } @@ -608,7 +610,8 @@ impl ConnectionActor { } fn notify_idle(&mut self) { - self.state.idle(self.id); - self.notified_idle = true; + if self.state.idle(self.id) { + self.notified_idle = true; + } } } diff --git a/src/proxy.rs b/src/proxy.rs index 0858ba5..29a4904 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -45,6 +45,7 @@ fn spawn_udp_listener(state: Arc, socket: Arc) { match AnyPacket::read(&mut cursor) { Ok(packet) => { + if let AnyPacket::Framework(FrameworkMessage::RegisterUDP { connection_id, }) = packet diff --git a/src/state.rs b/src/state.rs index 6924e58..13a3309 100644 --- a/src/state.rs +++ b/src/state.rs @@ -293,21 +293,21 @@ impl Rooms { } } - pub fn idle(&self, room_id: &RoomId, connection_id: ConnectionId) { + pub fn idle(&self, room_id: &RoomId, connection_id: ConnectionId) -> bool { let rooms = match self.rooms.read() { Ok(rooms) => rooms, - Err(_) => return, + Err(_) => return true, }; if let Some(room) = rooms.get(room_id) { // Don't process host idle if room.host_connection_id == connection_id { - return; + return true; } // Check if client is in room if !room.members.contains_key(&connection_id) { - return; + return true; } // Send to host @@ -315,14 +315,23 @@ impl Rooms { let packet = AnyPacket::App(AppPacket::ConnectionIdling(ConnectionIdlingPacket { connection_id, })); - if let Err(e) = sender.try_send(ConnectionAction::SendTCP(packet)) { - warn!( - "Failed to forward idle packet to host {}: {}", - room.host_connection_id, e - ); + match sender.try_send(ConnectionAction::SendTCP(packet)) { + Ok(_) => return true, + Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => { + warn!("Host channel full, retrying idle packet later"); + return false; + } + Err(e) => { + warn!( + "Failed to forward idle packet to host {}: {}", + room.host_connection_id, e + ); + return true; + } } } } + true } } @@ -425,18 +434,20 @@ impl AppState { self.udp_routes.read().ok()?.get(addr).cloned() } - pub fn idle(&self, connection_id: ConnectionId) { + pub fn idle(&self, connection_id: ConnectionId) -> bool { // Only process valid connections if let Ok(conns) = self.connections.read() { if !conns.contains_key(&connection_id) { - return; + return true; } } else { - return; + return true; } // Find room and notify host if let Some(room_id) = self.rooms.find_connection_room_id(connection_id) { - self.rooms.idle(&room_id, connection_id); + self.rooms.idle(&room_id, connection_id) + } else { + true } } From 224f6c29d27aa16de4d03473211e0c50cb87e378 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Thu, 22 Jan 2026 02:58:38 +0700 Subject: [PATCH 074/115] refactor(state): simplify idle connection handling logic Remove redundant room_id parameter and connection validation checks Consolidate idle logic in AppState to directly call Rooms::idle --- src/state.rs | 31 ++++++++----------------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/src/state.rs b/src/state.rs index 13a3309..23770de 100644 --- a/src/state.rs +++ b/src/state.rs @@ -293,28 +293,26 @@ impl Rooms { } } - pub fn idle(&self, room_id: &RoomId, connection_id: ConnectionId) -> bool { + pub fn idle(&self, connection_id: ConnectionId) -> bool { let rooms = match self.rooms.read() { Ok(rooms) => rooms, Err(_) => return true, }; - if let Some(room) = rooms.get(room_id) { - // Don't process host idle + if let Some(room) = rooms + .iter() + .find(|(_, room)| room.members.contains_key(&connection_id)) + .map(|(_, room)| room) + { if room.host_connection_id == connection_id { return true; } - // Check if client is in room - if !room.members.contains_key(&connection_id) { - return true; - } - - // Send to host if let Some(sender) = room.members.get(&room.host_connection_id) { let packet = AnyPacket::App(AppPacket::ConnectionIdling(ConnectionIdlingPacket { connection_id, })); + match sender.try_send(ConnectionAction::SendTCP(packet)) { Ok(_) => return true, Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => { @@ -435,20 +433,7 @@ impl AppState { } pub fn idle(&self, connection_id: ConnectionId) -> bool { - // Only process valid connections - if let Ok(conns) = self.connections.read() { - if !conns.contains_key(&connection_id) { - return true; - } - } else { - return true; - } - // Find room and notify host - if let Some(room_id) = self.rooms.find_connection_room_id(connection_id) { - self.rooms.idle(&room_id, connection_id) - } else { - true - } + self.rooms.idle(connection_id) } pub fn remove_connection(&self, connection_id: ConnectionId) { From 5facfc99a35d907fc8cd48aca333ec373702db78 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Thu, 22 Jan 2026 03:05:25 +0700 Subject: [PATCH 075/115] refactor(proxy): simplify UDP packet handling logic Remove redundant comments and consolidate nested if-else statements into a cleaner match and guard clause structure. The changes improve code readability while maintaining the same functionality. --- src/connection.rs | 2 ++ src/proxy.rs | 20 ++++++++------------ 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/connection.rs b/src/connection.rs index c476f59..adc494f 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -610,8 +610,10 @@ impl ConnectionActor { } fn notify_idle(&mut self) { + info!("Connection {} is idle", self.id); if self.state.idle(self.id) { self.notified_idle = true; + info!("Notify Connection {} is idle success", self.id); } } } diff --git a/src/proxy.rs b/src/proxy.rs index 29a4904..c06f95c 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -45,25 +45,21 @@ fn spawn_udp_listener(state: Arc, socket: Arc) { match AnyPacket::read(&mut cursor) { Ok(packet) => { - if let AnyPacket::Framework(FrameworkMessage::RegisterUDP { connection_id, }) = packet { - // Handle Register UDP handle_register_udp(&state, connection_id, addr).await; } else { - // Normal packet, route it - - if let Some((sender, _)) = state.get_route(&addr) { - if let Err(e) = sender - .try_send(ConnectionAction::ProcessPacket(packet, false)) - { - warn!("Failed to forward UDP packet: {}", e); - } - } else { - // Unknown UDP sender, ignore + let Some((sender, _)) = state.get_route(&addr) else { warn!("Unknown UDP sender: {}", addr); + continue; + }; + + if let Err(e) = + sender.try_send(ConnectionAction::ProcessPacket(packet, false)) + { + warn!("Failed to forward UDP packet: {}", e); } } } From eed1e59ec9205d939d533229b0c8f33c74c3256a Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Thu, 22 Jan 2026 03:06:44 +0700 Subject: [PATCH 076/115] feat(connection): add logging for connection ticks Log connection ticks to help debug idle connection handling --- src/connection.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/connection.rs b/src/connection.rs index adc494f..666416e 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -83,6 +83,8 @@ impl ConnectionActor { } _ = tick_interval.tick() => { + info!("Connection {} tick", self.id); + if self.is_idle() && !self.notified_idle { self.notify_idle(); } else if self.tcp_writer.last_write.elapsed() > KEEP_ALIVE_INTERVAL_MS { From 076a3f56e985ada48db340e768fd54ce05ade942 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Thu, 22 Jan 2026 03:13:44 +0700 Subject: [PATCH 077/115] refactor(connection): remove redundant tick log and improve idle detection - Remove noisy tick interval logging that was cluttering logs - Add error logging when failing to acquire rooms read lock - Add host idle status logging for better debugging visibility --- src/connection.rs | 2 -- src/state.rs | 6 +++++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/connection.rs b/src/connection.rs index 666416e..adc494f 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -83,8 +83,6 @@ impl ConnectionActor { } _ = tick_interval.tick() => { - info!("Connection {} tick", self.id); - if self.is_idle() && !self.notified_idle { self.notify_idle(); } else if self.tcp_writer.last_write.elapsed() > KEEP_ALIVE_INTERVAL_MS { diff --git a/src/state.rs b/src/state.rs index 23770de..df84600 100644 --- a/src/state.rs +++ b/src/state.rs @@ -296,7 +296,10 @@ impl Rooms { pub fn idle(&self, connection_id: ConnectionId) -> bool { let rooms = match self.rooms.read() { Ok(rooms) => rooms, - Err(_) => return true, + Err(err) => { + error!("Failed to acquire rooms read lock: {}", err); + return true; + } }; if let Some(room) = rooms @@ -305,6 +308,7 @@ impl Rooms { .map(|(_, room)| room) { if room.host_connection_id == connection_id { + info!("Host {} is idle", connection_id); return true; } From 2a5864fe38f9cc928e868870acc5f64f87fb5946 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Thu, 22 Jan 2026 03:26:46 +0700 Subject: [PATCH 078/115] fix: remove redundant log and update idle state on send actions Remove unnecessary host idle log message that was cluttering logs. Update connection idle state when raw TCP/UDP packets are sent to prevent premature idle notifications. --- src/connection.rs | 2 ++ src/state.rs | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/connection.rs b/src/connection.rs index adc494f..18eeaf6 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -549,9 +549,11 @@ impl ConnectionActor { batch.extend_from_slice(&ConnectionActor::prepend_len(bytes.freeze())); } ConnectionAction::SendTCPRaw(b) => { + self.notified_idle = false; batch.extend_from_slice(&ConnectionActor::prepend_len(b)); } ConnectionAction::SendUDPRaw(b) => { + self.notified_idle = false; self.udp_writer.send_raw(&b).await?; } ConnectionAction::Close => { diff --git a/src/state.rs b/src/state.rs index df84600..f2b399f 100644 --- a/src/state.rs +++ b/src/state.rs @@ -308,7 +308,6 @@ impl Rooms { .map(|(_, room)| room) { if room.host_connection_id == connection_id { - info!("Host {} is idle", connection_id); return true; } From 209714736ed5df7a432754e016865858a9e86d46 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Thu, 22 Jan 2026 03:38:46 +0700 Subject: [PATCH 079/115] refactor(connection): remove redundant debug logging Remove unnecessary info-level logging for packet handling, packet sending, and idle notifications to reduce log noise and improve performance --- src/connection.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/connection.rs b/src/connection.rs index 18eeaf6..170ad61 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -136,8 +136,6 @@ impl ConnectionActor { async fn handle_packet(&mut self, packet: AnyPacket, is_tcp: bool) -> anyhow::Result<()> { let is_framework = matches!(packet, AnyPacket::Framework(_)); - info!("Received packet: {:?}", packet); - if !is_framework { let room_id_opt = self.state.rooms.find_connection_room_id(self.id); let is_host = if let Some(ref room_id) = room_id_opt { @@ -544,7 +542,6 @@ impl ConnectionActor { ) -> anyhow::Result<()> { match action { ConnectionAction::SendTCP(p) => { - info!("Connection {} sending TCP packet: {:?}", self.id, p); let bytes = p.to_bytes(); batch.extend_from_slice(&ConnectionActor::prepend_len(bytes.freeze())); } @@ -612,10 +609,8 @@ impl ConnectionActor { } fn notify_idle(&mut self) { - info!("Connection {} is idle", self.id); if self.state.idle(self.id) { self.notified_idle = true; - info!("Notify Connection {} is idle success", self.id); } } } From b2a6b2c2280b7408fcdc9e2c4b72040e2a99e1ce Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Thu, 22 Jan 2026 03:47:09 +0700 Subject: [PATCH 080/115] perf(connection): increase tick interval from 120Hz to 30Hz Reduces CPU usage by lowering the tick rate while maintaining acceptable performance --- src/connection.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/connection.rs b/src/connection.rs index 170ad61..1f5f147 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -20,7 +20,7 @@ const TCP_BUFFER_SIZE: usize = 32768; const CONNECTION_TIME_OUT_MS: Duration = Duration::from_millis(30000); const KEEP_ALIVE_INTERVAL_MS: Duration = Duration::from_millis(5000); const PACKET_LENGTH_LENGTH: usize = 2; -const TICK_INTERVAL_MS: u64 = 1000 / 120; +const TICK_INTERVAL_MS: u64 = 1000 / 30; pub struct ConnectionActor { pub id: ConnectionId, From 2706129c07a24d8c28c28d90c7a2066df1e8180e Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Thu, 22 Jan 2026 04:16:01 +0700 Subject: [PATCH 081/115] fix(connection): improve error logging with connection ID Include connection ID in packet read error messages to help with debugging --- src/connection.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/connection.rs b/src/connection.rs index 1f5f147..55b5720 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -125,7 +125,7 @@ impl ConnectionActor { self.handle_packet(packet, true).await?; } Err(e) => { - error!("Error reading packet: {:?}", e); + error!("Error reading packet: {:?} from connection {}", e, self.id); continue; } } From d8a5f94153d23b26e7b0242ce8bb2313e2de94f5 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Thu, 22 Jan 2026 04:45:30 +0700 Subject: [PATCH 082/115] refactor(proxy): improve udp packet handling readability Restructure the match statement for better clarity and maintainability while keeping the same functionality. The nested if-let has been replaced with a more explicit match pattern. --- src/proxy.rs | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/src/proxy.rs b/src/proxy.rs index c06f95c..10a2339 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -45,23 +45,25 @@ fn spawn_udp_listener(state: Arc, socket: Arc) { match AnyPacket::read(&mut cursor) { Ok(packet) => { - if let AnyPacket::Framework(FrameworkMessage::RegisterUDP { - connection_id, - }) = packet - { - handle_register_udp(&state, connection_id, addr).await; - } else { - let Some((sender, _)) = state.get_route(&addr) else { - warn!("Unknown UDP sender: {}", addr); - continue; - }; - - if let Err(e) = - sender.try_send(ConnectionAction::ProcessPacket(packet, false)) - { - warn!("Failed to forward UDP packet: {}", e); + match packet { + AnyPacket::Framework(FrameworkMessage::RegisterUDP { + connection_id, + }) => { + handle_register_udp(&state, connection_id, addr).await; } - } + _ => { + let Some((sender, _)) = state.get_route(&addr) else { + warn!("Unknown UDP sender: {}", addr); + continue; + }; + + if let Err(e) = sender + .try_send(ConnectionAction::ProcessPacket(packet, false)) + { + warn!("Failed to forward UDP packet: {}", e); + } + } + }; } Err(e) => { warn!("UDP Parse Error from {}: {}", addr, e); From bc80fcc052a775a09605cc90ccc0b29beb0e47bc Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Thu, 22 Jan 2026 04:55:30 +0700 Subject: [PATCH 083/115] fix(connection): handle unsupported client version with popup message When receiving an error while reading packets, show a popup message informing the user their client version is no longer supported and needs updating. --- src/connection.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/connection.rs b/src/connection.rs index 55b5720..86b2094 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -1,7 +1,7 @@ use crate::constant::{ArcCloseReason, CloseReason, MessageType}; use crate::packet::{ AnyPacket, AppPacket, ConnectionClosedPacket, ConnectionId, ConnectionPacketWrapPacket, - FrameworkMessage, Message2Packet, MessagePacket, RoomClosedPacket, RoomLinkPacket, + FrameworkMessage, Message2Packet, MessagePacket, PopupPacket, RoomClosedPacket, RoomLinkPacket, }; use crate::rate::AtomicRateLimiter; use crate::state::{AppState, ConnectionAction, RoomInit, RoomUpdate}; @@ -125,6 +125,11 @@ impl ConnectionActor { self.handle_packet(packet, true).await?; } Err(e) => { + let packet = AnyPacket::App(AppPacket::Popup(PopupPacket { + message: "Your version of MindustryTool is no longer supported. Please update to the latest version.".to_string(), + })); + + self.write_packet(packet).await?; error!("Error reading packet: {:?} from connection {}", e, self.id); continue; } From b958a8c4d02a0f593f27394e6cf03cea246fb24f Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Thu, 22 Jan 2026 20:19:24 +0700 Subject: [PATCH 084/115] refactor(connection): reorder select branches for better readability The TCP read branch was moved to the top of the select statement to make the control flow more logical and easier to follow. This is purely a structural change with no behavioral impact. --- src/connection.rs | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/connection.rs b/src/connection.rs index 86b2094..8db019e 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -52,6 +52,20 @@ impl ConnectionActor { let mut batch = BytesMut::new(); tokio::select! { + read_result = reader.read(&mut tmp_buf) => { + match read_result { + Ok(0) => break, + Ok(n) => { + self.last_read = Instant::now(); + self.notified_idle = false; + + buf.extend_from_slice(&tmp_buf[..n]); + self.process_tcp_buffer(&mut buf).await?; + } + Err(e) => return Err(e.into()), + } + } + action = self.rx.recv() => { if let Some(action) = action { self.handle_action(action, &mut batch).await?; @@ -68,20 +82,6 @@ impl ConnectionActor { } } - read_result = reader.read(&mut tmp_buf) => { - match read_result { - Ok(0) => break, - Ok(n) => { - self.last_read = Instant::now(); - self.notified_idle = false; - - buf.extend_from_slice(&tmp_buf[..n]); - self.process_tcp_buffer(&mut buf).await?; - } - Err(e) => return Err(e.into()), - } - } - _ = tick_interval.tick() => { if self.is_idle() && !self.notified_idle { self.notify_idle(); From 0406841090f0f86b74f8e04069f24984bcf91d02 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Thu, 22 Jan 2026 21:06:15 +0700 Subject: [PATCH 085/115] feat(models): add protocol version field to room structures Add protocol_version field to Room, RoomInit, and RoomView structs to track the protocol version being used. This enables better version compatibility checks and logging. --- src/connection.rs | 1 + src/models.rs | 2 ++ src/state.rs | 5 +++++ 3 files changed, 8 insertions(+) diff --git a/src/connection.rs b/src/connection.rs index 8db019e..0c28940 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -366,6 +366,7 @@ impl ConnectionActor { connection_id: self.id, password: p.password, stats: p.data, + protocol_version: p.version, sender, }); self.write_packet(AnyPacket::App(AppPacket::RoomLink(RoomLinkPacket { diff --git a/src/models.rs b/src/models.rs index ce1cdae..e3ccf6b 100644 --- a/src/models.rs +++ b/src/models.rs @@ -38,6 +38,8 @@ pub struct RoomView { #[serde(rename = "createdAt")] pub created_at: u128, pub ping: u128, + #[serde(rename = "protocolVersion")] + pub protocol_version: String, } #[derive(Clone, Debug, Serialize)] diff --git a/src/state.rs b/src/state.rs index f2b399f..7305936 100644 --- a/src/state.rs +++ b/src/state.rs @@ -36,6 +36,7 @@ pub struct Room { pub members: HashMap>, pub stats: Stats, pub ping: u128, + pub protocol_version: String, } #[derive(Clone, Debug)] @@ -56,6 +57,7 @@ pub struct RoomInit { pub password: String, pub stats: Stats, pub sender: mpsc::Sender, + pub protocol_version: String, } impl Rooms { @@ -165,6 +167,7 @@ impl Rooms { connection_id, stats, sender, + protocol_version, } = init; let password = if password.is_empty() { @@ -186,6 +189,7 @@ impl Rooms { members, created_at: current_time_millis(), updated_at: current_time_millis(), + protocol_version, ping: 0, }; @@ -351,6 +355,7 @@ impl From<&Room> for RoomView { version: room.stats.version.clone(), created_at: room.stats.created_at, ping: room.ping, + protocol_version: room.protocol_version.clone(), } } } From 2df35ec07088d02581312a199d442f6d6d0604e6 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Fri, 23 Jan 2026 03:01:37 +0700 Subject: [PATCH 086/115] fix(connection): handle peer disconnection with proper error propagation Replace break statements with error returns when peer closes connection to ensure proper error handling and cleanup rather than silently exiting loop. --- src/connection.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/connection.rs b/src/connection.rs index 0c28940..c4567ff 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -54,7 +54,7 @@ impl ConnectionActor { tokio::select! { read_result = reader.read(&mut tmp_buf) => { match read_result { - Ok(0) => break, + Ok(0) => return Err(anyhow::anyhow!("Connection closed by peer")), Ok(n) => { self.last_read = Instant::now(); self.notified_idle = false; @@ -78,7 +78,7 @@ impl ConnectionActor { self.tcp_writer.write(&batch).await?; } } else { - break; + return Err(anyhow::anyhow!("Connection closed by peer, no action")); } } From 968123695c2925d48fe95c243c62726dbf050ef4 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Fri, 23 Jan 2026 03:07:32 +0700 Subject: [PATCH 087/115] feat: add debug logging for packet send/receive operations Add info-level logging to track incoming and outgoing packets in ConnectionActor. This improves debugging capabilities for network communication issues by providing visibility into packet flow at connection level. --- src/connection.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/connection.rs b/src/connection.rs index c4567ff..8564d68 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -141,6 +141,8 @@ impl ConnectionActor { async fn handle_packet(&mut self, packet: AnyPacket, is_tcp: bool) -> anyhow::Result<()> { let is_framework = matches!(packet, AnyPacket::Framework(_)); + info!("Connection {} received packet: {:?}", self.id, packet); + if !is_framework { let room_id_opt = self.state.rooms.find_connection_room_id(self.id); let is_host = if let Some(ref room_id) = room_id_opt { @@ -550,14 +552,17 @@ impl ConnectionActor { ConnectionAction::SendTCP(p) => { let bytes = p.to_bytes(); batch.extend_from_slice(&ConnectionActor::prepend_len(bytes.freeze())); + info!("Connection {} sent packet: {:?}", self.id, p); } ConnectionAction::SendTCPRaw(b) => { self.notified_idle = false; + info!("Connection {} sent packet: {:?}", self.id, b); batch.extend_from_slice(&ConnectionActor::prepend_len(b)); } ConnectionAction::SendUDPRaw(b) => { self.notified_idle = false; self.udp_writer.send_raw(&b).await?; + info!("Connection {} sent packet: {:?}", self.id, b); } ConnectionAction::Close => { return Err(anyhow::anyhow!("Closed")); From bdf507b23fc242eb48d0ef805381c15337e24977 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Fri, 23 Jan 2026 03:10:29 +0700 Subject: [PATCH 088/115] refactor(proxy): simplify UDP packet handling with if let pattern Replace match statement with if-let for RegisterUDP packets to flatten nested structure and improve readability. --- src/proxy.rs | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/src/proxy.rs b/src/proxy.rs index 10a2339..c06f95c 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -45,25 +45,23 @@ fn spawn_udp_listener(state: Arc, socket: Arc) { match AnyPacket::read(&mut cursor) { Ok(packet) => { - match packet { - AnyPacket::Framework(FrameworkMessage::RegisterUDP { - connection_id, - }) => { - handle_register_udp(&state, connection_id, addr).await; + if let AnyPacket::Framework(FrameworkMessage::RegisterUDP { + connection_id, + }) = packet + { + handle_register_udp(&state, connection_id, addr).await; + } else { + let Some((sender, _)) = state.get_route(&addr) else { + warn!("Unknown UDP sender: {}", addr); + continue; + }; + + if let Err(e) = + sender.try_send(ConnectionAction::ProcessPacket(packet, false)) + { + warn!("Failed to forward UDP packet: {}", e); } - _ => { - let Some((sender, _)) = state.get_route(&addr) else { - warn!("Unknown UDP sender: {}", addr); - continue; - }; - - if let Err(e) = sender - .try_send(ConnectionAction::ProcessPacket(packet, false)) - { - warn!("Failed to forward UDP packet: {}", e); - } - } - }; + } } Err(e) => { warn!("UDP Parse Error from {}: {}", addr, e); From acb7855f3d89c83b27894b611d9dfd70613f4333 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Fri, 23 Jan 2026 03:26:47 +0700 Subject: [PATCH 089/115] fix: send popup and close connection on invalid room join When a connection attempts to join a non-existent room, the system now sends a "Room not found" popup message to the client before closing the connection. This provides clear user feedback. Also refactored UDP packet handling to use explicit match arms for better readability. --- src/connection.rs | 14 ++++++++++---- src/proxy.rs | 34 ++++++++++++++++++---------------- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/src/connection.rs b/src/connection.rs index 8564d68..433e69f 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -18,9 +18,9 @@ use tracing::{error, info, warn}; const TCP_BUFFER_SIZE: usize = 32768; const CONNECTION_TIME_OUT_MS: Duration = Duration::from_millis(30000); -const KEEP_ALIVE_INTERVAL_MS: Duration = Duration::from_millis(5000); +const KEEP_ALIVE_INTERVAL_MS: Duration = Duration::from_millis(3000); const PACKET_LENGTH_LENGTH: usize = 2; -const TICK_INTERVAL_MS: u64 = 1000 / 30; +const TICK_INTERVAL_MS: u64 = 1000 / 60; pub struct ConnectionActor { pub id: ConnectionId, @@ -65,7 +65,7 @@ impl ConnectionActor { Err(e) => return Err(e.into()), } } - + action = self.rx.recv() => { if let Some(action) = action { self.handle_action(action, &mut batch).await?; @@ -125,12 +125,13 @@ impl ConnectionActor { self.handle_packet(packet, true).await?; } Err(e) => { + error!("Error reading packet: {:?} from connection {}", e, self.id); + let packet = AnyPacket::App(AppPacket::Popup(PopupPacket { message: "Your version of MindustryTool is no longer supported. Please update to the latest version.".to_string(), })); self.write_packet(packet).await?; - error!("Error reading packet: {:?} from connection {}", e, self.id); continue; } } @@ -321,6 +322,11 @@ impl ConnectionActor { "Connection {} tried to join a non-existent room {}.", self.id, p.room_id ); + self.write_packet(AnyPacket::App(AppPacket::Popup(PopupPacket { + message: "Room not found".to_string(), + }))) + .await?; + self.write_packet(AnyPacket::App(AppPacket::ConnectionClosed( ConnectionClosedPacket { connection_id: self.id, diff --git a/src/proxy.rs b/src/proxy.rs index c06f95c..10a2339 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -45,23 +45,25 @@ fn spawn_udp_listener(state: Arc, socket: Arc) { match AnyPacket::read(&mut cursor) { Ok(packet) => { - if let AnyPacket::Framework(FrameworkMessage::RegisterUDP { - connection_id, - }) = packet - { - handle_register_udp(&state, connection_id, addr).await; - } else { - let Some((sender, _)) = state.get_route(&addr) else { - warn!("Unknown UDP sender: {}", addr); - continue; - }; - - if let Err(e) = - sender.try_send(ConnectionAction::ProcessPacket(packet, false)) - { - warn!("Failed to forward UDP packet: {}", e); + match packet { + AnyPacket::Framework(FrameworkMessage::RegisterUDP { + connection_id, + }) => { + handle_register_udp(&state, connection_id, addr).await; } - } + _ => { + let Some((sender, _)) = state.get_route(&addr) else { + warn!("Unknown UDP sender: {}", addr); + continue; + }; + + if let Err(e) = sender + .try_send(ConnectionAction::ProcessPacket(packet, false)) + { + warn!("Failed to forward UDP packet: {}", e); + } + } + }; } Err(e) => { warn!("UDP Parse Error from {}: {}", addr, e); From 31322ead5c31afe4717ddb93e2ff5c0f1a74c4ba Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Fri, 23 Jan 2026 03:37:46 +0700 Subject: [PATCH 090/115] perf: remove verbose packet logging to reduce overhead The info-level logs for every packet sent/received were generating significant noise and performance overhead. These logs are not needed for normal operation and can be enabled via debug logging when troubleshooting is required. --- src/connection.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/connection.rs b/src/connection.rs index 433e69f..508292c 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -85,7 +85,9 @@ impl ConnectionActor { _ = tick_interval.tick() => { if self.is_idle() && !self.notified_idle { self.notify_idle(); - } else if self.tcp_writer.last_write.elapsed() > KEEP_ALIVE_INTERVAL_MS { + } + + if self.tcp_writer.last_write.elapsed() > KEEP_ALIVE_INTERVAL_MS { self.write_packet(AnyPacket::Framework(FrameworkMessage::KeepAlive)).await?; } @@ -142,8 +144,6 @@ impl ConnectionActor { async fn handle_packet(&mut self, packet: AnyPacket, is_tcp: bool) -> anyhow::Result<()> { let is_framework = matches!(packet, AnyPacket::Framework(_)); - info!("Connection {} received packet: {:?}", self.id, packet); - if !is_framework { let room_id_opt = self.state.rooms.find_connection_room_id(self.id); let is_host = if let Some(ref room_id) = room_id_opt { @@ -558,17 +558,14 @@ impl ConnectionActor { ConnectionAction::SendTCP(p) => { let bytes = p.to_bytes(); batch.extend_from_slice(&ConnectionActor::prepend_len(bytes.freeze())); - info!("Connection {} sent packet: {:?}", self.id, p); } ConnectionAction::SendTCPRaw(b) => { self.notified_idle = false; - info!("Connection {} sent packet: {:?}", self.id, b); batch.extend_from_slice(&ConnectionActor::prepend_len(b)); } ConnectionAction::SendUDPRaw(b) => { self.notified_idle = false; self.udp_writer.send_raw(&b).await?; - info!("Connection {} sent packet: {:?}", self.id, b); } ConnectionAction::Close => { return Err(anyhow::anyhow!("Closed")); From e4f08d302f51569ce99340adf812909d6b4750e6 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Fri, 23 Jan 2026 03:50:13 +0700 Subject: [PATCH 091/115] fix(packet): validate no extra data after packet and add logging - Ensure packet parsing fails if buffer has remaining bytes after reading - Add info log when reading AppPacket to aid debugging - Refactor keep-alive condition to use else-if for clarity --- src/connection.rs | 4 +--- src/packet.rs | 14 +++++++++++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/connection.rs b/src/connection.rs index 508292c..76fdf14 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -85,9 +85,7 @@ impl ConnectionActor { _ = tick_interval.tick() => { if self.is_idle() && !self.notified_idle { self.notify_idle(); - } - - if self.tcp_writer.last_write.elapsed() > KEEP_ALIVE_INTERVAL_MS { + } else if self.tcp_writer.last_write.elapsed() > KEEP_ALIVE_INTERVAL_MS { self.write_packet(AnyPacket::Framework(FrameworkMessage::KeepAlive)).await?; } diff --git a/src/packet.rs b/src/packet.rs index 1860faf..75a37c0 100644 --- a/src/packet.rs +++ b/src/packet.rs @@ -2,6 +2,7 @@ use crate::constant::{ArcCloseReason, CloseReason, MessageType}; use crate::error::AppError; use crate::models::Stats; use bytes::{Buf, BufMut, Bytes, BytesMut}; +use tracing::info; use std::convert::TryFrom; use std::io::Cursor; @@ -137,7 +138,7 @@ impl AnyPacket { let id = buf.get_i8(); - match id { + let result = match id { FRAMEWORK_PACKET_ID => Ok(AnyPacket::Framework(FrameworkMessage::read(buf)?)), APP_PACKET_ID => Ok(AnyPacket::App(AppPacket::read(buf)?)), _ => { @@ -151,7 +152,16 @@ impl AnyPacket { Ok(AnyPacket::Raw(bytes)) } + }; + + if buf.has_remaining() { + return Err(AppError::PacketParsing(format!( + "Extra data ({} bytes) after packet", + buf.remaining() + ))); } + + result } pub fn to_bytes(&self) -> BytesMut { @@ -223,6 +233,8 @@ impl AppPacket { pub fn read(buf: &mut Cursor) -> Result { let pid = buf.get_u8(); + info!("Read AppPacket with ID: {}", pid); + match pid { 0 => { let connection_id = ConnectionId(buf.get_u32()); From 331caba5940d9ef43bc469bc101c150f3dd4f7b0 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Fri, 23 Jan 2026 04:22:39 +0700 Subject: [PATCH 092/115] refactor: replace RwLock with DashMap for concurrent state management - Replace RwLock-protected HashMaps with DashMap for rooms and connections to improve concurrent access performance - Add connection_to_room mapping for O(1) room lookup by connection ID - Implement vectored I/O for TCP writes to reduce syscall overhead - Move rate limiting earlier in packet processing pipeline - Simplify room state access patterns and eliminate lock poisoning concerns --- src/connection.rs | 271 ++++++++++++++++++++++++---------------------- src/http.rs | 38 +++---- src/proxy.rs | 29 ++++- src/state.rs | 190 +++++++++++--------------------- src/writer.rs | 7 ++ 5 files changed, 250 insertions(+), 285 deletions(-) diff --git a/src/connection.rs b/src/connection.rs index 76fdf14..8817321 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -8,8 +8,8 @@ use crate::state::{AppState, ConnectionAction, RoomInit, RoomUpdate}; use crate::utils::current_time_millis; use crate::writer::{TcpWriter, UdpWriter}; use anyhow::anyhow; -use bytes::{Buf, BufMut, Bytes, BytesMut}; -use std::io::Cursor; +use bytes::{Buf, Bytes, BytesMut}; +use std::io::{Cursor, IoSlice}; use std::sync::Arc; use std::time::{Duration, Instant}; use tokio::io::AsyncReadExt; @@ -45,21 +45,20 @@ impl ConnectionActor { self.notify_idle(); let mut buf = BytesMut::with_capacity(TCP_BUFFER_SIZE); - let mut tmp_buf = [0u8; TCP_BUFFER_SIZE]; let mut tick_interval = tokio::time::interval(Duration::from_millis(TICK_INTERVAL_MS)); loop { - let mut batch = BytesMut::new(); + let mut headers: Vec<[u8; 2]> = Vec::with_capacity(16); + let mut payloads: Vec = Vec::with_capacity(16); tokio::select! { - read_result = reader.read(&mut tmp_buf) => { + read_result = reader.read_buf(&mut buf) => { match read_result { Ok(0) => return Err(anyhow::anyhow!("Connection closed by peer")), - Ok(n) => { + Ok(_n) => { self.last_read = Instant::now(); self.notified_idle = false; - buf.extend_from_slice(&tmp_buf[..n]); self.process_tcp_buffer(&mut buf).await?; } Err(e) => return Err(e.into()), @@ -68,14 +67,19 @@ impl ConnectionActor { action = self.rx.recv() => { if let Some(action) = action { - self.handle_action(action, &mut batch).await?; + self.handle_action(action, &mut headers, &mut payloads).await?; while let Ok(action) = self.rx.try_recv() { - self.handle_action(action, &mut batch).await?; + self.handle_action(action, &mut headers, &mut payloads).await?; } - if !batch.is_empty() { - self.tcp_writer.write(&batch).await?; + if !payloads.is_empty() { + let mut slices = Vec::with_capacity(headers.len() * 2); + for (header, payload) in headers.iter().zip(payloads.iter()) { + slices.push(IoSlice::new(header)); + slices.push(IoSlice::new(payload)); + } + self.tcp_writer.write_vectored(&slices).await?; } } else { return Err(anyhow::anyhow!("Connection closed by peer, no action")); @@ -111,6 +115,12 @@ impl ConnectionActor { cur.get_u16() as usize }; + // Move rate limit check before parsing + if !self.limiter.check() { + warn!("Connection {} rate limit exceeded", self.id); + return Err(anyhow::anyhow!("Rate limit exceeded")); + } + if buf.len() < PACKET_LENGTH_LENGTH + len { break; } @@ -143,23 +153,21 @@ impl ConnectionActor { let is_framework = matches!(packet, AnyPacket::Framework(_)); if !is_framework { - let room_id_opt = self.state.rooms.find_connection_room_id(self.id); + let room_id_opt = self.state.room_state.find_connection_room_id(self.id); let is_host = if let Some(ref room_id) = room_id_opt { - if let Some(rooms) = self.state.rooms.read() { - rooms - .get(room_id) - .map(|r| r.host_connection_id == self.id) - .unwrap_or(false) - } else { - false - } + self.state + .room_state + .rooms + .get(room_id) + .map(|r| r.host_connection_id == self.id) + .unwrap_or(false) } else { false }; if !is_host && !self.limiter.check() { if let Some(ref room_id) = room_id_opt { - self.state.rooms.broadcast( + self.state.room_state.broadcast( room_id, ConnectionAction::SendTCP(AnyPacket::App(AppPacket::Message2( Message2Packet { @@ -187,7 +195,7 @@ impl ConnectionActor { AnyPacket::Framework(f) => self.handle_framework(f).await?, AnyPacket::App(a) => self.handle_app(a).await?, AnyPacket::Raw(bytes) => { - if let Some(room_id) = self.state.rooms.find_connection_room_id(self.id) { + if let Some(room_id) = self.state.room_state.find_connection_room_id(self.id) { let packet = AnyPacket::App(AppPacket::ConnectionPacketWrap( ConnectionPacketWrapPacket { connection_id: self.id, @@ -197,7 +205,7 @@ impl ConnectionActor { )); self.state - .rooms + .room_state .forward_to_host(&room_id, ConnectionAction::SendTCP(packet)); } else { if self.packet_queue.len() < 16 { @@ -237,37 +245,39 @@ impl ConnectionActor { async fn handle_app(&mut self, packet: AppPacket) -> anyhow::Result<()> { match packet { AppPacket::Stats(p) => { - if let Some(room_id) = self.state.rooms.find_connection_room_id(self.id) { - if let Ok(mut rooms) = self.state.rooms.rooms.write() { - if let Some(room) = rooms.get_mut(&room_id) { - let sent_at = p.data.created_at; - - room.stats = p.data; - room.updated_at = current_time_millis(); - room.ping = current_time_millis() - sent_at; - - if let Err(err) = - self.state.rooms.broadcast_sender.send(RoomUpdate::Update { + if let Some(room_id) = self.state.room_state.find_connection_room_id(self.id) { + if let Some(mut room) = self.state.room_state.rooms.get_mut(&room_id) { + let sent_at = p.data.created_at; + + room.stats = p.data; + room.updated_at = current_time_millis(); + room.ping = current_time_millis() - sent_at; + + if let Err(err) = + self.state + .room_state + .broadcast_sender + .send(RoomUpdate::Update { id: room.id.clone(), data: room.clone(), }) - { - warn!("Fail to broadcast room update {}", err); - } + { + warn!("Fail to broadcast room update {}", err); } } } } AppPacket::RoomJoin(p) => { - if let Some(current_room_id) = self.state.rooms.find_connection_room_id(self.id) { - let is_host = if let Some(rooms) = self.state.rooms.read() { - rooms - .get(¤t_room_id) - .map(|r| r.host_connection_id == self.id) - .unwrap_or(false) - } else { - false - }; + if let Some(current_room_id) = + self.state.room_state.find_connection_room_id(self.id) + { + let is_host = self + .state + .room_state + .rooms + .get(¤t_room_id) + .map(|r| r.host_connection_id == self.id) + .unwrap_or(false); if is_host { self.write_packet(AnyPacket::App(AppPacket::Message2(Message2Packet { @@ -286,12 +296,11 @@ impl ConnectionActor { "Connection {} left room {} to join {}", self.id, current_room_id, p.room_id ); - self.state.rooms.leave(self.id); + self.state.room_state.leave(self.id); } let (can_join, wrong_password) = (|| { - let rooms = self.state.rooms.read()?; - let room = rooms.get(&p.room_id)?; + let room = self.state.room_state.rooms.get(&p.room_id)?; if let Some(ref pass) = room.password { if pass != &p.password { @@ -336,12 +345,12 @@ impl ConnectionActor { } if let Some(sender) = self.state.get_sender(self.id) { - self.state.rooms.join(self.id, &p.room_id, sender)?; + self.state.room_state.join(self.id, &p.room_id, sender)?; info!("Connection {} joined the room {}.", self.id, p.room_id); for bytes in self.packet_queue.drain(..) { - self.state.rooms.forward_to_host( + self.state.room_state.forward_to_host( &p.room_id, ConnectionAction::SendTCP(AnyPacket::App( AppPacket::ConnectionPacketWrap(ConnectionPacketWrapPacket { @@ -355,7 +364,9 @@ impl ConnectionActor { } } AppPacket::RoomCreationRequest(p) => { - if let Some(current_room_id) = self.state.rooms.find_connection_room_id(self.id) { + if let Some(current_room_id) = + self.state.room_state.find_connection_room_id(self.id) + { self.write_packet(AnyPacket::App(AppPacket::Message2(Message2Packet { message: MessageType::AlreadyHosting, }))) @@ -368,7 +379,7 @@ impl ConnectionActor { } if let Some(sender) = self.state.get_sender(self.id) { - let room_id = self.state.rooms.create(RoomInit { + let room_id = self.state.room_state.create(RoomInit { connection_id: self.id, password: p.password, stats: p.data, @@ -380,18 +391,19 @@ impl ConnectionActor { }))) .await?; - let Some(rooms) = self.state.rooms.read() else { - return Err(anyhow!("Can not read rooms")); - }; - - let Some(room) = rooms.get(&room_id) else { + let Some(room) = self.state.room_state.rooms.get(&room_id) else { return Err(anyhow!("Can not find room {}", room_id)); }; - if let Err(err) = self.state.rooms.broadcast_sender.send(RoomUpdate::Update { - id: room.id.clone(), - data: room.clone(), - }) { + if let Err(err) = + self.state + .room_state + .broadcast_sender + .send(RoomUpdate::Update { + id: room.id.clone(), + data: room.clone(), + }) + { warn!("Fail to broadcast room update {}", err); } @@ -399,15 +411,14 @@ impl ConnectionActor { } } AppPacket::RoomClosureRequest(_) => { - if let Some(room_id) = self.state.rooms.find_connection_room_id(self.id) { - let is_host = if let Some(rooms) = self.state.rooms.read() { - rooms - .get(&room_id) - .map(|r| r.host_connection_id == self.id) - .unwrap_or(false) - } else { - false - }; + if let Some(room_id) = self.state.room_state.find_connection_room_id(self.id) { + let is_host = self + .state + .room_state + .rooms + .get(&room_id) + .map(|r| r.host_connection_id == self.id) + .unwrap_or(false); if !is_host { self.write_packet(AnyPacket::App(AppPacket::Message2(Message2Packet { @@ -421,7 +432,7 @@ impl ConnectionActor { return Ok(()); } - let members = self.state.rooms.get_room_members(&room_id); + let members = self.state.room_state.get_room_members(&room_id); for (id, sender) in members { if id != self.id { if let Err(e) = sender.try_send(ConnectionAction::SendTCP( @@ -437,7 +448,7 @@ impl ConnectionActor { } } - self.state.rooms.close(&room_id); + self.state.room_state.close(&room_id); info!( "Room {} closed by connection {} (the host).", room_id, self.id @@ -445,15 +456,14 @@ impl ConnectionActor { } } AppPacket::ConnectionClosed(p) => { - if let Some(room_id) = self.state.rooms.find_connection_room_id(self.id) { - let is_host = if let Some(rooms) = self.state.rooms.read() { - rooms - .get(&room_id) - .map(|r| r.host_connection_id == self.id) - .unwrap_or(false) - } else { - false - }; + if let Some(room_id) = self.state.room_state.find_connection_room_id(self.id) { + let is_host = self + .state + .room_state + .rooms + .get(&room_id) + .map(|r| r.host_connection_id == self.id) + .unwrap_or(false); if !is_host { self.write_packet(AnyPacket::App(AppPacket::Message2(Message2Packet { @@ -465,7 +475,10 @@ impl ConnectionActor { } if let Some(sender) = self.state.get_sender(p.connection_id) { - let target_room = self.state.rooms.find_connection_room_id(p.connection_id); + let target_room = self + .state + .room_state + .find_connection_room_id(p.connection_id); if target_room.as_ref() == Some(&room_id) { info!( "Connection {} (room {}) closed the connection {}.", @@ -499,42 +512,43 @@ impl ConnectionActor { is_tcp, buffer, }) => { - if let Some(room_id) = self.state.rooms.find_connection_room_id(self.id) { - if let Some(rooms) = self.state.rooms.read() { - let is_owner = rooms - .get(&room_id) - .map(|r| r.host_connection_id == self.id) - .unwrap_or(false); - - if !is_owner { - return Err(anyhow!("Not room owner")); - } + if let Some(room_id) = self.state.room_state.find_connection_room_id(self.id) { + let is_owner = self + .state + .room_state + .rooms + .get(&room_id) + .map(|r| r.host_connection_id == self.id) + .unwrap_or(false); - let Some(sender) = self.state.get_sender(connection_id) else { - warn!("Connection not found: {}", connection_id); + if !is_owner { + return Err(anyhow!("Not room owner")); + } - self.state.rooms.forward_to_host( - &room_id, - ConnectionAction::SendTCP(AnyPacket::App( - AppPacket::ConnectionClosed(ConnectionClosedPacket { - connection_id, - reason: ArcCloseReason::Closed, - }), - )), - ); + let Some(sender) = self.state.get_sender(connection_id) else { + warn!("Connection not found: {}", connection_id); + + self.state.room_state.forward_to_host( + &room_id, + ConnectionAction::SendTCP(AnyPacket::App(AppPacket::ConnectionClosed( + ConnectionClosedPacket { + connection_id, + reason: ArcCloseReason::Closed, + }, + ))), + ); - return Ok(()); - }; + return Ok(()); + }; - let action = if is_tcp { - ConnectionAction::SendTCPRaw(buffer) - } else { - ConnectionAction::SendUDPRaw(buffer) - }; + let action = if is_tcp { + ConnectionAction::SendTCPRaw(buffer) + } else { + ConnectionAction::SendUDPRaw(buffer) + }; - if let Err(e) = sender.try_send(action) { - warn!("Failed to forward packet to {}: {}", connection_id, e); - } + if let Err(e) = sender.try_send(action) { + warn!("Failed to forward packet to {}: {}", connection_id, e); } } else { warn!("No room found for connection {}", self.id); @@ -550,16 +564,21 @@ impl ConnectionActor { async fn handle_action( &mut self, action: ConnectionAction, - batch: &mut BytesMut, + headers: &mut Vec<[u8; 2]>, + payloads: &mut Vec, ) -> anyhow::Result<()> { match action { ConnectionAction::SendTCP(p) => { - let bytes = p.to_bytes(); - batch.extend_from_slice(&ConnectionActor::prepend_len(bytes.freeze())); + let bytes = p.to_bytes().freeze(); + let len = bytes.len() as u16; + headers.push(len.to_be_bytes()); + payloads.push(bytes); } ConnectionAction::SendTCPRaw(b) => { self.notified_idle = false; - batch.extend_from_slice(&ConnectionActor::prepend_len(b)); + let len = b.len() as u16; + headers.push(len.to_be_bytes()); + payloads.push(b); } ConnectionAction::SendUDPRaw(b) => { self.notified_idle = false; @@ -601,19 +620,11 @@ impl ConnectionActor { Ok(()) } - pub fn prepend_len(payload: Bytes) -> BytesMut { - let mut out: BytesMut = BytesMut::with_capacity(2 + payload.len()); - - out.put_u16(payload.len() as u16); - out.extend_from_slice(&payload); - - out - } - async fn write_packet(&mut self, packet: AnyPacket) -> anyhow::Result<()> { - self.tcp_writer - .write(&ConnectionActor::prepend_len(packet.to_bytes().freeze())) - .await + let bytes = packet.to_bytes().freeze(); + let len = (bytes.len() as u16).to_be_bytes(); + let slices = [IoSlice::new(&len), IoSlice::new(&bytes)]; + self.tcp_writer.write_vectored(&slices).await } fn is_idle(&self) -> bool { diff --git a/src/http.rs b/src/http.rs index de6e2b5..3488044 100644 --- a/src/http.rs +++ b/src/http.rs @@ -38,24 +38,18 @@ async fn ping() -> impl IntoResponse { } async fn rooms_sse(State(state): State>) -> impl IntoResponse { - let rx = state.rooms.broadcast_sender.subscribe(); + let rx = state.room_state.broadcast_sender.subscribe(); let stream = BroadcastStream::new(rx); - let initial_rooms: Vec = { - let rooms = state.rooms.read(); - - if let Some(rooms) = rooms { - rooms - .iter() - .map(|(key, room)| RoomUpdateEvent { - room_id: key.0.clone(), - data: RoomView::from(room), - }) - .collect() - } else { - vec![] - } - }; + let initial_rooms: Vec = state + .room_state + .rooms + .iter() + .map(|entry| RoomUpdateEvent { + room_id: entry.key().0.clone(), + data: RoomView::from(entry.value()), + }) + .collect(); let init_stream = once(async move { Event::default() @@ -101,13 +95,11 @@ async fn room_page( Path(room_id): Path, State(state): State>, ) -> impl IntoResponse { - let stats = { - if let Some(rooms) = state.rooms.read() { - rooms.get(&RoomId(room_id)).map(|r| r.stats.clone()) - } else { - None - } - }; + let stats = state + .room_state + .rooms + .get(&RoomId(room_id)) + .map(|r| r.stats.clone()); if let Some(stats) = stats { let html = format!( diff --git a/src/proxy.rs b/src/proxy.rs index 10a2339..2dda813 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -3,7 +3,7 @@ use crate::packet::{AnyPacket, FrameworkMessage}; use crate::rate::AtomicRateLimiter; use crate::state::{AppState, ConnectionAction}; use crate::writer::{TcpWriter, UdpWriter}; -use bytes::Bytes; +use bytes::BytesMut; use std::io::Cursor; use std::net::SocketAddr; use std::sync::Arc; @@ -35,13 +35,30 @@ pub async fn run(state: Arc, port: u16) -> anyhow::Result<()> { fn spawn_udp_listener(state: Arc, socket: Arc) { tokio::spawn(async move { - let mut buf = [0u8; UDP_BUFFER_SIZE]; + let mut buf = BytesMut::with_capacity(UDP_BUFFER_SIZE); loop { - match socket.recv_from(&mut buf).await { - Ok((len, addr)) => { - let data = &buf[..len]; - let mut cursor = Cursor::new(Bytes::copy_from_slice(data)); + if buf.len() > 0 { + buf.clear(); + } + // BytesMut logic: if we froze the previous buffer, clear() might not free memory if shared. + // But we need to ensure we have capacity. + if buf.capacity() < UDP_BUFFER_SIZE { + buf.reserve(UDP_BUFFER_SIZE); + } + + match socket.recv_buf_from(&mut buf).await { + Ok((_len, addr)) => { + // Check rate limit early + if let Some((_, limiter)) = state.get_route(&addr) { + if !limiter.check() { + // Rate limit exceeded, drop packet + continue; + } + } + + let bytes = buf.split().freeze(); + let mut cursor = Cursor::new(bytes); match AnyPacket::read(&mut cursor) { Ok(packet) => { diff --git a/src/state.rs b/src/state.rs index 7305936..b217b10 100644 --- a/src/state.rs +++ b/src/state.rs @@ -9,9 +9,10 @@ use crate::packet::{ use crate::rate::AtomicRateLimiter; use crate::utils::current_time_millis; use bytes::Bytes; +use dashmap::DashMap; use std::collections::HashMap; use std::net::SocketAddr; -use std::sync::{Arc, RwLock, RwLockReadGuard}; +use std::sync::Arc; use tokio::sync::mpsc; use tracing::{error, info, warn}; use uuid::Uuid; @@ -45,8 +46,9 @@ pub enum RoomUpdate { Remove(RoomId), } -pub struct Rooms { - pub rooms: RwLock>, +pub struct RoomState { + pub rooms: DashMap, + pub connection_to_room: DashMap, pub broadcast_sender: tokio::sync::broadcast::Sender, // Keep a receiver to prevent the channel from closing when all clients disconnect pub _broadcast_receiver: tokio::sync::broadcast::Receiver, @@ -60,28 +62,23 @@ pub struct RoomInit { pub protocol_version: String, } -impl Rooms { +impl RoomState { pub fn get_sender( &self, room_id: &RoomId, connection_id: ConnectionId, ) -> Option> { - let rooms = self.rooms.read().ok()?; - - rooms.get(room_id)?.members.get(&connection_id).cloned() + self.rooms + .get(room_id)? + .members + .get(&connection_id) + .cloned() } pub fn find_connection_room_id(&self, connection_id: ConnectionId) -> Option { - let rooms = self.rooms.read().ok()?; - - rooms - .iter() - .find(|(_, room)| room.members.contains_key(&connection_id)) - .map(|(id, _)| id.clone()) - } - - pub fn read(&self) -> Option>> { - self.rooms.read().ok() + self.connection_to_room + .get(&connection_id) + .map(|r| r.clone()) } pub fn join( @@ -90,10 +87,10 @@ impl Rooms { room_id: &RoomId, sender: mpsc::Sender, ) -> Result<(), AppError> { - let mut rooms = self.rooms.write().map_err(|_| AppError::LockPoison)?; - - if let Some(room) = rooms.get_mut(room_id) { + if let Some(mut room) = self.rooms.get_mut(room_id) { room.members.insert(connection_id, sender); + self.connection_to_room + .insert(connection_id, room_id.clone()); let sender = match room.members.get(&room.host_connection_id) { Some(sender) => sender, @@ -124,14 +121,9 @@ impl Rooms { } pub fn leave(&self, connection_id: ConnectionId) -> Option { - let mut rooms = self.rooms.write().ok()?; - - let room_id = rooms - .iter() - .find(|(_, room)| room.members.contains_key(&connection_id)) - .map(|(id, _)| id.clone())?; + let (_, room_id) = self.connection_to_room.remove(&connection_id)?; - if let Some(room) = rooms.get_mut(&room_id) { + if let Some(mut room) = self.rooms.get_mut(&room_id) { room.members.remove(&connection_id); let sender = match room.members.get(&room.host_connection_id) { @@ -193,23 +185,22 @@ impl Rooms { ping: 0, }; - if let Ok(mut rooms) = self.rooms.write() { - rooms.insert(room_id.clone(), room); - } + self.rooms.insert(room_id.clone(), room); + self.connection_to_room + .insert(connection_id, room_id.clone()); room_id } pub fn close(&self, room_id: &RoomId) { - let removed = { - if let Ok(mut rooms) = self.rooms.write() { - rooms.remove(room_id) - } else { - None + let removed = self.rooms.remove(room_id); + + if let Some((_, room)) = removed { + // Cleanup connection_to_room map + for conn_id in room.members.keys() { + self.connection_to_room.remove(conn_id); } - }; - if removed.is_some() { info!("Room closed {}", room_id); if let Err(err) = self @@ -227,30 +218,20 @@ impl Rooms { action: ConnectionAction, exclude_id: Option, ) { - if let Ok(rooms) = self.rooms.read() { - if let Some(room) = rooms.get(room_id) { - for (id, sender) in &room.members { - if Some(*id) == exclude_id { - continue; - } - if let Err(e) = sender.try_send(action.clone()) { - warn!("Failed to broadcast to {}: {}", id, e); - } + if let Some(room) = self.rooms.get(room_id) { + for (id, sender) in &room.members { + if Some(*id) == exclude_id { + continue; + } + if let Err(e) = sender.try_send(action.clone()) { + warn!("Failed to broadcast to {}: {}", id, e); } } } } pub fn forward_to_host(&self, room_id: &RoomId, action: ConnectionAction) { - let rooms = match self.rooms.read() { - Ok(rooms) => rooms, - Err(e) => { - error!("Failed to acquire rooms read lock: {}", e); - return; - } - }; - - let room = match rooms.get(room_id) { + let room = match self.rooms.get(room_id) { Some(room) => room, None => { warn!("Room {} not found for forwarding", room_id); @@ -281,15 +262,7 @@ impl Rooms { &self, room_id: &RoomId, ) -> Vec<(ConnectionId, mpsc::Sender)> { - let rooms = match self.rooms.read() { - Ok(rooms) => rooms, - Err(e) => { - error!("Failed to acquire rooms read lock: {}", e); - return Vec::new(); - } - }; - - if let Some(room) = rooms.get(room_id) { + if let Some(room) = self.rooms.get(room_id) { room.members.iter().map(|(k, v)| (*k, v.clone())).collect() } else { warn!("Room {} not found for getting members", room_id); @@ -298,19 +271,12 @@ impl Rooms { } pub fn idle(&self, connection_id: ConnectionId) -> bool { - let rooms = match self.rooms.read() { - Ok(rooms) => rooms, - Err(err) => { - error!("Failed to acquire rooms read lock: {}", err); - return true; - } + let room_id = match self.connection_to_room.get(&connection_id) { + Some(id) => id.clone(), + None => return true, }; - if let Some(room) = rooms - .iter() - .find(|(_, room)| room.members.contains_key(&connection_id)) - .map(|(_, room)| room) - { + if let Some(room) = self.rooms.get(&room_id) { if room.host_connection_id == connection_id { return true; } @@ -362,11 +328,10 @@ impl From<&Room> for RoomView { pub struct AppState { pub config: Config, - pub rooms: Rooms, + pub room_state: RoomState, pub connections: - RwLock, Arc)>>, - pub udp_routes: - RwLock, Arc)>>, + DashMap, Arc)>, + pub udp_routes: DashMap, Arc)>, } impl AppState { @@ -374,13 +339,14 @@ impl AppState { let (tx, rx) = tokio::sync::broadcast::channel(1024); Self { config, - rooms: Rooms { - rooms: RwLock::new(HashMap::new()), + room_state: RoomState { + rooms: DashMap::new(), + connection_to_room: DashMap::new(), broadcast_sender: tx, _broadcast_receiver: rx, }, - connections: RwLock::new(HashMap::new()), - udp_routes: RwLock::new(HashMap::new()), + connections: DashMap::new(), + udp_routes: DashMap::new(), } } @@ -390,22 +356,11 @@ impl AppState { sender: mpsc::Sender, limiter: Arc, ) { - match self.connections.write() { - Ok(mut conns) => { - conns.insert(id, (sender, limiter)); - } - Err(err) => { - error!("{}", err) - } - } + self.connections.insert(id, (sender, limiter)); } pub fn has_connection_id(&self, id: ConnectionId) -> bool { - self.connections - .read() - .ok() - .map(|conns| conns.contains_key(&id)) - .unwrap_or(false) + self.connections.contains_key(&id) } pub fn register_udp( @@ -414,52 +369,39 @@ impl AppState { sender: mpsc::Sender, limiter: Arc, ) { - if let Ok(mut routes) = self.udp_routes.write() { - routes.insert(addr, (sender, limiter)); - } + self.udp_routes.insert(addr, (sender, limiter)); } pub fn remove_udp(&self, addr: SocketAddr) { - if let Ok(mut routes) = self.udp_routes.write() { - routes.remove(&addr); - } + self.udp_routes.remove(&addr); } pub fn get_sender(&self, id: ConnectionId) -> Option> { - self.connections - .read() - .ok()? - .get(&id) - .map(|(s, _)| s.clone()) + self.connections.get(&id).map(|val| val.0.clone()) } pub fn get_route( &self, addr: &SocketAddr, ) -> Option<(mpsc::Sender, Arc)> { - self.udp_routes.read().ok()?.get(addr).cloned() + self.udp_routes.get(addr).map(|val| val.clone()) } pub fn idle(&self, connection_id: ConnectionId) -> bool { - self.rooms.idle(connection_id) + self.room_state.idle(connection_id) } pub fn remove_connection(&self, connection_id: ConnectionId) { - if let Ok(mut conns) = self.connections.write() { - conns.remove(&connection_id); - } + self.connections.remove(&connection_id); + // Handle room logic - let room_id_opt = self.rooms.leave(connection_id); + let room_id_opt = self.room_state.leave(connection_id); if let Some(room_id) = room_id_opt { // Check if host let should_close = { - if let Ok(rooms) = self.rooms.rooms.read() { - if let Some(room) = rooms.get(&room_id) { - room.host_connection_id == connection_id - } else { - false - } + if let Some(room) = self.room_state.rooms.get(&room_id) { + room.host_connection_id == connection_id } else { false } @@ -468,12 +410,8 @@ impl AppState { if should_close { // Close room and disconnect all members let members = { - if let Ok(rooms) = self.rooms.rooms.read() { - if let Some(room) = rooms.get(&room_id) { - room.members.values().cloned().collect::>() - } else { - Vec::new() - } + if let Some(room) = self.room_state.rooms.get(&room_id) { + room.members.values().cloned().collect::>() } else { Vec::new() } @@ -485,7 +423,7 @@ impl AppState { } } - self.rooms.close(&room_id); + self.room_state.close(&room_id); } } } diff --git a/src/writer.rs b/src/writer.rs index bfa03be..22910a5 100644 --- a/src/writer.rs +++ b/src/writer.rs @@ -1,4 +1,5 @@ use anyhow::anyhow; +use std::io::IoSlice; use std::net::SocketAddr; use std::sync::Arc; use std::time::Instant; @@ -23,6 +24,12 @@ impl TcpWriter { self.last_write = Instant::now(); Ok(()) } + + pub async fn write_vectored(&mut self, bufs: &[IoSlice<'_>]) -> anyhow::Result<()> { + self.writer.write_vectored(bufs).await?; + self.last_write = Instant::now(); + Ok(()) + } } pub struct UdpWriter { From b07e27b530c3719d42cf0a3285678042e1d9cadd Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Fri, 23 Jan 2026 04:27:48 +0700 Subject: [PATCH 093/115] perf(packet): remove debug logging from packet parsing Eliminate an info-level log statement that was executed on every packet read. This improves performance by reducing I/O overhead in hot code paths. --- src/packet.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/packet.rs b/src/packet.rs index 75a37c0..df8f3e7 100644 --- a/src/packet.rs +++ b/src/packet.rs @@ -233,8 +233,6 @@ impl AppPacket { pub fn read(buf: &mut Cursor) -> Result { let pid = buf.get_u8(); - info!("Read AppPacket with ID: {}", pid); - match pid { 0 => { let connection_id = ConnectionId(buf.get_u32()); From 4d7b4188a7795a46493d0071d07400a11a242b2b Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Fri, 23 Jan 2026 15:34:53 +0700 Subject: [PATCH 094/115] refactor(room): simplify room membership tracking and cleanup logic - Store room membership directly in ConnectionActor to avoid separate lookup map - Remove connection_to_room map and use direct room references - Send room closed packets to all members when closing a room - Fix idle detection to use stored room information - Ensure proper cleanup when connections leave rooms --- .trae/rules/base.md | 2 + src/connection.rs | 174 +++++++++++++++++++++----------------------- src/packet.rs | 2 - src/proxy.rs | 9 +-- src/state.rs | 89 +++++++++------------- 5 files changed, 123 insertions(+), 153 deletions(-) diff --git a/.trae/rules/base.md b/.trae/rules/base.md index 744f66f..0078b75 100644 --- a/.trae/rules/base.md +++ b/.trae/rules/base.md @@ -111,3 +111,5 @@ One async runtime Do not mix Tokio + async-std Use channels instead of shared mutable state + +8. Run cargo check before committing, and fix all errors diff --git a/src/connection.rs b/src/connection.rs index 8817321..a30e213 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -1,7 +1,7 @@ -use crate::constant::{ArcCloseReason, CloseReason, MessageType}; +use crate::constant::{ArcCloseReason, MessageType}; use crate::packet::{ AnyPacket, AppPacket, ConnectionClosedPacket, ConnectionId, ConnectionPacketWrapPacket, - FrameworkMessage, Message2Packet, MessagePacket, PopupPacket, RoomClosedPacket, RoomLinkPacket, + FrameworkMessage, Message2Packet, MessagePacket, PopupPacket, RoomId, RoomLinkPacket, }; use crate::rate::AtomicRateLimiter; use crate::state::{AppState, ConnectionAction, RoomInit, RoomUpdate}; @@ -22,6 +22,21 @@ const KEEP_ALIVE_INTERVAL_MS: Duration = Duration::from_millis(3000); const PACKET_LENGTH_LENGTH: usize = 2; const TICK_INTERVAL_MS: u64 = 1000 / 60; +pub struct ConnectionRoom { + room_id: RoomId, + is_host: bool, +} + +impl ConnectionRoom { + pub fn room_id(&self) -> &RoomId { + &self.room_id + } + + pub fn is_host(&self) -> bool { + self.is_host + } +} + pub struct ConnectionActor { pub id: ConnectionId, pub state: Arc, @@ -32,6 +47,7 @@ pub struct ConnectionActor { pub last_read: Instant, pub packet_queue: Vec, pub notified_idle: bool, + pub room: Option, } impl ConnectionActor { @@ -89,7 +105,9 @@ impl ConnectionActor { _ = tick_interval.tick() => { if self.is_idle() && !self.notified_idle { self.notify_idle(); - } else if self.tcp_writer.last_write.elapsed() > KEEP_ALIVE_INTERVAL_MS { + } + + if self.tcp_writer.last_write.elapsed() > KEEP_ALIVE_INTERVAL_MS { self.write_packet(AnyPacket::Framework(FrameworkMessage::KeepAlive)).await?; } @@ -115,7 +133,6 @@ impl ConnectionActor { cur.get_u16() as usize }; - // Move rate limit check before parsing if !self.limiter.check() { warn!("Connection {} rate limit exceeded", self.id); return Err(anyhow::anyhow!("Rate limit exceeded")); @@ -153,22 +170,12 @@ impl ConnectionActor { let is_framework = matches!(packet, AnyPacket::Framework(_)); if !is_framework { - let room_id_opt = self.state.room_state.find_connection_room_id(self.id); - let is_host = if let Some(ref room_id) = room_id_opt { - self.state - .room_state - .rooms - .get(room_id) - .map(|r| r.host_connection_id == self.id) - .unwrap_or(false) - } else { - false - }; + let is_host = self.room.as_ref().map(|r| r.is_host).unwrap_or(false); if !is_host && !self.limiter.check() { - if let Some(ref room_id) = room_id_opt { + if let Some(ref room) = self.room { self.state.room_state.broadcast( - room_id, + &room.room_id, ConnectionAction::SendTCP(AnyPacket::App(AppPacket::Message2( Message2Packet { message: MessageType::PacketSpamming, @@ -195,7 +202,7 @@ impl ConnectionActor { AnyPacket::Framework(f) => self.handle_framework(f).await?, AnyPacket::App(a) => self.handle_app(a).await?, AnyPacket::Raw(bytes) => { - if let Some(room_id) = self.state.room_state.find_connection_room_id(self.id) { + if let Some(ref room) = self.room { let packet = AnyPacket::App(AppPacket::ConnectionPacketWrap( ConnectionPacketWrapPacket { connection_id: self.id, @@ -206,7 +213,7 @@ impl ConnectionActor { self.state .room_state - .forward_to_host(&room_id, ConnectionAction::SendTCP(packet)); + .forward_to_host(&room.room_id, ConnectionAction::SendTCP(packet)); } else { if self.packet_queue.len() < 16 { self.packet_queue.push(bytes); @@ -245,21 +252,29 @@ impl ConnectionActor { async fn handle_app(&mut self, packet: AppPacket) -> anyhow::Result<()> { match packet { AppPacket::Stats(p) => { - if let Some(room_id) = self.state.room_state.find_connection_room_id(self.id) { - if let Some(mut room) = self.state.room_state.rooms.get_mut(&room_id) { + if let Some(ref room) = self.room { + if let Some(mut r) = self.state.room_state.rooms.get_mut(&room.room_id) { + if !room.is_host { + warn!( + "Connection {} tried to update stats but is not host", + self.id + ); + return Ok(()); + } + let sent_at = p.data.created_at; - room.stats = p.data; - room.updated_at = current_time_millis(); - room.ping = current_time_millis() - sent_at; + r.stats = p.data; + r.updated_at = current_time_millis(); + r.ping = current_time_millis() - sent_at; if let Err(err) = self.state .room_state .broadcast_sender .send(RoomUpdate::Update { - id: room.id.clone(), - data: room.clone(), + id: r.id.clone(), + data: r.clone(), }) { warn!("Fail to broadcast room update {}", err); @@ -268,17 +283,9 @@ impl ConnectionActor { } } AppPacket::RoomJoin(p) => { - if let Some(current_room_id) = - self.state.room_state.find_connection_room_id(self.id) + if let Some((current_room_id, is_host)) = + self.room.as_ref().map(|r| (r.room_id.clone(), r.is_host)) { - let is_host = self - .state - .room_state - .rooms - .get(¤t_room_id) - .map(|r| r.host_connection_id == self.id) - .unwrap_or(false); - if is_host { self.write_packet(AnyPacket::App(AppPacket::Message2(Message2Packet { message: MessageType::AlreadyHosting, @@ -296,7 +303,8 @@ impl ConnectionActor { "Connection {} left room {} to join {}", self.id, current_room_id, p.room_id ); - self.state.room_state.leave(self.id); + self.state.room_state.leave(self.id, ¤t_room_id); + self.room = None; } let (can_join, wrong_password) = (|| { @@ -346,6 +354,10 @@ impl ConnectionActor { if let Some(sender) = self.state.get_sender(self.id) { self.state.room_state.join(self.id, &p.room_id, sender)?; + self.room = Some(ConnectionRoom { + room_id: p.room_id.clone(), + is_host: false, + }); info!("Connection {} joined the room {}.", self.id, p.room_id); @@ -364,16 +376,14 @@ impl ConnectionActor { } } AppPacket::RoomCreationRequest(p) => { - if let Some(current_room_id) = - self.state.room_state.find_connection_room_id(self.id) - { + if let Some(room_id) = self.room.as_ref().map(|r| r.room_id.clone()) { self.write_packet(AnyPacket::App(AppPacket::Message2(Message2Packet { message: MessageType::AlreadyHosting, }))) .await?; warn!( "Connection {} tried to create a room but is already hosting/in the room {}.", - self.id, current_room_id + self.id, room_id ); return Ok(()); } @@ -386,6 +396,11 @@ impl ConnectionActor { protocol_version: p.version, sender, }); + self.room = Some(ConnectionRoom { + room_id: room_id.clone(), + is_host: true, + }); + self.write_packet(AnyPacket::App(AppPacket::RoomLink(RoomLinkPacket { room_id: room_id.clone(), }))) @@ -411,15 +426,9 @@ impl ConnectionActor { } } AppPacket::RoomClosureRequest(_) => { - if let Some(room_id) = self.state.room_state.find_connection_room_id(self.id) { - let is_host = self - .state - .room_state - .rooms - .get(&room_id) - .map(|r| r.host_connection_id == self.id) - .unwrap_or(false); - + if let Some((room_id, is_host)) = + self.room.as_ref().map(|r| (r.room_id.clone(), r.is_host)) + { if !is_host { self.write_packet(AnyPacket::App(AppPacket::Message2(Message2Packet { message: MessageType::RoomClosureDenied, @@ -432,23 +441,9 @@ impl ConnectionActor { return Ok(()); } - let members = self.state.room_state.get_room_members(&room_id); - for (id, sender) in members { - if id != self.id { - if let Err(e) = sender.try_send(ConnectionAction::SendTCP( - AnyPacket::App(AppPacket::RoomClosed(RoomClosedPacket { - reason: CloseReason::Closed, - })), - )) { - warn!("Failed to send room closed packet to {}: {}", id, e); - } - if let Err(e) = sender.try_send(ConnectionAction::Close) { - warn!("Failed to send close action to {}: {}", id, e); - } - } - } - self.state.room_state.close(&room_id); + self.room = None; + info!( "Room {} closed by connection {} (the host).", room_id, self.id @@ -456,15 +451,9 @@ impl ConnectionActor { } } AppPacket::ConnectionClosed(p) => { - if let Some(room_id) = self.state.room_state.find_connection_room_id(self.id) { - let is_host = self - .state - .room_state - .rooms - .get(&room_id) - .map(|r| r.host_connection_id == self.id) - .unwrap_or(false); - + if let Some((room_id, is_host)) = + self.room.as_ref().map(|r| (r.room_id.clone(), r.is_host)) + { if !is_host { self.write_packet(AnyPacket::App(AppPacket::Message2(Message2Packet { message: MessageType::ConClosureDenied, @@ -475,11 +464,15 @@ impl ConnectionActor { } if let Some(sender) = self.state.get_sender(p.connection_id) { - let target_room = self + let is_in_room = self .state .room_state - .find_connection_room_id(p.connection_id); - if target_room.as_ref() == Some(&room_id) { + .rooms + .get(&room_id) + .map(|r| r.members.contains_key(&p.connection_id)) + .unwrap_or(false); + + if is_in_room { info!( "Connection {} (room {}) closed the connection {}.", self.id, room_id, p.connection_id @@ -502,7 +495,10 @@ impl ConnectionActor { warn!("Failed to send close action to {}: {}", p.connection_id, e); } } else { - warn!("Connection {} (room {}) tried to close a connection from another room.", self.id, room_id); + warn!( + "Connection {} (room {}) tried to close a connection from another room.", + self.id, room_id + ); } } } @@ -512,16 +508,10 @@ impl ConnectionActor { is_tcp, buffer, }) => { - if let Some(room_id) = self.state.room_state.find_connection_room_id(self.id) { - let is_owner = self - .state - .room_state - .rooms - .get(&room_id) - .map(|r| r.host_connection_id == self.id) - .unwrap_or(false); - - if !is_owner { + if let Some((room_id, is_host)) = + self.room.as_ref().map(|r| (r.room_id.clone(), r.is_host)) + { + if !is_host { return Err(anyhow!("Not room owner")); } @@ -632,7 +622,11 @@ impl ConnectionActor { } fn notify_idle(&mut self) { - if self.state.idle(self.id) { + let Some(room) = &self.room else { + return; + }; + + if self.state.idle(self.id, &room.room_id()) { self.notified_idle = true; } } diff --git a/src/packet.rs b/src/packet.rs index df8f3e7..1b26d74 100644 --- a/src/packet.rs +++ b/src/packet.rs @@ -2,7 +2,6 @@ use crate::constant::{ArcCloseReason, CloseReason, MessageType}; use crate::error::AppError; use crate::models::Stats; use bytes::{Buf, BufMut, Bytes, BytesMut}; -use tracing::info; use std::convert::TryFrom; use std::io::Cursor; @@ -397,7 +396,6 @@ pub fn read_stats(buf: &mut Cursor) -> Result { match serde_json::from_str::(&json) { Ok(data) => Ok(data), Err(e) => { - // Log? No, we return error. Err(AppError::PacketParsing(format!( "Failed to parse stats: {}. JSON: {}", e, json diff --git a/src/proxy.rs b/src/proxy.rs index 2dda813..d5e173f 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -25,7 +25,6 @@ pub async fn run(state: Arc, port: u16) -> anyhow::Result<()> { info!("Proxy Server listening on TCP/UDP {}", port); - // Spawn multiple UDP listeners for better throughput for _ in 0..4 { spawn_udp_listener(state.clone(), udp_socket.clone()); } @@ -41,18 +40,15 @@ fn spawn_udp_listener(state: Arc, socket: Arc) { if buf.len() > 0 { buf.clear(); } - // BytesMut logic: if we froze the previous buffer, clear() might not free memory if shared. - // But we need to ensure we have capacity. + if buf.capacity() < UDP_BUFFER_SIZE { buf.reserve(UDP_BUFFER_SIZE); } match socket.recv_buf_from(&mut buf).await { Ok((_len, addr)) => { - // Check rate limit early if let Some((_, limiter)) = state.get_route(&addr) { if !limiter.check() { - // Rate limit exceeded, drop packet continue; } } @@ -146,13 +142,14 @@ async fn accept_tcp_connection( last_read: Instant::now(), packet_queue: Vec::new(), notified_idle: false, + room: None, }; if let Err(e) = actor.run(reader).await { error!("Connection {} error: {}", id, e); } - state.remove_connection(id); + state.remove_connection(id, actor.room.as_ref().map(|r| r.room_id().clone())); if let Some(addr) = actor.udp_writer.addr { state.remove_udp(addr); diff --git a/src/state.rs b/src/state.rs index b217b10..2b8b52d 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,10 +1,10 @@ use crate::config::Config; -use crate::constant::ArcCloseReason; +use crate::constant::{ArcCloseReason, CloseReason}; use crate::error::AppError; use crate::models::{RoomView, Stats}; use crate::packet::{ AnyPacket, AppPacket, ConnectionClosedPacket, ConnectionId, ConnectionIdlingPacket, - ConnectionJoinPacket, RoomId, + ConnectionJoinPacket, RoomClosedPacket, RoomId, }; use crate::rate::AtomicRateLimiter; use crate::utils::current_time_millis; @@ -40,6 +40,12 @@ pub struct Room { pub protocol_version: String, } +impl Room { + pub fn is_host(&self, connection_id: ConnectionId) -> bool { + self.host_connection_id == connection_id + } +} + #[derive(Clone, Debug)] pub enum RoomUpdate { Update { id: RoomId, data: Room }, @@ -48,7 +54,6 @@ pub enum RoomUpdate { pub struct RoomState { pub rooms: DashMap, - pub connection_to_room: DashMap, pub broadcast_sender: tokio::sync::broadcast::Sender, // Keep a receiver to prevent the channel from closing when all clients disconnect pub _broadcast_receiver: tokio::sync::broadcast::Receiver, @@ -75,12 +80,6 @@ impl RoomState { .cloned() } - pub fn find_connection_room_id(&self, connection_id: ConnectionId) -> Option { - self.connection_to_room - .get(&connection_id) - .map(|r| r.clone()) - } - pub fn join( &self, connection_id: ConnectionId, @@ -89,8 +88,6 @@ impl RoomState { ) -> Result<(), AppError> { if let Some(mut room) = self.rooms.get_mut(room_id) { room.members.insert(connection_id, sender); - self.connection_to_room - .insert(connection_id, room_id.clone()); let sender = match room.members.get(&room.host_connection_id) { Some(sender) => sender, @@ -120,10 +117,8 @@ impl RoomState { Ok(()) } - pub fn leave(&self, connection_id: ConnectionId) -> Option { - let (_, room_id) = self.connection_to_room.remove(&connection_id)?; - - if let Some(mut room) = self.rooms.get_mut(&room_id) { + pub fn leave(&self, connection_id: ConnectionId, room_id: &RoomId) { + if let Some(mut room) = self.rooms.get_mut(room_id) { room.members.remove(&connection_id); let sender = match room.members.get(&room.host_connection_id) { @@ -133,7 +128,7 @@ impl RoomState { "Host {} not found in room {} while leaving", room.host_connection_id, room_id ); - return Some(room_id); + return; } }; @@ -149,8 +144,6 @@ impl RoomState { ); } } - - Some(room_id) } pub fn create(&self, init: RoomInit) -> RoomId { @@ -186,8 +179,6 @@ impl RoomState { }; self.rooms.insert(room_id.clone(), room); - self.connection_to_room - .insert(connection_id, room_id.clone()); room_id } @@ -196,13 +187,22 @@ impl RoomState { let removed = self.rooms.remove(room_id); if let Some((_, room)) = removed { - // Cleanup connection_to_room map - for conn_id in room.members.keys() { - self.connection_to_room.remove(conn_id); - } - info!("Room closed {}", room_id); + for (id, sender) in room.members { + if let Err(e) = sender.try_send(ConnectionAction::SendTCP(AnyPacket::App( + AppPacket::RoomClosed(RoomClosedPacket { + reason: CloseReason::Closed, + }), + ))) { + warn!("Failed to send room closed packet to {}: {}", id, e); + } + + if let Err(e) = sender.try_send(ConnectionAction::Close) { + warn!("Failed to send close action to {}: {}", id, e); + } + } + if let Err(err) = self .broadcast_sender .send(RoomUpdate::Remove(room_id.clone())) @@ -270,14 +270,9 @@ impl RoomState { } } - pub fn idle(&self, connection_id: ConnectionId) -> bool { - let room_id = match self.connection_to_room.get(&connection_id) { - Some(id) => id.clone(), - None => return true, - }; - - if let Some(room) = self.rooms.get(&room_id) { - if room.host_connection_id == connection_id { + pub fn idle(&self, connection_id: ConnectionId, room_id: &RoomId) -> bool { + if let Some(room) = self.rooms.get(room_id) { + if room.is_host(connection_id) { return true; } @@ -341,7 +336,6 @@ impl AppState { config, room_state: RoomState { rooms: DashMap::new(), - connection_to_room: DashMap::new(), broadcast_sender: tx, _broadcast_receiver: rx, }, @@ -387,42 +381,27 @@ impl AppState { self.udp_routes.get(addr).map(|val| val.clone()) } - pub fn idle(&self, connection_id: ConnectionId) -> bool { - self.room_state.idle(connection_id) + pub fn idle(&self, connection_id: ConnectionId, room_id: &RoomId) -> bool { + self.room_state.idle(connection_id, room_id) } - pub fn remove_connection(&self, connection_id: ConnectionId) { + pub fn remove_connection(&self, connection_id: ConnectionId, room_id: Option) { self.connections.remove(&connection_id); // Handle room logic - let room_id_opt = self.room_state.leave(connection_id); + if let Some(room_id) = room_id { + self.room_state.leave(connection_id, &room_id); - if let Some(room_id) = room_id_opt { // Check if host let should_close = { if let Some(room) = self.room_state.rooms.get(&room_id) { - room.host_connection_id == connection_id + room.is_host(connection_id) } else { false } }; if should_close { - // Close room and disconnect all members - let members = { - if let Some(room) = self.room_state.rooms.get(&room_id) { - room.members.values().cloned().collect::>() - } else { - Vec::new() - } - }; - - for sender in members { - if let Err(e) = sender.try_send(ConnectionAction::Close) { - warn!("Failed to send close action to member: {}", e); - } - } - self.room_state.close(&room_id); } } From d5758ec7787e22a57d5f8c87acbf69ddcba11e03 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Fri, 23 Jan 2026 16:29:06 +0700 Subject: [PATCH 095/115] fix: improve connection cleanup and error handling - Change connection error logging from error! to info! level for expected closures - Simplify timeout handling by returning error directly instead of breaking loop - Move UDP cleanup to RegisterUDP action to ensure proper state management - Reorder UDP registration to prevent orphaned UDP mappings - Remove redundant connection closed log when UDP cleanup occurs --- src/connection.rs | 21 +++++++++------------ src/proxy.rs | 3 +-- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/connection.rs b/src/connection.rs index a30e213..499ca8c 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -113,13 +113,11 @@ impl ConnectionActor { if self.last_read.elapsed() > CONNECTION_TIME_OUT_MS { info!("Connection {} timed out", self.id); - break; + return Err(anyhow::anyhow!("Connection timed out")); } } } } - - Ok(()) } async fn process_tcp_buffer(&mut self, buf: &mut BytesMut) -> anyhow::Result<()> { @@ -241,7 +239,7 @@ impl ConnectionActor { } } FrameworkMessage::KeepAlive => {} - FrameworkMessage::RegisterUDP { .. } => {} + FrameworkMessage::RegisterUDP { .. } => panic!("This should be handled previous"), _ => { warn!("Unhandled Framework Packet: {:?}", packet); } @@ -578,17 +576,12 @@ impl ConnectionActor { return Err(anyhow::anyhow!("Closed")); } ConnectionAction::RegisterUDP(addr) => { - self.notified_idle = false; - - if self.udp_writer.addr.is_some() { - return Ok(()); + if let Some(addr) = self.udp_writer.addr { + self.state.remove_udp(addr); } - self.udp_writer.set_addr(addr); - - info!("New connection {} from {}", self.id, addr); - if let Some(sender) = self.state.get_sender(self.id) { + self.udp_writer.set_addr(addr); self.state.register_udp(addr, sender, self.limiter.clone()); } else { return Err(anyhow::anyhow!( @@ -601,6 +594,10 @@ impl ConnectionActor { connection_id: self.id, })) .await?; + + self.notified_idle = false; + + info!("New connection {} from {}", self.id, addr); } ConnectionAction::ProcessPacket(packet, is_tcp) => { self.notified_idle = false; diff --git a/src/proxy.rs b/src/proxy.rs index d5e173f..55acd76 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -146,14 +146,13 @@ async fn accept_tcp_connection( }; if let Err(e) = actor.run(reader).await { - error!("Connection {} error: {}", id, e); + info!("Connection {} closed: {}", id, e); } state.remove_connection(id, actor.room.as_ref().map(|r| r.room_id().clone())); if let Some(addr) = actor.udp_writer.addr { state.remove_udp(addr); - info!("Connection {} closed", id); } }); } From 62a32f781800d13287124a6e59d1468db3dadf9d Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sat, 24 Jan 2026 03:37:11 +0700 Subject: [PATCH 096/115] refactor: consolidate idle notification logic and adjust constants - Move `notified_idle` flag from `ConnectionActor` to `TcpWriter` for better encapsulation - Centralize idle notification check after batch writes to avoid duplication - Increase packet rate limit from 1000 to 5000 per 3-second window - Increase TCP batch processing capacity from 16 to 64 packets - Remove redundant `notified_idle` resets in packet processing paths - Add debug log for UDP connection closure - Simplify JSON error formatting in stats parsing --- src/connection.rs | 22 ++++++++-------------- src/packet.rs | 10 ++++------ src/proxy.rs | 2 +- src/writer.rs | 10 ++++------ 4 files changed, 17 insertions(+), 27 deletions(-) diff --git a/src/connection.rs b/src/connection.rs index 499ca8c..a4ab19f 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -46,7 +46,6 @@ pub struct ConnectionActor { pub limiter: Arc, pub last_read: Instant, pub packet_queue: Vec, - pub notified_idle: bool, pub room: Option, } @@ -64,8 +63,8 @@ impl ConnectionActor { let mut tick_interval = tokio::time::interval(Duration::from_millis(TICK_INTERVAL_MS)); loop { - let mut headers: Vec<[u8; 2]> = Vec::with_capacity(16); - let mut payloads: Vec = Vec::with_capacity(16); + let mut headers: Vec<[u8; 2]> = Vec::with_capacity(64); + let mut payloads: Vec = Vec::with_capacity(64); tokio::select! { read_result = reader.read_buf(&mut buf) => { @@ -73,7 +72,6 @@ impl ConnectionActor { Ok(0) => return Err(anyhow::anyhow!("Connection closed by peer")), Ok(_n) => { self.last_read = Instant::now(); - self.notified_idle = false; self.process_tcp_buffer(&mut buf).await?; } @@ -97,16 +95,17 @@ impl ConnectionActor { } self.tcp_writer.write_vectored(&slices).await?; } + + if self.is_idle() && !self.tcp_writer.notified_idle { + self.notify_idle(); + } + } else { return Err(anyhow::anyhow!("Connection closed by peer, no action")); } } _ = tick_interval.tick() => { - if self.is_idle() && !self.notified_idle { - self.notify_idle(); - } - if self.tcp_writer.last_write.elapsed() > KEEP_ALIVE_INTERVAL_MS { self.write_packet(AnyPacket::Framework(FrameworkMessage::KeepAlive)).await?; } @@ -563,13 +562,11 @@ impl ConnectionActor { payloads.push(bytes); } ConnectionAction::SendTCPRaw(b) => { - self.notified_idle = false; let len = b.len() as u16; headers.push(len.to_be_bytes()); payloads.push(b); } ConnectionAction::SendUDPRaw(b) => { - self.notified_idle = false; self.udp_writer.send_raw(&b).await?; } ConnectionAction::Close => { @@ -595,12 +592,9 @@ impl ConnectionActor { })) .await?; - self.notified_idle = false; - info!("New connection {} from {}", self.id, addr); } ConnectionAction::ProcessPacket(packet, is_tcp) => { - self.notified_idle = false; self.handle_packet(packet, is_tcp).await?; } } @@ -624,7 +618,7 @@ impl ConnectionActor { }; if self.state.idle(self.id, &room.room_id()) { - self.notified_idle = true; + self.tcp_writer.notified_idle = true; } } } diff --git a/src/packet.rs b/src/packet.rs index 1b26d74..6c92b00 100644 --- a/src/packet.rs +++ b/src/packet.rs @@ -395,11 +395,9 @@ pub fn read_stats(buf: &mut Cursor) -> Result { match serde_json::from_str::(&json) { Ok(data) => Ok(data), - Err(e) => { - Err(AppError::PacketParsing(format!( - "Failed to parse stats: {}. JSON: {}", - e, json - ))) - } + Err(e) => Err(AppError::PacketParsing(format!( + "Failed to parse stats: {}. JSON: {}", + e, json + ))), } } diff --git a/src/proxy.rs b/src/proxy.rs index 55acd76..f987980 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -141,7 +141,6 @@ async fn accept_tcp_connection( limiter, last_read: Instant::now(), packet_queue: Vec::new(), - notified_idle: false, room: None, }; @@ -153,6 +152,7 @@ async fn accept_tcp_connection( if let Some(addr) = actor.udp_writer.addr { state.remove_udp(addr); + info!("UDP connection {} closed", addr); } }); } diff --git a/src/writer.rs b/src/writer.rs index 22910a5..f84512d 100644 --- a/src/writer.rs +++ b/src/writer.rs @@ -9,6 +9,7 @@ use tokio::net::UdpSocket; pub struct TcpWriter { writer: tokio::net::tcp::OwnedWriteHalf, pub last_write: Instant, + pub notified_idle: bool, } impl TcpWriter { @@ -16,18 +17,15 @@ impl TcpWriter { Self { writer, last_write: Instant::now(), + notified_idle: false, } } - pub async fn write(&mut self, data: &[u8]) -> anyhow::Result<()> { - self.writer.write_all(data).await?; - self.last_write = Instant::now(); - Ok(()) - } - pub async fn write_vectored(&mut self, bufs: &[IoSlice<'_>]) -> anyhow::Result<()> { self.writer.write_vectored(bufs).await?; self.last_write = Instant::now(); + self.notified_idle = false; + Ok(()) } } From 6199f8ed75e35eea9e1d808733d9ef8bae1578e3 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sat, 24 Jan 2026 03:38:50 +0700 Subject: [PATCH 097/115] perf: increase tick rate from 60Hz to 15Hz for better performance The tick interval was changed from ~16.67ms (60Hz) to ~66.67ms (15Hz) to reduce CPU usage and improve overall system performance, especially under high connection loads. --- src/connection.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/connection.rs b/src/connection.rs index a4ab19f..3e2d47c 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -20,7 +20,7 @@ const TCP_BUFFER_SIZE: usize = 32768; const CONNECTION_TIME_OUT_MS: Duration = Duration::from_millis(30000); const KEEP_ALIVE_INTERVAL_MS: Duration = Duration::from_millis(3000); const PACKET_LENGTH_LENGTH: usize = 2; -const TICK_INTERVAL_MS: u64 = 1000 / 60; +const TICK_INTERVAL_MS: u64 = 1000 / 15; pub struct ConnectionRoom { room_id: RoomId, From 4203aa939d0e593024f74b510b8916a9dc76177c Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sat, 24 Jan 2026 03:54:42 +0700 Subject: [PATCH 098/115] fix(packet): change ConnectionId type from u32 to i32 The ConnectionId type is used in network packets and must match the signed integer type used by the underlying protocol. This ensures proper serialization/deserialization when handling negative values that may be used as special identifiers. --- src/connection.rs | 4 ++-- src/packet.rs | 26 +++++++++++++------------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/connection.rs b/src/connection.rs index 3e2d47c..f4e4769 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -573,8 +573,8 @@ impl ConnectionActor { return Err(anyhow::anyhow!("Closed")); } ConnectionAction::RegisterUDP(addr) => { - if let Some(addr) = self.udp_writer.addr { - self.state.remove_udp(addr); + if self.udp_writer.addr.is_some() { + return Ok(()); } if let Some(sender) = self.state.get_sender(self.id) { diff --git a/src/packet.rs b/src/packet.rs index 6c92b00..b41876c 100644 --- a/src/packet.rs +++ b/src/packet.rs @@ -9,7 +9,7 @@ pub const APP_PACKET_ID: i8 = -4; pub const FRAMEWORK_PACKET_ID: i8 = -2; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct ConnectionId(pub u32); +pub struct ConnectionId(pub i32); impl std::fmt::Display for ConnectionId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -193,10 +193,10 @@ impl FrameworkMessage { 1 => Ok(FrameworkMessage::DiscoverHost), 2 => Ok(FrameworkMessage::KeepAlive), 3 => Ok(FrameworkMessage::RegisterUDP { - connection_id: ConnectionId(buf.get_u32()), + connection_id: ConnectionId(buf.get_i32()), }), 4 => Ok(FrameworkMessage::RegisterTCP { - connection_id: ConnectionId(buf.get_u32()), + connection_id: ConnectionId(buf.get_i32()), }), _ => Err(AppError::PacketParsing(format!( "Unknown Framework ID: {}", @@ -218,11 +218,11 @@ impl FrameworkMessage { FrameworkMessage::KeepAlive => buf.put_u8(2), FrameworkMessage::RegisterUDP { connection_id } => { buf.put_u8(3); - buf.put_u32(connection_id.0); + buf.put_i32(connection_id.0); } FrameworkMessage::RegisterTCP { connection_id } => { buf.put_u8(4); - buf.put_u32(connection_id.0); + buf.put_i32(connection_id.0); } } } @@ -234,7 +234,7 @@ impl AppPacket { match pid { 0 => { - let connection_id = ConnectionId(buf.get_u32()); + let connection_id = ConnectionId(buf.get_i32()); let is_tcp = buf.get_u8() != 0; let start = buf.position() as usize; @@ -252,15 +252,15 @@ impl AppPacket { )) } 1 => Ok(AppPacket::ConnectionClosed(ConnectionClosedPacket { - connection_id: ConnectionId(buf.get_u32()), + connection_id: ConnectionId(buf.get_i32()), reason: ArcCloseReason::try_from(buf.get_u8())?, })), 2 => Ok(AppPacket::ConnectionJoin(ConnectionJoinPacket { - connection_id: ConnectionId(buf.get_u32()), + connection_id: ConnectionId(buf.get_i32()), room_id: RoomId(read_string(buf)?), })), 3 => Ok(AppPacket::ConnectionIdling(ConnectionIdlingPacket { - connection_id: ConnectionId(buf.get_u32()), + connection_id: ConnectionId(buf.get_i32()), })), 4 => Ok(AppPacket::RoomCreationRequest(RoomCreationRequestPacket { version: read_string(buf)?, @@ -303,23 +303,23 @@ impl AppPacket { match self { AppPacket::ConnectionPacketWrap(p) => { buf.put_u8(0); - buf.put_u32(p.connection_id.0); + buf.put_i32(p.connection_id.0); buf.put_u8(if p.is_tcp { 1 } else { 0 }); buf.extend_from_slice(&p.buffer); } AppPacket::ConnectionClosed(p) => { buf.put_u8(1); - buf.put_u32(p.connection_id.0); + buf.put_i32(p.connection_id.0); buf.put_u8(p.reason as u8); } AppPacket::ConnectionJoin(p) => { buf.put_u8(2); - buf.put_u32(p.connection_id.0); + buf.put_i32(p.connection_id.0); write_string(buf, &p.room_id.0); } AppPacket::ConnectionIdling(p) => { buf.put_u8(3); - buf.put_u32(p.connection_id.0); + buf.put_i32(p.connection_id.0); } AppPacket::RoomCreationRequest(_) => { panic!("Client only") From 123a8c5fbe22f3b3496f39756c78e33d3a556577 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sat, 24 Jan 2026 04:14:48 +0700 Subject: [PATCH 099/115] perf(connection): batch process packets to reduce allocations Process up to PACKET_BATCH_SIZE messages per iteration instead of using try_recv in a potentially unbounded loop. This reduces vector reallocations and improves memory efficiency during high message volume. --- src/connection.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/connection.rs b/src/connection.rs index f4e4769..4f600bd 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -21,6 +21,7 @@ const CONNECTION_TIME_OUT_MS: Duration = Duration::from_millis(30000); const KEEP_ALIVE_INTERVAL_MS: Duration = Duration::from_millis(3000); const PACKET_LENGTH_LENGTH: usize = 2; const TICK_INTERVAL_MS: u64 = 1000 / 15; +const PACKET_BACTH_SIZE: usize = 16; pub struct ConnectionRoom { room_id: RoomId, @@ -63,8 +64,8 @@ impl ConnectionActor { let mut tick_interval = tokio::time::interval(Duration::from_millis(TICK_INTERVAL_MS)); loop { - let mut headers: Vec<[u8; 2]> = Vec::with_capacity(64); - let mut payloads: Vec = Vec::with_capacity(64); + let mut headers: Vec<[u8; 2]> = Vec::with_capacity(PACKET_BACTH_SIZE); + let mut payloads: Vec = Vec::with_capacity(PACKET_BACTH_SIZE); tokio::select! { read_result = reader.read_buf(&mut buf) => { @@ -83,9 +84,13 @@ impl ConnectionActor { if let Some(action) = action { self.handle_action(action, &mut headers, &mut payloads).await?; - while let Ok(action) = self.rx.try_recv() { + for _ in 0..PACKET_BACTH_SIZE { + if let Ok(action) = self.rx.try_recv() { self.handle_action(action, &mut headers, &mut payloads).await?; + } else { + break; } + } if !payloads.is_empty() { let mut slices = Vec::with_capacity(headers.len() * 2); From 0d1245da4d149cec877b1b1016ca76985925daf8 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sat, 24 Jan 2026 13:36:15 +0700 Subject: [PATCH 100/115] fix(connection): improve idle notification and logging logic - Only log connection closure when UDP writer address exists - Rename `notified_idle` to `idling` for clarity in TCP writer - Move idle notification from action handler to tick interval - Increase tick frequency from 15Hz to 60Hz for better responsiveness - Fix indentation in packet batch processing loop --- src/connection.rs | 28 +++++++++++++--------------- src/proxy.rs | 4 +++- src/writer.rs | 6 +++--- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/connection.rs b/src/connection.rs index 4f600bd..5c00ed5 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -20,7 +20,7 @@ const TCP_BUFFER_SIZE: usize = 32768; const CONNECTION_TIME_OUT_MS: Duration = Duration::from_millis(30000); const KEEP_ALIVE_INTERVAL_MS: Duration = Duration::from_millis(3000); const PACKET_LENGTH_LENGTH: usize = 2; -const TICK_INTERVAL_MS: u64 = 1000 / 15; +const TICK_INTERVAL_MS: u64 = 1000 / 60; const PACKET_BACTH_SIZE: usize = 16; pub struct ConnectionRoom { @@ -84,13 +84,14 @@ impl ConnectionActor { if let Some(action) = action { self.handle_action(action, &mut headers, &mut payloads).await?; - for _ in 0..PACKET_BACTH_SIZE { - if let Ok(action) = self.rx.try_recv() { - self.handle_action(action, &mut headers, &mut payloads).await?; - } else { - break; + for _ in 0..PACKET_BACTH_SIZE { + if let Ok(action) = self.rx.try_recv() { + self.handle_action(action, &mut headers, &mut payloads).await?; + + } else { + break; + } } - } if !payloads.is_empty() { let mut slices = Vec::with_capacity(headers.len() * 2); @@ -101,9 +102,6 @@ impl ConnectionActor { self.tcp_writer.write_vectored(&slices).await?; } - if self.is_idle() && !self.tcp_writer.notified_idle { - self.notify_idle(); - } } else { return Err(anyhow::anyhow!("Connection closed by peer, no action")); @@ -111,6 +109,10 @@ impl ConnectionActor { } _ = tick_interval.tick() => { + if !self.tcp_writer.idling { + self.notify_idle(); + } + if self.tcp_writer.last_write.elapsed() > KEEP_ALIVE_INTERVAL_MS { self.write_packet(AnyPacket::Framework(FrameworkMessage::KeepAlive)).await?; } @@ -613,17 +615,13 @@ impl ConnectionActor { self.tcp_writer.write_vectored(&slices).await } - fn is_idle(&self) -> bool { - true - } - fn notify_idle(&mut self) { let Some(room) = &self.room else { return; }; if self.state.idle(self.id, &room.room_id()) { - self.tcp_writer.notified_idle = true; + self.tcp_writer.idling = false; } } } diff --git a/src/proxy.rs b/src/proxy.rs index f987980..f96524a 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -145,7 +145,9 @@ async fn accept_tcp_connection( }; if let Err(e) = actor.run(reader).await { - info!("Connection {} closed: {}", id, e); + if actor.udp_writer.addr.is_some() { + info!("Connection {} closed: {}", id, e); + } } state.remove_connection(id, actor.room.as_ref().map(|r| r.room_id().clone())); diff --git a/src/writer.rs b/src/writer.rs index f84512d..3aa322b 100644 --- a/src/writer.rs +++ b/src/writer.rs @@ -9,7 +9,7 @@ use tokio::net::UdpSocket; pub struct TcpWriter { writer: tokio::net::tcp::OwnedWriteHalf, pub last_write: Instant, - pub notified_idle: bool, + pub idling: bool, } impl TcpWriter { @@ -17,14 +17,14 @@ impl TcpWriter { Self { writer, last_write: Instant::now(), - notified_idle: false, + idling: false, } } pub async fn write_vectored(&mut self, bufs: &[IoSlice<'_>]) -> anyhow::Result<()> { self.writer.write_vectored(bufs).await?; self.last_write = Instant::now(); - self.notified_idle = false; + self.idling = false; Ok(()) } From 5995f767551f3a222fd9665bb691eed0029500b5 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sat, 24 Jan 2026 14:11:39 +0700 Subject: [PATCH 101/115] fix(connection): correct idle state handling for TCP writer The idling flag was incorrectly set to false after writes, causing premature idle notifications. Set it to true after successful writes and adjust the idle check condition to match. Also increase tick frequency from 60Hz to 120Hz for more responsive idle detection. --- src/connection.rs | 13 +++----- src/state.rs | 85 ++++++++++++++++------------------------------- src/writer.rs | 2 +- 3 files changed, 34 insertions(+), 66 deletions(-) diff --git a/src/connection.rs b/src/connection.rs index 5c00ed5..28f8df9 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -20,7 +20,7 @@ const TCP_BUFFER_SIZE: usize = 32768; const CONNECTION_TIME_OUT_MS: Duration = Duration::from_millis(30000); const KEEP_ALIVE_INTERVAL_MS: Duration = Duration::from_millis(3000); const PACKET_LENGTH_LENGTH: usize = 2; -const TICK_INTERVAL_MS: u64 = 1000 / 60; +const TICK_INTERVAL_MS: u64 = 1000 / 120; const PACKET_BACTH_SIZE: usize = 16; pub struct ConnectionRoom { @@ -109,7 +109,7 @@ impl ConnectionActor { } _ = tick_interval.tick() => { - if !self.tcp_writer.idling { + if self.tcp_writer.idling { self.notify_idle(); } @@ -468,13 +468,8 @@ impl ConnectionActor { } if let Some(sender) = self.state.get_sender(p.connection_id) { - let is_in_room = self - .state - .room_state - .rooms - .get(&room_id) - .map(|r| r.members.contains_key(&p.connection_id)) - .unwrap_or(false); + let is_in_room = + self.state.room_state.is_in_room(&p.connection_id, &room_id); if is_in_room { info!( diff --git a/src/state.rs b/src/state.rs index 2b8b52d..f522d8a 100644 --- a/src/state.rs +++ b/src/state.rs @@ -31,6 +31,7 @@ pub enum ConnectionAction { pub struct Room { pub id: RoomId, pub host_connection_id: ConnectionId, + pub host_sender: mpsc::Sender, pub password: Option, pub created_at: u128, pub updated_at: u128, @@ -89,22 +90,12 @@ impl RoomState { if let Some(mut room) = self.rooms.get_mut(room_id) { room.members.insert(connection_id, sender); - let sender = match room.members.get(&room.host_connection_id) { - Some(sender) => sender, - None => { - error!( - "Host {} not found in room {} while joining", - room.host_connection_id, room_id - ); - return Ok(()); - } - }; let packet = AnyPacket::App(AppPacket::ConnectionJoin(ConnectionJoinPacket { connection_id, room_id: room_id.clone(), })); - if let Err(e) = sender.try_send(ConnectionAction::SendTCP(packet)) { + if let Err(e) = room.host_sender.try_send(ConnectionAction::SendTCP(packet)) { info!( "Failed to forward to host {}: {}", room.host_connection_id, e @@ -121,23 +112,12 @@ impl RoomState { if let Some(mut room) = self.rooms.get_mut(room_id) { room.members.remove(&connection_id); - let sender = match room.members.get(&room.host_connection_id) { - Some(sender) => sender, - None => { - error!( - "Host {} not found in room {} while leaving", - room.host_connection_id, room_id - ); - return; - } - }; - let packet = AnyPacket::App(AppPacket::ConnectionClosed(ConnectionClosedPacket { connection_id, reason: ArcCloseReason::Closed, })); - if let Err(e) = sender.try_send(ConnectionAction::SendTCP(packet)) { + if let Err(e) = room.host_sender.try_send(ConnectionAction::SendTCP(packet)) { info!( "Failed to forward to host {}: {}", room.host_connection_id, e @@ -162,13 +142,12 @@ impl RoomState { }; let room_id = RoomId(Uuid::now_v7().to_string()); - let mut members = HashMap::new(); - - members.insert(connection_id, sender); + let members = HashMap::new(); let room = Room { id: room_id.clone(), host_connection_id: connection_id, + host_sender: sender, password, stats, members, @@ -239,18 +218,7 @@ impl RoomState { } }; - let sender = match room.members.get(&room.host_connection_id) { - Some(sender) => sender, - None => { - error!( - "Host {} not found in room {} while forward", - room.host_connection_id, room_id - ); - return; - } - }; - - if let Err(e) = sender.try_send(action) { + if let Err(e) = room.host_sender.try_send(action) { warn!( "Failed to forward to host {}: {}", room.host_connection_id, e @@ -276,29 +244,34 @@ impl RoomState { return true; } - if let Some(sender) = room.members.get(&room.host_connection_id) { - let packet = AnyPacket::App(AppPacket::ConnectionIdling(ConnectionIdlingPacket { - connection_id, - })); - - match sender.try_send(ConnectionAction::SendTCP(packet)) { - Ok(_) => return true, - Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => { - warn!("Host channel full, retrying idle packet later"); - return false; - } - Err(e) => { - warn!( - "Failed to forward idle packet to host {}: {}", - room.host_connection_id, e - ); - return true; - } + let packet = AnyPacket::App(AppPacket::ConnectionIdling(ConnectionIdlingPacket { + connection_id, + })); + + match room.host_sender.try_send(ConnectionAction::SendTCP(packet)) { + Ok(_) => return true, + Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => { + warn!("Host channel full, retrying idle packet later"); + return false; + } + Err(e) => { + warn!( + "Failed to forward idle packet to host {}: {}", + room.host_connection_id, e + ); + return true; } } } true } + + pub fn is_in_room(&self, connection_id: &ConnectionId, room_id: &RoomId) -> bool { + self.rooms + .get(&room_id) + .map(|r| r.members.contains_key(&connection_id)) + .unwrap_or(false) + } } impl From<&Room> for RoomView { diff --git a/src/writer.rs b/src/writer.rs index 3aa322b..d405069 100644 --- a/src/writer.rs +++ b/src/writer.rs @@ -24,7 +24,7 @@ impl TcpWriter { pub async fn write_vectored(&mut self, bufs: &[IoSlice<'_>]) -> anyhow::Result<()> { self.writer.write_vectored(bufs).await?; self.last_write = Instant::now(); - self.idling = false; + self.idling = true; Ok(()) } From afe2dbc9a4721f81eaf714f4e02dfabec3216bd0 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sat, 24 Jan 2026 14:39:02 +0700 Subject: [PATCH 102/115] feat(connection): handle stream packets to prevent connection timeout Add packet ID constants for stream packets and detect them in incoming data. When a stream packet is received, mark the TCP writer as idling to prevent the server from timing out the connection while stream data is being sent. --- src/connection.rs | 9 +++++++-- src/packet.rs | 11 +++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/connection.rs b/src/connection.rs index 28f8df9..b196b53 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -118,7 +118,7 @@ impl ConnectionActor { } if self.last_read.elapsed() > CONNECTION_TIME_OUT_MS { - info!("Connection {} timed out", self.id); + info!("{} Connection timed out", self.id); return Err(anyhow::anyhow!("Connection timed out")); } } @@ -138,7 +138,7 @@ impl ConnectionActor { }; if !self.limiter.check() { - warn!("Connection {} rate limit exceeded", self.id); + warn!("{} Connection rate limit exceeded", self.id); return Err(anyhow::anyhow!("Rate limit exceeded")); } @@ -514,6 +514,11 @@ impl ConnectionActor { return Err(anyhow!("Not room owner")); } + if AnyPacket::is_stream_packet(&buffer) { + info!("Stream packet received"); + self.tcp_writer.idling = true; + } + let Some(sender) = self.state.get_sender(connection_id) else { warn!("Connection not found: {}", connection_id); diff --git a/src/packet.rs b/src/packet.rs index b41876c..2f76838 100644 --- a/src/packet.rs +++ b/src/packet.rs @@ -8,6 +8,11 @@ use std::io::Cursor; pub const APP_PACKET_ID: i8 = -4; pub const FRAMEWORK_PACKET_ID: i8 = -2; +// These packet require server to send a idle packet to keep the host sending stream +pub const STREAM_BEGIN_PACKET_ID: u8 = 0; +pub const STREAM_CHUNK_PACKET_ID: u8 = 1; +pub const WORLD_STREAM_PACKET_ID: u8 = 2; + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct ConnectionId(pub i32); @@ -130,6 +135,12 @@ pub struct StatsPacket { } impl AnyPacket { + pub fn is_stream_packet(bytes: &Bytes) -> bool { + bytes.starts_with(&[STREAM_BEGIN_PACKET_ID]) + || bytes.starts_with(&[STREAM_CHUNK_PACKET_ID]) + || bytes.starts_with(&[WORLD_STREAM_PACKET_ID]) + } + pub fn read(buf: &mut Cursor) -> Result { if !buf.has_remaining() { return Err(AppError::PacketParsing("Empty packet".to_string())); From 0740b855ba30c3baad5a241f2097e76395f99ae7 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sat, 24 Jan 2026 15:05:49 +0700 Subject: [PATCH 103/115] chore: reduce log noise and increase log verbosity - Remove unnecessary info log for stream packets to reduce noise - Change default log level from INFO to DEBUG for better debugging --- src/connection.rs | 1 - src/main.rs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/connection.rs b/src/connection.rs index b196b53..d38d3d6 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -515,7 +515,6 @@ impl ConnectionActor { } if AnyPacket::is_stream_packet(&buffer) { - info!("Stream packet received"); self.tcp_writer.idling = true; } diff --git a/src/main.rs b/src/main.rs index 6b1c08a..397d5e6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,7 +21,7 @@ use tracing_subscriber::FmtSubscriber; async fn main() -> anyhow::Result<()> { // Initialize logging let subscriber = FmtSubscriber::builder() - .with_max_level(Level::INFO) + .with_max_level(Level::DEBUG) .finish(); tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); From b74f2443520c57def08fb96aa3fe867974b56a23 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sat, 24 Jan 2026 15:06:18 +0700 Subject: [PATCH 104/115] perf: increase packet batch size to improve throughput The PACKET_BACTH_SIZE constant is doubled from 16 to 32 to allow more packets to be processed in each batch, reducing overhead and improving network throughput. --- src/connection.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/connection.rs b/src/connection.rs index d38d3d6..5bdff85 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -21,7 +21,7 @@ const CONNECTION_TIME_OUT_MS: Duration = Duration::from_millis(30000); const KEEP_ALIVE_INTERVAL_MS: Duration = Duration::from_millis(3000); const PACKET_LENGTH_LENGTH: usize = 2; const TICK_INTERVAL_MS: u64 = 1000 / 120; -const PACKET_BACTH_SIZE: usize = 16; +const PACKET_BACTH_SIZE: usize = 32; pub struct ConnectionRoom { room_id: RoomId, From c19d40785e7e918ce8e780bae259f6dca25bc627 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sat, 24 Jan 2026 15:35:01 +0700 Subject: [PATCH 105/115] refactor(constant): rename and restructure close reason enums - Rename `ArcCloseReason` to `ConnectionCloseReason` and add `PacketSpam` variant - Rename `CloseReason` to `RoomCloseReason` and remove unused variants - Update all references to use new enum names - Simplify connection close logic by removing redundant packet sends - Remove obsolete packet spam handling in connection actor --- src/connection.rs | 41 +++++++++-------------------------------- src/constant.rs | 32 +++++++++++++++----------------- src/packet.rs | 10 +++++----- src/state.rs | 12 +++++++----- 4 files changed, 36 insertions(+), 59 deletions(-) diff --git a/src/connection.rs b/src/connection.rs index 5bdff85..ee21a88 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -1,4 +1,4 @@ -use crate::constant::{ArcCloseReason, MessageType}; +use crate::constant::{ConnectionCloseReason, MessageType}; use crate::packet::{ AnyPacket, AppPacket, ConnectionClosedPacket, ConnectionId, ConnectionPacketWrapPacket, FrameworkMessage, Message2Packet, MessagePacket, PopupPacket, RoomId, RoomLinkPacket, @@ -177,26 +177,6 @@ impl ConnectionActor { let is_host = self.room.as_ref().map(|r| r.is_host).unwrap_or(false); if !is_host && !self.limiter.check() { - if let Some(ref room) = self.room { - self.state.room_state.broadcast( - &room.room_id, - ConnectionAction::SendTCP(AnyPacket::App(AppPacket::Message2( - Message2Packet { - message: MessageType::PacketSpamming, - }, - ))), - None, - ); - } - - self.write_packet(AnyPacket::App(AppPacket::ConnectionClosed( - ConnectionClosedPacket { - connection_id: self.id, - reason: ArcCloseReason::Closed, - }, - ))) - .await?; - warn!("Connection {} disconnected for packet spamming.", self.id); return Err(anyhow!("Packet Spamming")); } @@ -307,6 +287,7 @@ impl ConnectionActor { "Connection {} left room {} to join {}", self.id, current_room_id, p.room_id ); + self.state.room_state.leave(self.id, ¤t_room_id); self.room = None; } @@ -346,13 +327,6 @@ impl ConnectionActor { }))) .await?; - self.write_packet(AnyPacket::App(AppPacket::ConnectionClosed( - ConnectionClosedPacket { - connection_id: self.id, - reason: ArcCloseReason::Error, - }, - ))) - .await?; return Ok(()); } @@ -490,7 +464,10 @@ impl ConnectionActor { p.connection_id, e ); } - if let Err(e) = sender.try_send(ConnectionAction::Close) { + + if let Err(e) = sender + .try_send(ConnectionAction::Close(ConnectionCloseReason::Closed)) + { warn!("Failed to send close action to {}: {}", p.connection_id, e); } } else { @@ -526,7 +503,7 @@ impl ConnectionActor { ConnectionAction::SendTCP(AnyPacket::App(AppPacket::ConnectionClosed( ConnectionClosedPacket { connection_id, - reason: ArcCloseReason::Closed, + reason: ConnectionCloseReason::Closed, }, ))), ); @@ -575,8 +552,8 @@ impl ConnectionActor { ConnectionAction::SendUDPRaw(b) => { self.udp_writer.send_raw(&b).await?; } - ConnectionAction::Close => { - return Err(anyhow::anyhow!("Closed")); + ConnectionAction::Close(reason) => { + return Err(anyhow::anyhow!("Connection closed: {:?}", reason)); } ConnectionAction::RegisterUDP(addr) => { if self.udp_writer.addr.is_some() { diff --git a/src/constant.rs b/src/constant.rs index e829ab6..44edbe1 100644 --- a/src/constant.rs +++ b/src/constant.rs @@ -4,24 +4,20 @@ use std::convert::TryFrom; #[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr, PartialEq)] #[repr(u8)] -pub enum CloseReason { +pub enum RoomCloseReason { Closed = 0, - ObsoleteClient = 1, - OutdatedVersion = 2, - ServerClosed = 3, - PackingSpamming = 4, + OutdatedVersion = 1, + ServerClosed = 2, } -impl TryFrom for CloseReason { +impl TryFrom for RoomCloseReason { type Error = AppError; fn try_from(value: u8) -> Result { match value { - 0 => Ok(CloseReason::Closed), - 1 => Ok(CloseReason::ObsoleteClient), - 2 => Ok(CloseReason::OutdatedVersion), - 3 => Ok(CloseReason::ServerClosed), - 4 => Ok(CloseReason::PackingSpamming), + 0 => Ok(RoomCloseReason::Closed), + 1 => Ok(RoomCloseReason::OutdatedVersion), + 2 => Ok(RoomCloseReason::ServerClosed), _ => Err(AppError::PacketParsing(format!( "Invalid CloseReason: {}", value @@ -32,22 +28,24 @@ impl TryFrom for CloseReason { #[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr, PartialEq)] #[repr(u8)] -pub enum ArcCloseReason { +pub enum ConnectionCloseReason { Closed = 0, Timeout = 1, Error = 2, + PacketSpam = 3, } -impl TryFrom for ArcCloseReason { +impl TryFrom for ConnectionCloseReason { type Error = AppError; fn try_from(value: u8) -> Result { match value { - 0 => Ok(ArcCloseReason::Closed), - 1 => Ok(ArcCloseReason::Timeout), - 2 => Ok(ArcCloseReason::Error), + 0 => Ok(ConnectionCloseReason::Closed), + 1 => Ok(ConnectionCloseReason::Timeout), + 2 => Ok(ConnectionCloseReason::Error), + 3 => Ok(ConnectionCloseReason::PacketSpam), _ => Err(AppError::PacketParsing(format!( - "Invalid ArcCloseReason: {}", + "Invalid ConnectionCloseReason: {}", value ))), } diff --git a/src/packet.rs b/src/packet.rs index 2f76838..25328bf 100644 --- a/src/packet.rs +++ b/src/packet.rs @@ -1,4 +1,4 @@ -use crate::constant::{ArcCloseReason, CloseReason, MessageType}; +use crate::constant::{ConnectionCloseReason, MessageType, RoomCloseReason}; use crate::error::AppError; use crate::models::Stats; use bytes::{Buf, BufMut, Bytes, BytesMut}; @@ -74,7 +74,7 @@ pub struct ConnectionPacketWrapPacket { #[derive(Debug, Clone)] pub struct ConnectionClosedPacket { pub connection_id: ConnectionId, - pub reason: ArcCloseReason, + pub reason: ConnectionCloseReason, } #[derive(Debug, Clone)] @@ -104,7 +104,7 @@ pub struct RoomClosureRequestPacket; #[derive(Debug, Clone)] pub struct RoomClosedPacket { - pub reason: CloseReason, + pub reason: RoomCloseReason, } #[derive(Debug, Clone)] @@ -264,7 +264,7 @@ impl AppPacket { } 1 => Ok(AppPacket::ConnectionClosed(ConnectionClosedPacket { connection_id: ConnectionId(buf.get_i32()), - reason: ArcCloseReason::try_from(buf.get_u8())?, + reason: ConnectionCloseReason::try_from(buf.get_u8())?, })), 2 => Ok(AppPacket::ConnectionJoin(ConnectionJoinPacket { connection_id: ConnectionId(buf.get_i32()), @@ -280,7 +280,7 @@ impl AppPacket { })), 5 => Ok(AppPacket::RoomClosureRequest(RoomClosureRequestPacket)), 6 => Ok(AppPacket::RoomClosed(RoomClosedPacket { - reason: CloseReason::try_from(buf.get_u8())?, + reason: RoomCloseReason::try_from(buf.get_u8())?, })), 7 => Ok(AppPacket::RoomLink(RoomLinkPacket { room_id: RoomId(read_string(buf)?), diff --git a/src/state.rs b/src/state.rs index f522d8a..c20abaf 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,5 +1,5 @@ use crate::config::Config; -use crate::constant::{ArcCloseReason, CloseReason}; +use crate::constant::{ConnectionCloseReason, RoomCloseReason}; use crate::error::AppError; use crate::models::{RoomView, Stats}; use crate::packet::{ @@ -22,7 +22,7 @@ pub enum ConnectionAction { SendTCP(AnyPacket), SendTCPRaw(Bytes), SendUDPRaw(Bytes), - Close, + Close(ConnectionCloseReason), RegisterUDP(SocketAddr), ProcessPacket(AnyPacket, bool), } @@ -114,7 +114,7 @@ impl RoomState { let packet = AnyPacket::App(AppPacket::ConnectionClosed(ConnectionClosedPacket { connection_id, - reason: ArcCloseReason::Closed, + reason: ConnectionCloseReason::Closed, })); if let Err(e) = room.host_sender.try_send(ConnectionAction::SendTCP(packet)) { @@ -171,13 +171,15 @@ impl RoomState { for (id, sender) in room.members { if let Err(e) = sender.try_send(ConnectionAction::SendTCP(AnyPacket::App( AppPacket::RoomClosed(RoomClosedPacket { - reason: CloseReason::Closed, + reason: RoomCloseReason::Closed, }), ))) { warn!("Failed to send room closed packet to {}: {}", id, e); } - if let Err(e) = sender.try_send(ConnectionAction::Close) { + if let Err(e) = + sender.try_send(ConnectionAction::Close(ConnectionCloseReason::Closed)) + { warn!("Failed to send close action to {}: {}", id, e); } } From 68176f6b23a157dc33da90585eb1c96b8fd6f2d5 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sat, 24 Jan 2026 17:04:38 +0700 Subject: [PATCH 106/115] refactor(connection): simplify error handling and remove outdated packets Remove obsolete popup and message packets for unsupported version and failed room joins. Replace with direct error returns to streamline error flow and reduce unnecessary packet construction. --- src/connection.rs | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/src/connection.rs b/src/connection.rs index ee21a88..6a66a49 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -1,7 +1,7 @@ use crate::constant::{ConnectionCloseReason, MessageType}; use crate::packet::{ AnyPacket, AppPacket, ConnectionClosedPacket, ConnectionId, ConnectionPacketWrapPacket, - FrameworkMessage, Message2Packet, MessagePacket, PopupPacket, RoomId, RoomLinkPacket, + FrameworkMessage, Message2Packet, RoomId, RoomLinkPacket, }; use crate::rate::AtomicRateLimiter; use crate::state::{AppState, ConnectionAction, RoomInit, RoomUpdate}; @@ -157,12 +157,6 @@ impl ConnectionActor { } Err(e) => { error!("Error reading packet: {:?} from connection {}", e, self.id); - - let packet = AnyPacket::App(AppPacket::Popup(PopupPacket { - message: "Your version of MindustryTool is no longer supported. Please update to the latest version.".to_string(), - })); - - self.write_packet(packet).await?; continue; } } @@ -310,11 +304,8 @@ impl ConnectionActor { "Connection {} tried to join room {} with wrong password.", self.id, p.room_id ); - self.write_packet(AnyPacket::App(AppPacket::Message(MessagePacket { - message: "Wrong password".to_string(), - }))) - .await?; - return Ok(()); + + return Err(anyhow!("Wrong password")); } if !can_join { @@ -322,12 +313,8 @@ impl ConnectionActor { "Connection {} tried to join a non-existent room {}.", self.id, p.room_id ); - self.write_packet(AnyPacket::App(AppPacket::Popup(PopupPacket { - message: "Room not found".to_string(), - }))) - .await?; - return Ok(()); + return Err(anyhow!("Room not found")); } if let Some(sender) = self.state.get_sender(self.id) { From a663613bcc45631ed2c4cf1cf1b89cf36852f693 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sat, 24 Jan 2026 18:43:35 +0700 Subject: [PATCH 107/115] fix(connection): replace generic error responses with localized messages Instead of returning generic error messages when room join fails, send localized message packets to the client. This provides better user feedback and allows for internationalization. Also remove redundant connection closed packet sending when closing a connection, as the close action itself handles termination. --- src/connection.rs | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/src/connection.rs b/src/connection.rs index 6a66a49..3e1279e 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -1,7 +1,7 @@ use crate::constant::{ConnectionCloseReason, MessageType}; use crate::packet::{ AnyPacket, AppPacket, ConnectionClosedPacket, ConnectionId, ConnectionPacketWrapPacket, - FrameworkMessage, Message2Packet, RoomId, RoomLinkPacket, + FrameworkMessage, Message2Packet, MessagePacket, RoomId, RoomLinkPacket, }; use crate::rate::AtomicRateLimiter; use crate::state::{AppState, ConnectionAction, RoomInit, RoomUpdate}; @@ -305,7 +305,12 @@ impl ConnectionActor { self.id, p.room_id ); - return Err(anyhow!("Wrong password")); + self.write_packet(AnyPacket::App(AppPacket::Message(MessagePacket { + message: "@player-connect.wrong-password".to_string(), + }))) + .await?; + + return Ok(()); } if !can_join { @@ -314,7 +319,12 @@ impl ConnectionActor { self.id, p.room_id ); - return Err(anyhow!("Room not found")); + self.write_packet(AnyPacket::App(AppPacket::Message(MessagePacket { + message: "@player-connect.room-not-found".to_string(), + }))) + .await?; + + return Ok(()); } if let Some(sender) = self.state.get_sender(self.id) { @@ -438,20 +448,6 @@ impl ConnectionActor { self.id, room_id, p.connection_id ); - if let Err(e) = - sender.try_send(ConnectionAction::SendTCP(AnyPacket::App( - AppPacket::ConnectionClosed(ConnectionClosedPacket { - connection_id: p.connection_id, - reason: p.reason, - }), - ))) - { - warn!( - "Failed to send connection closed packet to {}: {}", - p.connection_id, e - ); - } - if let Err(e) = sender .try_send(ConnectionAction::Close(ConnectionCloseReason::Closed)) { From 924cde6f719a3a92c60efaab0f3f3e9817117666 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sun, 25 Jan 2026 02:03:44 +0700 Subject: [PATCH 108/115] fix(connection): correct UDP registration connection ID and log closure reason - Use ConnectionId(0) instead of self.id for UDP registration to match proxy handling - Include closure reason in debug log for better diagnostics - Remove unused SocketAddr import and inline handle_register_udp function --- src/connection.rs | 6 +++--- src/proxy.rs | 23 ++++++++++------------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/connection.rs b/src/connection.rs index 3e1279e..e054edc 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -444,8 +444,8 @@ impl ConnectionActor { if is_in_room { info!( - "Connection {} (room {}) closed the connection {}.", - self.id, room_id, p.connection_id + "Connection {} (room {}) closed the connection {}: {:?}.", + self.id, room_id, p.connection_id, p.reason ); if let Err(e) = sender @@ -554,7 +554,7 @@ impl ConnectionActor { } self.write_packet(AnyPacket::Framework(FrameworkMessage::RegisterUDP { - connection_id: self.id, + connection_id: ConnectionId(0), })) .await?; diff --git a/src/proxy.rs b/src/proxy.rs index f96524a..710ae0c 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -5,7 +5,6 @@ use crate::state::{AppState, ConnectionAction}; use crate::writer::{TcpWriter, UdpWriter}; use bytes::BytesMut; use std::io::Cursor; -use std::net::SocketAddr; use std::sync::Arc; use std::time::{Duration, Instant}; use tokio::net::{TcpListener, UdpSocket}; @@ -62,7 +61,16 @@ fn spawn_udp_listener(state: Arc, socket: Arc) { AnyPacket::Framework(FrameworkMessage::RegisterUDP { connection_id, }) => { - handle_register_udp(&state, connection_id, addr).await; + if let Some(sender) = state.get_sender(connection_id) { + if let Err(e) = + sender.try_send(ConnectionAction::RegisterUDP(addr)) + { + info!( + "Failed to register UDP for connection {}: {}", + connection_id, e + ); + } + } } _ => { let Some((sender, _)) = state.get_route(&addr) else { @@ -89,17 +97,6 @@ fn spawn_udp_listener(state: Arc, socket: Arc) { }); } -async fn handle_register_udp(state: &Arc, connection_id: ConnectionId, addr: SocketAddr) { - if let Some(sender) = state.get_sender(connection_id) { - if let Err(e) = sender.try_send(ConnectionAction::RegisterUDP(addr)) { - info!( - "Failed to register UDP for connection {}: {}", - connection_id, e - ); - } - } -} - async fn accept_tcp_connection( state: Arc, listener: TcpListener, From cc798ea73f7a0bdc35bf252a52c7b4dbaab42b6e Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sun, 25 Jan 2026 05:41:37 +0700 Subject: [PATCH 109/115] fix: notify host of room closure before members Send room closed packet to host before notifying members to ensure host receives closure notification even if member notifications fail. --- src/state.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/state.rs b/src/state.rs index c20abaf..bef5a1d 100644 --- a/src/state.rs +++ b/src/state.rs @@ -167,16 +167,16 @@ impl RoomState { if let Some((_, room)) = removed { info!("Room closed {}", room_id); + + if let Err(e) = room.host_sender.try_send(ConnectionAction::SendTCP(AnyPacket::App( + AppPacket::RoomClosed(RoomClosedPacket { + reason: RoomCloseReason::Closed, + }), + ))) { + warn!("Failed to send room closed packet to host {}: {}", room.host_connection_id, e); + } for (id, sender) in room.members { - if let Err(e) = sender.try_send(ConnectionAction::SendTCP(AnyPacket::App( - AppPacket::RoomClosed(RoomClosedPacket { - reason: RoomCloseReason::Closed, - }), - ))) { - warn!("Failed to send room closed packet to {}: {}", id, e); - } - if let Err(e) = sender.try_send(ConnectionAction::Close(ConnectionCloseReason::Closed)) { From 5e3af9edbf128f39cb330bb00fde56f0b2c5f960 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Sun, 25 Jan 2026 20:23:24 +0700 Subject: [PATCH 110/115] fix: pass RoomCloseReason when closing rooms Ensure RoomCloseReason::Closed is explicitly passed to room_state.close() calls to maintain consistency and provide proper logging context. --- src/connection.rs | 4 ++-- src/state.rs | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/connection.rs b/src/connection.rs index e054edc..82e175e 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -1,4 +1,4 @@ -use crate::constant::{ConnectionCloseReason, MessageType}; +use crate::constant::{ConnectionCloseReason, MessageType, RoomCloseReason}; use crate::packet::{ AnyPacket, AppPacket, ConnectionClosedPacket, ConnectionId, ConnectionPacketWrapPacket, FrameworkMessage, Message2Packet, MessagePacket, RoomId, RoomLinkPacket, @@ -416,7 +416,7 @@ impl ConnectionActor { return Ok(()); } - self.state.room_state.close(&room_id); + self.state.room_state.close(&room_id, RoomCloseReason::Closed); self.room = None; info!( diff --git a/src/state.rs b/src/state.rs index bef5a1d..fa54ace 100644 --- a/src/state.rs +++ b/src/state.rs @@ -162,15 +162,15 @@ impl RoomState { room_id } - pub fn close(&self, room_id: &RoomId) { + pub fn close(&self, room_id: &RoomId, reason: RoomCloseReason) { let removed = self.rooms.remove(room_id); if let Some((_, room)) = removed { - info!("Room closed {}", room_id); + info!("Room closed {}: {:?}", room_id, reason); if let Err(e) = room.host_sender.try_send(ConnectionAction::SendTCP(AnyPacket::App( AppPacket::RoomClosed(RoomClosedPacket { - reason: RoomCloseReason::Closed, + reason, }), ))) { warn!("Failed to send room closed packet to host {}: {}", room.host_connection_id, e); @@ -377,7 +377,7 @@ impl AppState { }; if should_close { - self.room_state.close(&room_id); + self.room_state.close(&room_id, RoomCloseReason::Closed); } } } From fb879b116ca12131f022d48067c937f495031396 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Tue, 27 Jan 2026 20:35:57 +0700 Subject: [PATCH 111/115] fix(proxy): increase packet rate limit from 1000 to 10000 The previous limit of 1000 packets per 3 seconds was too restrictive for high-traffic scenarios, causing legitimate packets to be dropped. This increase allows the proxy to handle higher loads while still maintaining rate limiting protection. --- src/proxy.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/proxy.rs b/src/proxy.rs index 710ae0c..c4e20d7 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -14,8 +14,8 @@ use tracing::{error, info, warn}; use crate::packet::ConnectionId; const UDP_BUFFER_SIZE: usize = 4096; const CHANNEL_CAPACITY: usize = 1024; -const PACKET_RATE_LIMIT_WINDOW: Duration = Duration::from_millis(3000); -const PACKET_RATE_LIMIT: u32 = 1000; +const PACKET_RATE_LIMIT_WINDOW: Duration = Duration::from_millis(1000); +const PACKET_RATE_LIMIT: u32 = 50000; pub async fn run(state: Arc, port: u16) -> anyhow::Result<()> { let address = format!("0.0.0.0:{}", port); From 37c1b183ce4f5611c81581d37db94c37bc3ec581 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Tue, 27 Jan 2026 22:08:41 +0700 Subject: [PATCH 112/115] feat: add ping packet handling for connection actor - Define new PingPacket struct with send_at timestamp - Add Ping variant to AppPacket enum with serialization/deserialization - Implement ping packet handling in connection actor to echo back ping packets --- src/connection.rs | 8 +++++++- src/packet.rs | 13 +++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/connection.rs b/src/connection.rs index 82e175e..73ef816 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -229,6 +229,10 @@ impl ConnectionActor { async fn handle_app(&mut self, packet: AppPacket) -> anyhow::Result<()> { match packet { + AppPacket::Ping(p) => { + self.write_packet(AnyPacket::App(AppPacket::Ping(p))) + .await?; + } AppPacket::Stats(p) => { if let Some(ref room) = self.room { if let Some(mut r) = self.state.room_state.rooms.get_mut(&room.room_id) { @@ -416,7 +420,9 @@ impl ConnectionActor { return Ok(()); } - self.state.room_state.close(&room_id, RoomCloseReason::Closed); + self.state + .room_state + .close(&room_id, RoomCloseReason::Closed); self.room = None; info!( diff --git a/src/packet.rs b/src/packet.rs index 25328bf..32164d6 100644 --- a/src/packet.rs +++ b/src/packet.rs @@ -62,6 +62,7 @@ pub enum AppPacket { Popup(PopupPacket), Message2(Message2Packet), Stats(StatsPacket), + Ping(PingPacket), } #[derive(Debug, Clone)] @@ -134,6 +135,11 @@ pub struct StatsPacket { pub data: Stats, } +#[derive(Debug, Clone)] +pub struct PingPacket { + pub send_at: i64, +} + impl AnyPacket { pub fn is_stream_packet(bytes: &Bytes) -> bool { bytes.starts_with(&[STREAM_BEGIN_PACKET_ID]) @@ -301,6 +307,9 @@ impl AppPacket { 12 => Ok(AppPacket::Stats(StatsPacket { data: read_stats(buf)?, })), + 13 => Ok(AppPacket::Ping(PingPacket { + send_at: buf.get_i64(), + })), _ => Err(AppError::PacketParsing(format!( "Unknown App Packet ID: {}", pid @@ -366,6 +375,10 @@ impl AppPacket { AppPacket::Stats(_) => { panic!("Client only") } + AppPacket::Ping(p) => { + buf.put_u8(13); + buf.put_i64(p.send_at); + } } } } From 3af02561e36ef21c02dd1f46d23ce88cd1a235bc Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Wed, 28 Jan 2026 03:23:22 +0700 Subject: [PATCH 113/115] refactor(state): encapsulate fields and extract room update logic - Make `rooms`, `connections`, and `udp_routes` fields private - Extract stats update logic into `RoomState::update_state` method - Extract join validation logic into `RoomState::can_join` method - Broadcast room updates immediately upon room creation - Fix error message order for join failures --- src/connection.rs | 77 ++++++++++------------------------------------- src/state.rs | 70 ++++++++++++++++++++++++++++++++++-------- 2 files changed, 73 insertions(+), 74 deletions(-) diff --git a/src/connection.rs b/src/connection.rs index 73ef816..1637eb8 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -4,8 +4,7 @@ use crate::packet::{ FrameworkMessage, Message2Packet, MessagePacket, RoomId, RoomLinkPacket, }; use crate::rate::AtomicRateLimiter; -use crate::state::{AppState, ConnectionAction, RoomInit, RoomUpdate}; -use crate::utils::current_time_millis; +use crate::state::{AppState, ConnectionAction, RoomInit}; use crate::writer::{TcpWriter, UdpWriter}; use anyhow::anyhow; use bytes::{Buf, Bytes, BytesMut}; @@ -235,33 +234,15 @@ impl ConnectionActor { } AppPacket::Stats(p) => { if let Some(ref room) = self.room { - if let Some(mut r) = self.state.room_state.rooms.get_mut(&room.room_id) { - if !room.is_host { - warn!( - "Connection {} tried to update stats but is not host", - self.id - ); - return Ok(()); - } - - let sent_at = p.data.created_at; - - r.stats = p.data; - r.updated_at = current_time_millis(); - r.ping = current_time_millis() - sent_at; - - if let Err(err) = - self.state - .room_state - .broadcast_sender - .send(RoomUpdate::Update { - id: r.id.clone(), - data: r.clone(), - }) - { - warn!("Fail to broadcast room update {}", err); - } + if !room.is_host { + warn!( + "Connection {} tried to update stats but is not host", + self.id + ); + return Ok(()); } + + self.state.room_state.update_state(&room.room_id, p); } } AppPacket::RoomJoin(p) => { @@ -290,27 +271,17 @@ impl ConnectionActor { self.room = None; } - let (can_join, wrong_password) = (|| { - let room = self.state.room_state.rooms.get(&p.room_id)?; - - if let Some(ref pass) = room.password { - if pass != &p.password { - return Some((false, true)); - } - } - - Some((true, false)) - })() - .unwrap_or((false, false)); + let (room_exists, can_join) = + self.state.room_state.can_join(&p.room_id, &p.password); - if wrong_password { + if !room_exists { warn!( - "Connection {} tried to join room {} with wrong password.", + "Connection {} tried to join a non-existent room {}.", self.id, p.room_id ); self.write_packet(AnyPacket::App(AppPacket::Message(MessagePacket { - message: "@player-connect.wrong-password".to_string(), + message: "@player-connect.room-not-found".to_string(), }))) .await?; @@ -319,12 +290,12 @@ impl ConnectionActor { if !can_join { warn!( - "Connection {} tried to join a non-existent room {}.", + "Connection {} tried to join room {} with wrong password.", self.id, p.room_id ); self.write_packet(AnyPacket::App(AppPacket::Message(MessagePacket { - message: "@player-connect.room-not-found".to_string(), + message: "@player-connect.wrong-password".to_string(), }))) .await?; @@ -385,22 +356,6 @@ impl ConnectionActor { }))) .await?; - let Some(room) = self.state.room_state.rooms.get(&room_id) else { - return Err(anyhow!("Can not find room {}", room_id)); - }; - - if let Err(err) = - self.state - .room_state - .broadcast_sender - .send(RoomUpdate::Update { - id: room.id.clone(), - data: room.clone(), - }) - { - warn!("Fail to broadcast room update {}", err); - } - info!("Room {} created by connection {}.", room_id, self.id); } } diff --git a/src/state.rs b/src/state.rs index fa54ace..2a4680d 100644 --- a/src/state.rs +++ b/src/state.rs @@ -4,7 +4,7 @@ use crate::error::AppError; use crate::models::{RoomView, Stats}; use crate::packet::{ AnyPacket, AppPacket, ConnectionClosedPacket, ConnectionId, ConnectionIdlingPacket, - ConnectionJoinPacket, RoomClosedPacket, RoomId, + ConnectionJoinPacket, RoomClosedPacket, RoomId, StatsPacket, }; use crate::rate::AtomicRateLimiter; use crate::utils::current_time_millis; @@ -54,7 +54,7 @@ pub enum RoomUpdate { } pub struct RoomState { - pub rooms: DashMap, + rooms: DashMap, pub broadcast_sender: tokio::sync::broadcast::Sender, // Keep a receiver to prevent the channel from closing when all clients disconnect pub _broadcast_receiver: tokio::sync::broadcast::Receiver, @@ -126,6 +126,40 @@ impl RoomState { } } + pub fn update_state(&self, room_id: &RoomId, p: StatsPacket) { + if let Some(mut r) = self.rooms.get_mut(&room_id) { + let sent_at = p.data.created_at; + + r.stats = p.data; + r.updated_at = current_time_millis(); + r.ping = current_time_millis() - sent_at; + + if let Err(err) = self + .broadcast_sender + .send(RoomUpdate::Update { + id: r.id.clone(), + data: r.clone(), + }) + { + warn!("Fail to broadcast room update {}", err); + } + } else { + warn!("Room not found {}", room_id); + } + } + + pub fn can_join(&self, room_id: &RoomId, password: &str) -> (bool, bool) { + if let Some(room) = self.rooms.get(room_id) { + if let Some(pwd) = room.password.as_deref() { + (true, pwd == password) + } else { + (true, true) + } + } else { + (false, false) + } + } + pub fn create(&self, init: RoomInit) -> RoomId { let RoomInit { password, @@ -157,7 +191,14 @@ impl RoomState { ping: 0, }; - self.rooms.insert(room_id.clone(), room); + self.rooms.insert(room_id.clone(), room.clone()); + + if let Err(err) = self.broadcast_sender.send(RoomUpdate::Update { + id: room_id.clone(), + data: room, + }) { + warn!("Fail to broadcast room update {}", err); + } room_id } @@ -167,13 +208,17 @@ impl RoomState { if let Some((_, room)) = removed { info!("Room closed {}: {:?}", room_id, reason); - - if let Err(e) = room.host_sender.try_send(ConnectionAction::SendTCP(AnyPacket::App( - AppPacket::RoomClosed(RoomClosedPacket { - reason, - }), - ))) { - warn!("Failed to send room closed packet to host {}: {}", room.host_connection_id, e); + + if let Err(e) = room + .host_sender + .try_send(ConnectionAction::SendTCP(AnyPacket::App( + AppPacket::RoomClosed(RoomClosedPacket { reason }), + ))) + { + warn!( + "Failed to send room closed packet to host {}: {}", + room.host_connection_id, e + ); } for (id, sender) in room.members { @@ -299,9 +344,8 @@ impl From<&Room> for RoomView { pub struct AppState { pub config: Config, pub room_state: RoomState, - pub connections: - DashMap, Arc)>, - pub udp_routes: DashMap, Arc)>, + connections: DashMap, Arc)>, + udp_routes: DashMap, Arc)>, } impl AppState { From 0e08fc5abb66e93e69028d88c3df019f7f438b46 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Wed, 28 Jan 2026 03:30:12 +0700 Subject: [PATCH 114/115] fix: remove redundant log on connection timeout The info log for connection timeout was unnecessary as the error is already propagated and logged elsewhere. This cleanup reduces log noise while maintaining the same error handling behavior. --- src/connection.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/connection.rs b/src/connection.rs index 1637eb8..c6675f2 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -117,7 +117,6 @@ impl ConnectionActor { } if self.last_read.elapsed() > CONNECTION_TIME_OUT_MS { - info!("{} Connection timed out", self.id); return Err(anyhow::anyhow!("Connection timed out")); } } From 9a2d669337211ed8255856b5cc0e24ac9e9183c9 Mon Sep 17 00:00:00 2001 From: Sharlotte Date: Wed, 28 Jan 2026 03:33:28 +0700 Subject: [PATCH 115/115] refactor(http): remove room page endpoint and extract room views logic - Remove the `/rooms/:roomId` GET endpoint and its associated HTML generation - Extract room iteration logic into `RoomState::into_views()` method - Simplify SSE initialization by using the new method - Clean up unused imports in http module --- src/http.rs | 86 ++-------------------------------------------------- src/state.rs | 23 +++++++++----- 2 files changed, 18 insertions(+), 91 deletions(-) diff --git a/src/http.rs b/src/http.rs index 3488044..4da2b7c 100644 --- a/src/http.rs +++ b/src/http.rs @@ -1,12 +1,11 @@ use crate::models::{RemoveRemoveEvent, RoomUpdateEvent, RoomView}; -use crate::packet::RoomId; use crate::state::{AppState, RoomUpdate}; use axum::{ - extract::{Path, State}, + extract::State, http::header, response::{ sse::{Event, KeepAlive, Sse}, - Html, IntoResponse, + IntoResponse, }, routing::{get, post}, Router, @@ -23,7 +22,6 @@ pub async fn run(state: Arc, port: u16) -> anyhow::Result<()> { let app = Router::new() .route("/ping", get(ping)) .route("/rooms", get(rooms_sse)) - .route("/:roomId", get(room_page)) .route("/:roomId", post(room_port)) .with_state(state); @@ -41,15 +39,7 @@ async fn rooms_sse(State(state): State>) -> impl IntoResponse { let rx = state.room_state.broadcast_sender.subscribe(); let stream = BroadcastStream::new(rx); - let initial_rooms: Vec = state - .room_state - .rooms - .iter() - .map(|entry| RoomUpdateEvent { - room_id: entry.key().0.clone(), - data: RoomView::from(entry.value()), - }) - .collect(); + let initial_rooms: Vec = state.room_state.into_views(); let init_stream = once(async move { Event::default() @@ -91,76 +81,6 @@ async fn rooms_sse(State(state): State>) -> impl IntoResponse { ) } -async fn room_page( - Path(room_id): Path, - State(state): State>, -) -> impl IntoResponse { - let stats = state - .room_state - .rooms - .get(&RoomId(room_id)) - .map(|r| r.stats.clone()); - - if let Some(stats) = stats { - let html = format!( - r#" - - - - - Room: {} - - - - - - - - - - - - - - - -

Room: {}

-
    -
  • Map: {}
  • -
  • Gamemode: {}
  • -
  • Players: {}
  • -
  • Locale: {}
  • -
  • Version: {}
  • -
  • Created At: {}
  • -
- -"#, - stats.name, - stats.name, - stats.map_name, - stats.gamemode, - stats.players.len(), - stats.version, - stats.map_name, - stats.gamemode, - stats.players.len(), - stats.locale, - stats.version, - stats.created_at, - stats.name, - stats.map_name, - stats.gamemode, - stats.players.len(), - stats.locale, - stats.version, - stats.created_at - ); - Html(html) - } else { - Html("

Room not found

".to_string()) - } -} - async fn room_port(State(state): State>) -> impl IntoResponse { state.config.player_connect_port.to_string() } diff --git a/src/state.rs b/src/state.rs index 2a4680d..322500c 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,7 +1,7 @@ use crate::config::Config; use crate::constant::{ConnectionCloseReason, RoomCloseReason}; use crate::error::AppError; -use crate::models::{RoomView, Stats}; +use crate::models::{RoomUpdateEvent, RoomView, Stats}; use crate::packet::{ AnyPacket, AppPacket, ConnectionClosedPacket, ConnectionId, ConnectionIdlingPacket, ConnectionJoinPacket, RoomClosedPacket, RoomId, StatsPacket, @@ -81,6 +81,16 @@ impl RoomState { .cloned() } + pub fn into_views(&self) -> Vec { + self.rooms + .iter() + .map(|entry| RoomUpdateEvent { + room_id: entry.key().0.clone(), + data: RoomView::from(entry.value()), + }) + .collect() + } + pub fn join( &self, connection_id: ConnectionId, @@ -134,13 +144,10 @@ impl RoomState { r.updated_at = current_time_millis(); r.ping = current_time_millis() - sent_at; - if let Err(err) = self - .broadcast_sender - .send(RoomUpdate::Update { - id: r.id.clone(), - data: r.clone(), - }) - { + if let Err(err) = self.broadcast_sender.send(RoomUpdate::Update { + id: r.id.clone(), + data: r.clone(), + }) { warn!("Fail to broadcast room update {}", err); } } else {