Loading .gitattributes 0 → 100644 +8 −0 Changes for .gitattributes: 8 added lines, 0 removed lines. Original line number Diff line number Diff line # Everything in this repository is consumed on Linux: deploy/bootstrap.sh is embedded # verbatim by bootstrap_asset.go and piped into `bash -s`, the Dockerfiles and entrypoints # are read inside the images, and the operator ships YAML. Without eol=lf a Windows # checkout under core.autocrlf=true hands all of them CRs. * text=auto eol=lf # The gradle wrappers' Windows launchers are the one thing that wants CRLF. *.bat text eol=crlf .github/workflows/ci.yml 0 → 100644 +94 −0 Changes for .github/workflows/ci.yml: 94 added lines, 0 removed lines. Original line number Diff line number Diff line # Runs the checks on pull requests and on main. # # release.yml already runs `go vet` and `go test`, but only once a vX.Y.Z tag exists — by then # a red change is on the release path and the only remedy is a new tag. This is the same gate # moved to where it can still stop something, plus the panel suite, which nothing ran at all: # the release goes through the Dockerfile, and the Dockerfile runs `npm run build`, never # `npm test`. # # The push trigger is limited to main rather than every branch, for the reason release.yml is # not repeated here: a branch with an open PR would otherwise run the whole suite twice per # push, once for refs/heads/<branch> and once for refs/pull/N/merge. Those are different # concurrency groups, so neither cancels the other, and this repository is private and billed # for both. A branch with no PR open yet is the one case that loses coverage, and opening the # PR is what asks for the answer. name: ci on: push: branches: [main] pull_request: permissions: contents: read concurrency: group: ci-${{ github.ref }} cancel-in-progress: true jobs: go: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version-file: go.mod - run: go vet ./... - run: go test ./... shell: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 # bootstrap.sh is the only thing that ever runs on a fresh host, and nothing here can # run it — it wants root, a package manager and k3s. Syntax plus the extracted-block # checks in bootstrap_test.sh is the coverage that is reachable without a machine. # Each file is parsed by the interpreter its own shebang names. A blanket `sh -n` is # wrong and not obviously so: on a developer machine `sh` is usually bash and passes # everything, while the runner's `sh` is dash and rejects bootstrap.sh at the first of # its arrays. Honouring the shebang is what makes local and CI agree. - name: Check shell syntax run: | for f in $(git ls-files '*.sh'); do case "$(head -1 "$f")" in *bash) bash -n "$f" || exit 1 ;; *) sh -n "$f" || exit 1 ;; esac done - run: sh deploy/bootstrap_test.sh panel: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 # The Dockerfile's `FROM node:<major>` is the only place the panel's Node version is # declared — there is no .nvmrc and no engines field. Reading it here rather than # repeating the number keeps CI testing the version a release is actually built on; # a second copy would drift silently the first time the image is bumped. - name: Read the panel's Node version from the Dockerfile id: node run: | version="$(sed -n 's/^FROM.*node:\([0-9][0-9]*\)-.*/\1/p' Dockerfile | head -1)" [ -n "$version" ] || { echo "Dockerfile has no 'FROM ... node:<major>' line"; exit 1; } echo "version=${version}" >> "$GITHUB_OUTPUT" - uses: actions/setup-node@v4 with: node-version: ${{ steps.node.outputs.version }} cache: npm cache-dependency-path: panel/package-lock.json - run: npm ci working-directory: panel - run: npm test working-directory: panel - run: npm run typecheck working-directory: panel bootstrap_asset_test.go +9 −3 Changes for bootstrap_asset_test.go: 9 added lines, 3 removed lines. Original line number Diff line number Diff line Loading @@ -73,9 +73,15 @@ func TestLobbyLuckPermsWiringIsConsistent(t *testing.T) { // default it can inherit — and nothing else in the install would notice it missing. The failure // surfaces only when a legacy player joins, on a host that installed cleanly. func TestBootstrapPinsViaBlockConnectionsOff(t *testing.T) { // go:embed takes the working tree verbatim, and this repository pins no eol attribute, so // a Windows checkout embeds CRLF. Only the assertion spanning a line break below cares. script := strings.ReplaceAll(BootstrapScript(), "\r\n", "\n") // go:embed takes the working tree verbatim, so the eol attribute is what keeps a Windows // checkout from compiling CRs into the installer. Assert it rather than normalizing them // away: the only assertion that would otherwise notice is the one spanning a line break // below, and it would report a missing pin instead of the line endings. script := BootstrapScript() if strings.Contains(script, "\r\n") { t.Fatal("embedded bootstrap.sh has CRLF line endings; .gitattributes pins *.sh to LF " + "and this script is piped into `bash -s` on a Linux host") } const key = "serverside-blockconnections" if !strings.Contains(script, key+": false") { Loading cmd/felis/nano.go +10 −2 Changes for cmd/felis/nano.go: 10 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -13,8 +13,16 @@ package main // off-loopback with -listen; see the flag below for why that is an explicit opt-in. // // This is the no-database delivery of the identical brain `felis api` mounts through its // route table (internal/api.HasJoinedHandler). `felis setup --nano` / the bootstrap nano // choice install this as the runtime service; here it just serves. // route table (internal/api.HasJoinedHandler). The bootstrap installer's nano choice — its // `[2] Felis-nano` prompt, or FELIS_INSTALL_MODE=nano — installs this as the // felis-nano.service unit; here it just serves. // // There is deliberately no `felis setup --nano`. setup re-images the host through the full // bootstrap TUI and carries no install-mode parameter anywhere (nothing in Go reads or sets // FELIS_INSTALL_MODE), so such a flag would either re-run the installer — which is what // pointing at the installer already does — or tear a full install down into a nano one, // which is an uninstall, not a flag. This is the same reasoning that makes setup's --dev // refuse and name the installer rather than pretend to choose a channel. import ( "context" Loading deploy/bootstrap.sh +62 −8 Changes for deploy/bootstrap.sh: 62 added lines, 8 removed lines. Original line number Diff line number Diff line Loading @@ -30,6 +30,14 @@ # FELIS_INSTALL_MODE full|nano — skip the prompt (default: ask on a tty, else full) # FELIS_NANO_LISTEN listen addr for `felis nano` (default: 127.0.0.1:8081 — loopback # only; set a private-network IP to serve an off-host proxy) # FELIS_LEGACY_FORWARDING_SERVERS comma-separated backends that receive their identity # through the handshake address instead of modern forwarding # (default: legacy18). Read once at Velocity start, so changing it # means re-running this script and restarting the proxy. # FELIS_VELOCITY_FORK_JAR path to a Felis-Legacy Velocity fork build to install as the # proxy instead of the stock download (default: unset, stock). # FELIS_VELOCITY_FORK_JAR_SHA256 expected sha256 of that jar. REQUIRED whenever the jar # above is set; the install refuses on a mismatch. # FELIS_GO_VERSION Go toolchain used to build the nano binary (default: 1.26.4) # FELIS_REPO_URL git URL to build from (raw script mode only) # FELIS_VERSION_BOOTSTRAP release|dev — which version to install (default: release). Loading Loading @@ -95,6 +103,11 @@ INSTALL_MODE="${FELIS_INSTALL_MODE:-}" # own players stop getting in. Same-host Velocity reaches 127.0.0.1 fine; a proxy on # another machine must opt in explicitly with FELIS_NANO_LISTEN=<private-ip>:8081. FELIS_NANO_LISTEN="${FELIS_NANO_LISTEN:-127.0.0.1:8081}" # Backends that take their forwarded identity through the handshake address instead of # proxy-wide modern forwarding. See write_velocity_service for why a protocol-47 backend # needs this. Overridable because adding a second 1.8 backend otherwise means editing this # script; it is still a restart-time list, not one that follows the CRs. FELIS_LEGACY_FORWARDING_SERVERS="${FELIS_LEGACY_FORWARDING_SERVERS:-legacy18}" FELIS_GO_VERSION="${FELIS_GO_VERSION:-1.26.4}" PKG_LOCK_TIMEOUT="${PKG_LOCK_TIMEOUT:-${APT_LOCK_TIMEOUT:-900}}" APT_LOCK_TIMEOUT="${APT_LOCK_TIMEOUT:-$PKG_LOCK_TIMEOUT}" Loading Loading @@ -123,9 +136,20 @@ FELIS_VELOCITY_VERSION="${FELIS_VELOCITY_VERSION:-3.5.1}" # # Opt-in because it is unmeasured where it counts: FL-008's probe runs offline-mode # against a stub, and this jar would carry every real Mojang session on the server. # The build lives in Felis-Legacy and is not byte-reproducible, so there is no digest # to pin here — the jar is trusted because that probe certified the build. FELIS_VELOCITY_FORK_JAR="${FELIS_VELOCITY_FORK_JAR:-}" # Expected sha256 of that jar, REQUIRED whenever it is set. Case and internal spaces are # ignored, so whatever sha256sum, Get-FileHash or certutil printed can be pasted as-is. # No digest is hardcoded here: # the build lives in Felis-Legacy and has never been reproduced on a second machine, so # any constant this script carried would pin one machine's output rather than the fork. # # So this is not a supply-chain signature and does not pretend to be one — an operator # who can write the jar can write this value too. What it does buy: a path is not an # identity, and every re-run of this script re-checks it. A truncated copy, a stale build # left at the same path, or the two-patch jar where the three-patch one was meant all # change the digest and stop the install. Naming the digest once is what turns "whatever # is at that path today" into one specific build. FELIS_VELOCITY_FORK_JAR_SHA256="${FELIS_VELOCITY_FORK_JAR_SHA256:-}" # Temurin 25: Velocity 3.5 needs 21+, and 25 is also what a future Velocity 4 requires, # so the runtime does not have to move again when the pin does. Distro JDK packaging is # a lottery across four package managers — a tarball is one code path everywhere (same Loading Loading @@ -1462,12 +1486,31 @@ install_jre() { install_velocity() { install_jre local url tmp local url tmp have want prepare_velocity_layout if [ -n "$FELIS_VELOCITY_FORK_JAR" ]; then [ -f "$FELIS_VELOCITY_FORK_JAR" ] \ || die "FELIS_VELOCITY_FORK_JAR is not a readable file: ${FELIS_VELOCITY_FORK_JAR}" log "installing the Felis-Legacy Velocity fork from ${FELIS_VELOCITY_FORK_JAR}" # Hash stdin, never the path — same reason as install_via_plugins: sha256sum escapes its # output line for a filename carrying a backslash or a newline, and the leading "\" that # adds would fail every comparison below. have="$(sha256sum <"$FELIS_VELOCITY_FORK_JAR" | cut -d' ' -f1)" # Refuse rather than warn. This jar is the proxy every player connects through, and a # warning in an install log is not a gate. The digest is printed so the first run after # a deliberate rebuild is one copy-paste, not an investigation. [ -n "$FELIS_VELOCITY_FORK_JAR_SHA256" ] || die \ "FELIS_VELOCITY_FORK_JAR_SHA256 is required whenever FELIS_VELOCITY_FORK_JAR is set. The jar at that path hashes to ${have}. Check that against the build you meant to install, then re-run with FELIS_VELOCITY_FORK_JAR_SHA256=${have}" # Normalise the operator's digest before comparing. sha256sum prints lowercase, but the # build host is often Windows, where Get-FileHash prints uppercase and certutil has # shipped both with and without spaces between the bytes. All three name the same jar, # so comparing raw would refuse two of the three spellings and word it as tampering. want="$(printf '%s' "$FELIS_VELOCITY_FORK_JAR_SHA256" | tr -d '[:space:]' | tr 'A-Z' 'a-z')" [ "$have" = "$want" ] || die \ "FELIS_VELOCITY_FORK_JAR checksum mismatch: got ${have}, expected ${want}" log "installing the Felis-Legacy Velocity fork from ${FELIS_VELOCITY_FORK_JAR} (sha256 ${have})" atomic_install_file "$FELIS_VELOCITY_FORK_JAR" "${VELOCITY_DIR}/velocity.jar" 0644 root root else log "resolving the newest Velocity ${FELIS_VELOCITY_VERSION} build" Loading Loading @@ -1591,9 +1634,20 @@ install_velocity_service() { # behind ViaVersion, which strips modern forwarding's login-plugin-message when it down-translates # the proxy->backend pipeline to protocol 47; only the handshake field survives Via. The Felis # fork reads this list from -Dfelis.legacy-forwarding.servers and forwards those servers legacy; # every other backend keeps modern+secret untouched. v1 hardcodes the one legacy backend; the # upgrade path is to have the operator render this list from the MinecraftServer CRs. local legacy_forwarding_servers="legacy18" # every other backend keeps modern+secret untouched. # # The list is a JVM system property, so it is fixed for the life of the proxy process and a # change needs a Velocity restart. FELIS_LEGACY_FORWARDING_SERVERS makes that reachable # without editing this script, which is as far as a startup property can go. Having it follow # the MinecraftServer CRs instead is a larger change: the forwarding decision lives in the # fork's patch to Velocity core, not in the Felis plugin, so core would need to read state the # plugin owns and refreshes. # # The -D below is double-quoted in ExecStart on purpose. The fork trims each element, so it # accepts "legacy18, legacy112", but systemd splits ExecStart on whitespace before java ever # sees it -- unquoted, that spelling would hand java a stray "legacy112" argument and the unit # would not start. Quoting keeps the whole property one argv item. local legacy_forwarding_servers="${FELIS_LEGACY_FORWARDING_SERVERS}" cat > "$VELOCITY_SERVICE" <<EOF [Unit] Description=Felis Velocity proxy (Mojang authentication + modern forwarding) Loading @@ -1605,7 +1659,7 @@ Type=simple User=${VELOCITY_USER} Group=${VELOCITY_USER} WorkingDirectory=${VELOCITY_DIR} ExecStart=${JRE_DIR}/bin/java -Xms512M -Xmx1G -XX:+UseG1GC -XX:+ParallelRefProcEnabled -XX:+AlwaysPreTouch -Dmojang.sessionserver=http://${api_ip}:8081/session/minecraft/hasJoined -Dfelis.legacy-forwarding.servers=${legacy_forwarding_servers} -jar ${VELOCITY_DIR}/velocity.jar ExecStart=${JRE_DIR}/bin/java -Xms512M -Xmx1G -XX:+UseG1GC -XX:+ParallelRefProcEnabled -XX:+AlwaysPreTouch -Dmojang.sessionserver=http://${api_ip}:8081/session/minecraft/hasJoined "-Dfelis.legacy-forwarding.servers=${legacy_forwarding_servers}" -jar ${VELOCITY_DIR}/velocity.jar Restart=on-failure RestartSec=5 NoNewPrivileges=yes Loading Loading
.gitattributes 0 → 100644 +8 −0 Changes for .gitattributes: 8 added lines, 0 removed lines. Original line number Diff line number Diff line # Everything in this repository is consumed on Linux: deploy/bootstrap.sh is embedded # verbatim by bootstrap_asset.go and piped into `bash -s`, the Dockerfiles and entrypoints # are read inside the images, and the operator ships YAML. Without eol=lf a Windows # checkout under core.autocrlf=true hands all of them CRs. * text=auto eol=lf # The gradle wrappers' Windows launchers are the one thing that wants CRLF. *.bat text eol=crlf
.github/workflows/ci.yml 0 → 100644 +94 −0 Changes for .github/workflows/ci.yml: 94 added lines, 0 removed lines. Original line number Diff line number Diff line # Runs the checks on pull requests and on main. # # release.yml already runs `go vet` and `go test`, but only once a vX.Y.Z tag exists — by then # a red change is on the release path and the only remedy is a new tag. This is the same gate # moved to where it can still stop something, plus the panel suite, which nothing ran at all: # the release goes through the Dockerfile, and the Dockerfile runs `npm run build`, never # `npm test`. # # The push trigger is limited to main rather than every branch, for the reason release.yml is # not repeated here: a branch with an open PR would otherwise run the whole suite twice per # push, once for refs/heads/<branch> and once for refs/pull/N/merge. Those are different # concurrency groups, so neither cancels the other, and this repository is private and billed # for both. A branch with no PR open yet is the one case that loses coverage, and opening the # PR is what asks for the answer. name: ci on: push: branches: [main] pull_request: permissions: contents: read concurrency: group: ci-${{ github.ref }} cancel-in-progress: true jobs: go: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version-file: go.mod - run: go vet ./... - run: go test ./... shell: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 # bootstrap.sh is the only thing that ever runs on a fresh host, and nothing here can # run it — it wants root, a package manager and k3s. Syntax plus the extracted-block # checks in bootstrap_test.sh is the coverage that is reachable without a machine. # Each file is parsed by the interpreter its own shebang names. A blanket `sh -n` is # wrong and not obviously so: on a developer machine `sh` is usually bash and passes # everything, while the runner's `sh` is dash and rejects bootstrap.sh at the first of # its arrays. Honouring the shebang is what makes local and CI agree. - name: Check shell syntax run: | for f in $(git ls-files '*.sh'); do case "$(head -1 "$f")" in *bash) bash -n "$f" || exit 1 ;; *) sh -n "$f" || exit 1 ;; esac done - run: sh deploy/bootstrap_test.sh panel: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 # The Dockerfile's `FROM node:<major>` is the only place the panel's Node version is # declared — there is no .nvmrc and no engines field. Reading it here rather than # repeating the number keeps CI testing the version a release is actually built on; # a second copy would drift silently the first time the image is bumped. - name: Read the panel's Node version from the Dockerfile id: node run: | version="$(sed -n 's/^FROM.*node:\([0-9][0-9]*\)-.*/\1/p' Dockerfile | head -1)" [ -n "$version" ] || { echo "Dockerfile has no 'FROM ... node:<major>' line"; exit 1; } echo "version=${version}" >> "$GITHUB_OUTPUT" - uses: actions/setup-node@v4 with: node-version: ${{ steps.node.outputs.version }} cache: npm cache-dependency-path: panel/package-lock.json - run: npm ci working-directory: panel - run: npm test working-directory: panel - run: npm run typecheck working-directory: panel
bootstrap_asset_test.go +9 −3 Changes for bootstrap_asset_test.go: 9 added lines, 3 removed lines. Original line number Diff line number Diff line Loading @@ -73,9 +73,15 @@ func TestLobbyLuckPermsWiringIsConsistent(t *testing.T) { // default it can inherit — and nothing else in the install would notice it missing. The failure // surfaces only when a legacy player joins, on a host that installed cleanly. func TestBootstrapPinsViaBlockConnectionsOff(t *testing.T) { // go:embed takes the working tree verbatim, and this repository pins no eol attribute, so // a Windows checkout embeds CRLF. Only the assertion spanning a line break below cares. script := strings.ReplaceAll(BootstrapScript(), "\r\n", "\n") // go:embed takes the working tree verbatim, so the eol attribute is what keeps a Windows // checkout from compiling CRs into the installer. Assert it rather than normalizing them // away: the only assertion that would otherwise notice is the one spanning a line break // below, and it would report a missing pin instead of the line endings. script := BootstrapScript() if strings.Contains(script, "\r\n") { t.Fatal("embedded bootstrap.sh has CRLF line endings; .gitattributes pins *.sh to LF " + "and this script is piped into `bash -s` on a Linux host") } const key = "serverside-blockconnections" if !strings.Contains(script, key+": false") { Loading
cmd/felis/nano.go +10 −2 Changes for cmd/felis/nano.go: 10 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -13,8 +13,16 @@ package main // off-loopback with -listen; see the flag below for why that is an explicit opt-in. // // This is the no-database delivery of the identical brain `felis api` mounts through its // route table (internal/api.HasJoinedHandler). `felis setup --nano` / the bootstrap nano // choice install this as the runtime service; here it just serves. // route table (internal/api.HasJoinedHandler). The bootstrap installer's nano choice — its // `[2] Felis-nano` prompt, or FELIS_INSTALL_MODE=nano — installs this as the // felis-nano.service unit; here it just serves. // // There is deliberately no `felis setup --nano`. setup re-images the host through the full // bootstrap TUI and carries no install-mode parameter anywhere (nothing in Go reads or sets // FELIS_INSTALL_MODE), so such a flag would either re-run the installer — which is what // pointing at the installer already does — or tear a full install down into a nano one, // which is an uninstall, not a flag. This is the same reasoning that makes setup's --dev // refuse and name the installer rather than pretend to choose a channel. import ( "context" Loading
deploy/bootstrap.sh +62 −8 Changes for deploy/bootstrap.sh: 62 added lines, 8 removed lines. Original line number Diff line number Diff line Loading @@ -30,6 +30,14 @@ # FELIS_INSTALL_MODE full|nano — skip the prompt (default: ask on a tty, else full) # FELIS_NANO_LISTEN listen addr for `felis nano` (default: 127.0.0.1:8081 — loopback # only; set a private-network IP to serve an off-host proxy) # FELIS_LEGACY_FORWARDING_SERVERS comma-separated backends that receive their identity # through the handshake address instead of modern forwarding # (default: legacy18). Read once at Velocity start, so changing it # means re-running this script and restarting the proxy. # FELIS_VELOCITY_FORK_JAR path to a Felis-Legacy Velocity fork build to install as the # proxy instead of the stock download (default: unset, stock). # FELIS_VELOCITY_FORK_JAR_SHA256 expected sha256 of that jar. REQUIRED whenever the jar # above is set; the install refuses on a mismatch. # FELIS_GO_VERSION Go toolchain used to build the nano binary (default: 1.26.4) # FELIS_REPO_URL git URL to build from (raw script mode only) # FELIS_VERSION_BOOTSTRAP release|dev — which version to install (default: release). Loading Loading @@ -95,6 +103,11 @@ INSTALL_MODE="${FELIS_INSTALL_MODE:-}" # own players stop getting in. Same-host Velocity reaches 127.0.0.1 fine; a proxy on # another machine must opt in explicitly with FELIS_NANO_LISTEN=<private-ip>:8081. FELIS_NANO_LISTEN="${FELIS_NANO_LISTEN:-127.0.0.1:8081}" # Backends that take their forwarded identity through the handshake address instead of # proxy-wide modern forwarding. See write_velocity_service for why a protocol-47 backend # needs this. Overridable because adding a second 1.8 backend otherwise means editing this # script; it is still a restart-time list, not one that follows the CRs. FELIS_LEGACY_FORWARDING_SERVERS="${FELIS_LEGACY_FORWARDING_SERVERS:-legacy18}" FELIS_GO_VERSION="${FELIS_GO_VERSION:-1.26.4}" PKG_LOCK_TIMEOUT="${PKG_LOCK_TIMEOUT:-${APT_LOCK_TIMEOUT:-900}}" APT_LOCK_TIMEOUT="${APT_LOCK_TIMEOUT:-$PKG_LOCK_TIMEOUT}" Loading Loading @@ -123,9 +136,20 @@ FELIS_VELOCITY_VERSION="${FELIS_VELOCITY_VERSION:-3.5.1}" # # Opt-in because it is unmeasured where it counts: FL-008's probe runs offline-mode # against a stub, and this jar would carry every real Mojang session on the server. # The build lives in Felis-Legacy and is not byte-reproducible, so there is no digest # to pin here — the jar is trusted because that probe certified the build. FELIS_VELOCITY_FORK_JAR="${FELIS_VELOCITY_FORK_JAR:-}" # Expected sha256 of that jar, REQUIRED whenever it is set. Case and internal spaces are # ignored, so whatever sha256sum, Get-FileHash or certutil printed can be pasted as-is. # No digest is hardcoded here: # the build lives in Felis-Legacy and has never been reproduced on a second machine, so # any constant this script carried would pin one machine's output rather than the fork. # # So this is not a supply-chain signature and does not pretend to be one — an operator # who can write the jar can write this value too. What it does buy: a path is not an # identity, and every re-run of this script re-checks it. A truncated copy, a stale build # left at the same path, or the two-patch jar where the three-patch one was meant all # change the digest and stop the install. Naming the digest once is what turns "whatever # is at that path today" into one specific build. FELIS_VELOCITY_FORK_JAR_SHA256="${FELIS_VELOCITY_FORK_JAR_SHA256:-}" # Temurin 25: Velocity 3.5 needs 21+, and 25 is also what a future Velocity 4 requires, # so the runtime does not have to move again when the pin does. Distro JDK packaging is # a lottery across four package managers — a tarball is one code path everywhere (same Loading Loading @@ -1462,12 +1486,31 @@ install_jre() { install_velocity() { install_jre local url tmp local url tmp have want prepare_velocity_layout if [ -n "$FELIS_VELOCITY_FORK_JAR" ]; then [ -f "$FELIS_VELOCITY_FORK_JAR" ] \ || die "FELIS_VELOCITY_FORK_JAR is not a readable file: ${FELIS_VELOCITY_FORK_JAR}" log "installing the Felis-Legacy Velocity fork from ${FELIS_VELOCITY_FORK_JAR}" # Hash stdin, never the path — same reason as install_via_plugins: sha256sum escapes its # output line for a filename carrying a backslash or a newline, and the leading "\" that # adds would fail every comparison below. have="$(sha256sum <"$FELIS_VELOCITY_FORK_JAR" | cut -d' ' -f1)" # Refuse rather than warn. This jar is the proxy every player connects through, and a # warning in an install log is not a gate. The digest is printed so the first run after # a deliberate rebuild is one copy-paste, not an investigation. [ -n "$FELIS_VELOCITY_FORK_JAR_SHA256" ] || die \ "FELIS_VELOCITY_FORK_JAR_SHA256 is required whenever FELIS_VELOCITY_FORK_JAR is set. The jar at that path hashes to ${have}. Check that against the build you meant to install, then re-run with FELIS_VELOCITY_FORK_JAR_SHA256=${have}" # Normalise the operator's digest before comparing. sha256sum prints lowercase, but the # build host is often Windows, where Get-FileHash prints uppercase and certutil has # shipped both with and without spaces between the bytes. All three name the same jar, # so comparing raw would refuse two of the three spellings and word it as tampering. want="$(printf '%s' "$FELIS_VELOCITY_FORK_JAR_SHA256" | tr -d '[:space:]' | tr 'A-Z' 'a-z')" [ "$have" = "$want" ] || die \ "FELIS_VELOCITY_FORK_JAR checksum mismatch: got ${have}, expected ${want}" log "installing the Felis-Legacy Velocity fork from ${FELIS_VELOCITY_FORK_JAR} (sha256 ${have})" atomic_install_file "$FELIS_VELOCITY_FORK_JAR" "${VELOCITY_DIR}/velocity.jar" 0644 root root else log "resolving the newest Velocity ${FELIS_VELOCITY_VERSION} build" Loading Loading @@ -1591,9 +1634,20 @@ install_velocity_service() { # behind ViaVersion, which strips modern forwarding's login-plugin-message when it down-translates # the proxy->backend pipeline to protocol 47; only the handshake field survives Via. The Felis # fork reads this list from -Dfelis.legacy-forwarding.servers and forwards those servers legacy; # every other backend keeps modern+secret untouched. v1 hardcodes the one legacy backend; the # upgrade path is to have the operator render this list from the MinecraftServer CRs. local legacy_forwarding_servers="legacy18" # every other backend keeps modern+secret untouched. # # The list is a JVM system property, so it is fixed for the life of the proxy process and a # change needs a Velocity restart. FELIS_LEGACY_FORWARDING_SERVERS makes that reachable # without editing this script, which is as far as a startup property can go. Having it follow # the MinecraftServer CRs instead is a larger change: the forwarding decision lives in the # fork's patch to Velocity core, not in the Felis plugin, so core would need to read state the # plugin owns and refreshes. # # The -D below is double-quoted in ExecStart on purpose. The fork trims each element, so it # accepts "legacy18, legacy112", but systemd splits ExecStart on whitespace before java ever # sees it -- unquoted, that spelling would hand java a stray "legacy112" argument and the unit # would not start. Quoting keeps the whole property one argv item. local legacy_forwarding_servers="${FELIS_LEGACY_FORWARDING_SERVERS}" cat > "$VELOCITY_SERVICE" <<EOF [Unit] Description=Felis Velocity proxy (Mojang authentication + modern forwarding) Loading @@ -1605,7 +1659,7 @@ Type=simple User=${VELOCITY_USER} Group=${VELOCITY_USER} WorkingDirectory=${VELOCITY_DIR} ExecStart=${JRE_DIR}/bin/java -Xms512M -Xmx1G -XX:+UseG1GC -XX:+ParallelRefProcEnabled -XX:+AlwaysPreTouch -Dmojang.sessionserver=http://${api_ip}:8081/session/minecraft/hasJoined -Dfelis.legacy-forwarding.servers=${legacy_forwarding_servers} -jar ${VELOCITY_DIR}/velocity.jar ExecStart=${JRE_DIR}/bin/java -Xms512M -Xmx1G -XX:+UseG1GC -XX:+ParallelRefProcEnabled -XX:+AlwaysPreTouch -Dmojang.sessionserver=http://${api_ip}:8081/session/minecraft/hasJoined "-Dfelis.legacy-forwarding.servers=${legacy_forwarding_servers}" -jar ${VELOCITY_DIR}/velocity.jar Restart=on-failure RestartSec=5 NoNewPrivileges=yes Loading