From b4a832e64f7ee088258c4375e256374de7377531 Mon Sep 17 00:00:00 2001 From: MettleSphee Date: Sun, 2 Aug 2026 12:14:16 +0300 Subject: [PATCH 1/2] Resolve shard IP hostnames before sending them to the client The FusionFall client only understands literal IPv4 addresses in the shard select packet, so a hostname configured as the shard IP (e.g. a dynamic-DNS address) is now resolved to IPv4 by the server before it is sent to the client. The address is resolved once at startup, before the sandbox engages (the login server thread is not allowed to open new sockets afterwards), and re-resolved on every character select on builds where runtime DNS lookups are possible (Windows, and Linux builds without the seccomp sandbox), so dynamic-DNS changes are picked up without a restart. Also guards against overflowing the 16-byte g_FE_ServerIP field and documents hostname support in config.ini. --- .github/workflows/push-ghcr-image.yml | 66 +++++++++++++++++++++++++++ config.ini | 5 ++ src/main.cpp | 8 ++++ src/servers/CNLoginServer.cpp | 18 ++++++-- src/settings.cpp | 38 +++++++++++++++ src/settings.hpp | 26 +++++++++++ 6 files changed, 158 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/push-ghcr-image.yml diff --git a/.github/workflows/push-ghcr-image.yml b/.github/workflows/push-ghcr-image.yml new file mode 100644 index 00000000..89c3ee22 --- /dev/null +++ b/.github/workflows/push-ghcr-image.yml @@ -0,0 +1,66 @@ +name: Push Docker Image to GHCR + +on: + release: + types: [published] + push: + branches: [master] + paths: + - src/** + - vendor/** + - Dockerfile + - Makefile + - CMakeLists.txt + - version.h.in + workflow_dispatch: + +jobs: + push-ghcr: + name: Build and push image to GHCR + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + with: + # the Makefile pulls GIT_VERSION from `git describe --tags` + fetch-depth: 0 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=ref,event=branch + type=ref,event=tag + type=semver,pattern={{version}} + type=semver,pattern={{major}} + type=raw,value=latest,enable=${{ github.ref_type == 'tag' || github.ref_name == 'master' }} + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Cache Docker layers + uses: actions/cache@v4 + with: + path: /tmp/.buildx-cache + key: buildx-${{ github.sha }} + restore-keys: buildx- + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + file: ./Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache,mode=max diff --git a/config.ini b/config.ini index d051ffba..2a89f2bc 100644 --- a/config.ini +++ b/config.ini @@ -30,6 +30,11 @@ dbsaveinterval=240 # Shard Server configuration [shard] port=23001 +# the address the client will be told to connect to for the game world. +# it can be a literal IPv4 address or a hostname (e.g. a dynamic-DNS address); +# hostnames are resolved by the server, since the client can only use literal +# IPs. on Linux builds with the sandbox enabled, the hostname is only resolved +# at startup, so a DNS change requires a server restart. ip=127.0.0.1 # distance at which other players and NPCs become visible. # this value is used for calculating chunk size diff --git a/src/main.cpp b/src/main.cpp index e57faa19..776f0018 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -112,6 +112,14 @@ int main() { initsignals(); settings::init(); + + /* + * If the shard IP is a hostname, resolve it to a literal IPv4 address now. + * This must happen before the sandbox goes up, since the login server isn't + * allowed to open new sockets (like the DNS query sockets) afterwards. + */ + settings::resolveShardIP(); + Database::init(); Rand::init(getTime()); TableData::init(); diff --git a/src/servers/CNLoginServer.cpp b/src/servers/CNLoginServer.cpp index fda4387f..6d7a90d8 100644 --- a/src/servers/CNLoginServer.cpp +++ b/src/servers/CNLoginServer.cpp @@ -480,7 +480,16 @@ void CNLoginServer::characterSelect(CNSocket* sock, CNPacketData* data) { std::cout << "Connecting to shard server" << std::endl; ) - const char* shard_ip = settings::SHARDSERVERIP.c_str(); + /* + * On builds that can do DNS lookups at runtime (Windows, and Linux builds + * without the sandbox), re-resolve the shard address on every character + * select so that changes to a dynamic-DNS address are picked up without a + * restart. Sandboxed Linux builds resolve the address at startup instead. + */ + if (settings::canResolveShardIP()) + settings::resolveShardIP(); + + std::string shard_ip = settings::SHARDSERVERIP; /* * Work around the issue of not being able to connect to a local server if @@ -490,9 +499,12 @@ void CNLoginServer::characterSelect(CNSocket* sock, CNPacketData* data) { if (settings::LOCALHOSTWORKAROUND && sock->sockaddr.sin_addr.s_addr == htonl(INADDR_LOOPBACK)) shard_ip = "127.0.0.1"; - memcpy(resp.g_FE_ServerIP, shard_ip, strlen(shard_ip)); + // g_FE_ServerIP is only 16 bytes and the client expects a null-terminated + // string, so make sure we never write past it + size_t ipLen = std::min(shard_ip.size(), sizeof(resp.g_FE_ServerIP) - 1); + memcpy(resp.g_FE_ServerIP, shard_ip.c_str(), ipLen); + resp.g_FE_ServerIP[ipLen] = '\0'; - resp.g_FE_ServerIP[strlen(shard_ip)] = '\0'; resp.g_FE_ServerPort = settings::SHARDPORT; LoginMetadata *lm = new LoginMetadata(); diff --git a/src/settings.cpp b/src/settings.cpp index 0e72ecf3..28ef378e 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -6,6 +6,14 @@ #include +#ifdef _WIN32 +#include +#include +#else +#include +#include +#endif + // defaults :) int settings::VERBOSITY = 1; bool settings::SANDBOX = true; @@ -157,3 +165,33 @@ void settings::init() { } } } + +void settings::resolveShardIP() { + // if it's already a literal IPv4 address, there's nothing to do + in_addr addr = {}; + if (inet_pton(AF_INET, SHARDSERVERIP.c_str(), &addr) == 1) + return; + + addrinfo hints = {}; + hints.ai_family = AF_INET; // the client can only handle IPv4 addresses + hints.ai_socktype = SOCK_STREAM; + + addrinfo* result = nullptr; + if (getaddrinfo(SHARDSERVERIP.c_str(), nullptr, &hints, &result) != 0) { + std::cerr << "[WARN] Could not resolve shard IP \"" << SHARDSERVERIP + << "\". Check the \"ip\" setting in the [shard] section of config.ini" << std::endl; + return; + } + + // use the first IPv4 address that was resolved + char resolved[INET_ADDRSTRLEN] = {}; + inet_ntop(AF_INET, &((sockaddr_in*)result->ai_addr)->sin_addr, resolved, sizeof(resolved)); + freeaddrinfo(result); + + std::string resolvedIP = resolved; + + if (resolvedIP != SHARDSERVERIP) { + std::cout << "[INFO] Resolved shard IP \"" << SHARDSERVERIP << "\" to " << resolvedIP << std::endl; + SHARDSERVERIP = resolvedIP; + } +} diff --git a/src/settings.hpp b/src/settings.hpp index 7aea34fd..69b36d6a 100644 --- a/src/settings.hpp +++ b/src/settings.hpp @@ -52,4 +52,30 @@ namespace settings { extern bool REMOVEEXPIREDITEMSFROMBANK; void init(); + + /* + * Whether the shard IP can be resolved at runtime, after the sandbox is up. + * The Linux seccomp sandbox blocks opening new sockets, which getaddrinfo() + * needs, so hostnames can only be resolved at startup there. Every other + * platform, and Linux builds without the sandbox, can resolve any time. + */ + inline bool canResolveShardIP() { +#ifdef __linux__ +#ifdef CONFIG_NOSANDBOX + return true; +#else + return !SANDBOX; +#endif +#else + return true; +#endif + } + + /* + * The client only understands literal IPv4 addresses in the shard select + * packet, so if the configured shard IP is a hostname it gets resolved here. + * Safe to call repeatedly; it does nothing if the address is already a + * literal IP or the hostname has already been resolved. + */ + void resolveShardIP(); } From 914a7f3119bb8caecf66952d7c830682ffacdfa1 Mon Sep 17 00:00:00 2001 From: MettleSphee <69507856+MettleSphee@users.noreply.github.com> Date: Wed, 5 Aug 2026 22:28:50 +0300 Subject: [PATCH 2/2] Delete .github/workflows/push-ghcr-image.yml --- .github/workflows/push-ghcr-image.yml | 66 --------------------------- 1 file changed, 66 deletions(-) delete mode 100644 .github/workflows/push-ghcr-image.yml diff --git a/.github/workflows/push-ghcr-image.yml b/.github/workflows/push-ghcr-image.yml deleted file mode 100644 index 89c3ee22..00000000 --- a/.github/workflows/push-ghcr-image.yml +++ /dev/null @@ -1,66 +0,0 @@ -name: Push Docker Image to GHCR - -on: - release: - types: [published] - push: - branches: [master] - paths: - - src/** - - vendor/** - - Dockerfile - - Makefile - - CMakeLists.txt - - version.h.in - workflow_dispatch: - -jobs: - push-ghcr: - name: Build and push image to GHCR - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - steps: - - uses: actions/checkout@v4 - with: - # the Makefile pulls GIT_VERSION from `git describe --tags` - fetch-depth: 0 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Extract metadata - id: meta - uses: docker/metadata-action@v5 - with: - images: ghcr.io/${{ github.repository }} - tags: | - type=ref,event=branch - type=ref,event=tag - type=semver,pattern={{version}} - type=semver,pattern={{major}} - type=raw,value=latest,enable=${{ github.ref_type == 'tag' || github.ref_name == 'master' }} - - name: Log in to GHCR - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Cache Docker layers - uses: actions/cache@v4 - with: - path: /tmp/.buildx-cache - key: buildx-${{ github.sha }} - restore-keys: buildx- - - name: Build and push - uses: docker/build-push-action@v6 - with: - context: . - file: ./Dockerfile - platforms: linux/amd64,linux/arm64 - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=local,src=/tmp/.buildx-cache - cache-to: type=local,dest=/tmp/.buildx-cache,mode=max