diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..5d33771 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,62 @@ +.git/ +.github/ + +# Build directories +build/ +Build/ +build-*/ + +# CMake generated files +CMakeFiles/ +CMakeCache.txt +cmake_install.cmake +Makefile +install_manifest.txt +compile_commands.json + +# Compiled artifacts +*.o +*.obj +*.lo +*.slo +*.d +*.gch +*.pch +*.ilk +*.pdb +*.so +*.dylib +*.dll +*.so.* +*.lai +*.la +*.a +*.lib +*.exe +*.out +*.app + +# Temporary files +*.tmp +*.log +*.bak +*.swp +*.dwo + +# vcpkg +vcpkg_installed/ + +# IDE +.vs/ +CMakeSettings.json + +# Runtime/output +bin/ +.cache/ +test/ +proxy_settings.jsonc + +# Docker +Dockerfile +.dockerignore +docker-compose.yml diff --git a/.github/workflows/publishDocker.yml b/.github/workflows/publishDocker.yml new file mode 100644 index 0000000..e756659 --- /dev/null +++ b/.github/workflows/publishDocker.yml @@ -0,0 +1,41 @@ +name: Publish Docker image + +on: + release: + types: [published] + + push: + branches: [ "main", "development"] + +jobs: + push_to_registries: + name: Push Docker image to multiple registries + runs-on: ubuntu-latest + permissions: + packages: write + contents: read + steps: + - name: Check out the repo + uses: actions/checkout@v4 + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 + with: + images: ghcr.io/${{ github.repository }} + + - name: Build and push Docker images + uses: docker/build-push-action@v6 + with: + context: . + push: true + file: ./Dockerfile + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 6ccecd0..11d86d8 100644 --- a/.gitignore +++ b/.gitignore @@ -71,3 +71,4 @@ vcpkg_installed/ proxy_settings.jsonc /.vs /CMakeSettings.json +.vscode diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ffa7054 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,33 @@ +FROM gcc:15 AS builder + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && apt-get install -y \ + cmake \ + git \ + libssl-dev \ + pkg-config \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY . /app + +RUN cmake -B build -DCMAKE_BUILD_TYPE=Release && \ + cmake --build build --config Release -j$(nproc) + +FROM debian:trixie-slim + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && apt-get install -y \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY --from=builder /app/bin/ProxyPass /app/ProxyPass + +EXPOSE 19132/udp + +ENTRYPOINT ["/app/ProxyPass"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..4bf534f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,8 @@ +services: + proxy: + container_name: SurvivalProxy + restart: unless-stopped + image: proxy + build: . + ports: + - 19134:19132/udp \ No newline at end of file diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..5b993c3 --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1784120854, + "narHash": "sha256-KesHgItiZPgGX740axSiQLcIQ8D24MDqNpkKYWIek8k=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "753cc8a3a87467296ddd1fa93f0cc3e81120ee46", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..4bea6b3 --- /dev/null +++ b/flake.nix @@ -0,0 +1,44 @@ +{ + description = "ProxyPass build environment"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + }; + + outputs = { self, nixpkgs }: + let + system = "x86_64-linux"; + pkgs = import nixpkgs { inherit system; }; + opensslStatic = pkgs.openssl.override { static = true; }; + opensslCombined = pkgs.symlinkJoin { + name = "openssl-combined"; + paths = [ opensslStatic.out opensslStatic.dev ]; + }; + in + { + devShells.${system}.default = pkgs.mkShell { + buildInputs = with pkgs; [ + gcc15 + cmake + ninja + openssl + git + pkg-config + glibc.static + ]; + + OPENSSL_ROOT_DIR = "${opensslCombined}"; + + shellHook = '' + echo "ProxyPass build environment" + echo "GCC: $(gcc --version | head -n1)" + echo "CMake: $(cmake --version | head -n1)" + echo "Ninja: $(ninja --version)" + echo "" + echo "Build with:" + echo " CC=gcc CXX=g++ cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release" + echo " cmake --build build --config Release --parallel" + ''; + }; + }; +} diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..2111886 --- /dev/null +++ b/shell.nix @@ -0,0 +1,32 @@ +{ pkgs ? import {} }: + +let + opensslCombined = pkgs.symlinkJoin { + name = "openssl-combined"; + paths = [ pkgs.openssl.out pkgs.openssl.dev ]; + }; +in + +pkgs.mkShell { + buildInputs = with pkgs; [ + gcc15 + cmake + ninja + openssl + git + pkg-config + ]; + + OPENSSL_ROOT_DIR = "${opensslCombined}"; + + shellHook = '' + echo "ProxyPass build environment" + echo "GCC: $(gcc --version | head -n1)" + echo "CMake: $(cmake --version | head -n1)" + echo "Ninja: $(ninja --version)" + echo "" + echo "Build with:" + echo " CC=gcc CXX=g++ cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release" + echo " cmake --build build --config Release --parallel" + ''; +} diff --git a/src/Main.cpp b/src/Main.cpp index d4641fb..d3a0d39 100644 --- a/src/Main.cpp +++ b/src/Main.cpp @@ -18,6 +18,7 @@ #include "ProxyPass.hpp" int main() { + sculk::ProxyPass::setWorkingDirectory(); sculk::ProxyPass::initConsole(); auto proxyPass = sculk::ProxyPass(); if (!proxyPass.start()) { diff --git a/src/ProxyPass.cpp b/src/ProxyPass.cpp index 74f39f2..b8aea79 100644 --- a/src/ProxyPass.cpp +++ b/src/ProxyPass.cpp @@ -16,8 +16,19 @@ #include "ProxyPass.hpp" #include "Logger.hpp" +#include +#include +#include +#include +#include #include #include +#include + +#ifndef _WIN32 +#include +#endif + #include #include #include @@ -30,6 +41,46 @@ namespace sculk { +namespace { + std::atomic gProxyPassInstance{nullptr}; + + void proxyPassSignalHandler(int /*signum*/) { + if (auto* instance = gProxyPassInstance.load(std::memory_order_acquire)) { + instance->requestStop(); + } + } +} + +void ProxyPass::setWorkingDirectory() { + std::filesystem::path exePath; +#ifdef _WIN32 + wchar_t buffer[MAX_PATH]; + if (GetModuleFileNameW(nullptr, buffer, MAX_PATH) != 0) { + exePath = buffer; + } +#else + char buffer[PATH_MAX]; + ssize_t len = readlink("/proc/self/exe", buffer, sizeof(buffer) - 1); + if (len != -1) { + buffer[len] = '\0'; + exePath = buffer; + } +#endif + if (exePath.empty()) { + return; + } + + auto dir = exePath.parent_path(); + for (int i = 0; i < 4 && !dir.empty(); ++i) { + if (std::filesystem::exists(dir / "proxy_settings.jsonc")) { + std::filesystem::current_path(dir); + return; + } + dir = dir.parent_path(); + } + std::filesystem::current_path(exePath.parent_path()); +} + void ProxyPass::initConsole() { #ifdef _WIN32 SetConsoleOutputCP(CP_UTF8); @@ -50,6 +101,8 @@ void ProxyPass::initConsole() { } #endif std::ios::sync_with_stdio(false); + setvbuf(stdout, nullptr, _IOLBF, 0); + setvbuf(stderr, nullptr, _IOLBF, 0); std::print("\033]0;ProxyPass\007"); } @@ -412,6 +465,13 @@ void ProxyPass::handleFirstClientPacket( return; } + getLogger().info( + "Upstream connection to {}:{} established for player: {}.", + mSettings.upstream_host, + mSettings.upstream_port, + currentBridge->mConnectionRequest.getXboxLiveName() + ); + if (!currentBridge->mClientReady.load(std::memory_order_acquire)) { return; } @@ -435,7 +495,9 @@ void ProxyPass::handleFirstClientPacket( } getLogger().info( - "Failed to connect to upstream server for player: {}.", + "Upstream connection to {}:{} failed for player: {}.", + mSettings.upstream_host, + mSettings.upstream_port, currentBridge->mConnectionRequest.getXboxLiveName() ); getLogger().info( @@ -463,10 +525,17 @@ void ProxyPass::handleFirstClientPacket( return disconnectClient(guid, "Failed to initialize proxy bridge", protocol::DisconnectFailReason::Unknown); } - if (bridge->mProxyClient.connect(mSettings.upstream_host, mSettings.upstream_port) - != protocol::ClientNetworkSystem::ConnectionResult::ConnectionAttemptStarted) [[unlikely]] { + getLogger().info( + "Connecting to upstream server at {}:{} for player: {}.", + mSettings.upstream_host, + mSettings.upstream_port, + bridge->mConnectionRequest.getXboxLiveName() + ); + auto connectResult = bridge->mProxyClient.connect(mSettings.upstream_host, mSettings.upstream_port); + if (connectResult != protocol::ClientNetworkSystem::ConnectionResult::ConnectionAttemptStarted) [[unlikely]] { getLogger().info( - "Failed to connect to upstream server for player: {}.", + "Failed to start upstream connection (result: {}) for player: {}.", + std::to_underlying(connectResult), bridge->mConnectionRequest.getXboxLiveName() ); getLogger().info( @@ -613,22 +682,40 @@ void ProxyPass::shutdown() { mThreadPool.reset(); } +void ProxyPass::requestStop() { + if (!mShouldStop.exchange(true)) { + std::lock_guard lock(mStopMutex); + mStopCv.notify_all(); + } +} + void ProxyPass::waitForStop() { - std::string command{}; - while (true) { - if (!(std::cin >> command)) { - break; - } - if (command == "stop") { - shutdown(); - getLogger().info("Proxy server stopped."); - break; + gProxyPassInstance.store(this, std::memory_order_release); + std::signal(SIGINT, proxyPassSignalHandler); + std::signal(SIGTERM, proxyPassSignalHandler); + + std::thread inputThread([this]() { + std::string command{}; + while (std::cin >> command) { + if (command == "stop") { + requestStop(); + break; + } + getLogger().error( + "Unknown command: {}. Please check that the command exists and that you have permission to use it.", + command + ); } - getLogger().error( - "Unknown command: {}. Please check that the command exists and that you have permission to use it.", - command - ); + }); + inputThread.detach(); + + { + std::unique_lock lock(mStopMutex); + mStopCv.wait(lock, [this] { return mShouldStop.load(); }); } + + shutdown(); + getLogger().info("Proxy server stopped."); } } // namespace sculk diff --git a/src/ProxyPass.hpp b/src/ProxyPass.hpp index e36394d..2e8552c 100644 --- a/src/ProxyPass.hpp +++ b/src/ProxyPass.hpp @@ -18,6 +18,9 @@ #include "Logger.hpp" #include "ProxyBridge.hpp" #include "ProxySettings.hpp" +#include +#include +#include #include #include #include @@ -39,6 +42,9 @@ class ProxyPass { protocol::PemKeyPair mProxyServerKeyPair{}; std::unique_ptr mThreadPool{}; std::unique_ptr mProxyServer{}; + std::mutex mStopMutex{}; + std::condition_variable mStopCv{}; + std::atomic mShouldStop{false}; public: ProxyPass(); @@ -50,8 +56,12 @@ class ProxyPass { void shutdown(); + void requestStop(); + void waitForStop(); + static void setWorkingDirectory(); + static void initConsole(); private: