diff --git a/docs/minecraft-prism.md b/docs/minecraft-prism.md index 955f7dae..5a97e67a 100755 --- a/docs/minecraft-prism.md +++ b/docs/minecraft-prism.md @@ -1,7 +1,11 @@ # WorldOfSosal: Prism automatic updates -The published client entry point is: -https://store.hectic-lab.com/minecraft/world-of-sosal/ +The published client entry points are: +- https://bfs.band/minecraft/ (BFS / Element host) +- https://store.hectic-lab.com/minecraft/world-of-sosal/ (hectic-lab) + +Each site provides its own Prism ZIP with that site's update URL and matching +server address. Both installs use the same Minecraft world and modpack release. Players import `WorldOfSosal-Prism.zip` into Prism once and approve its pre-launch command. Before each launch, packwiz-installer reconciles the client with the @@ -23,14 +27,12 @@ client export must consume the same archive; publishing only the client can make it incompatible with the running server. ```sh -python3 script/build-prism-pack.py WorldOfSosal.mrpack /tmp/world-of-sosal-release \ - --server store.hectic-lab.com:25568 # Test the client and deploy the matching server release first. -python3 script/publish-prism-pack.py /tmp/world-of-sosal-release hectic-lab +python3 script/publish-prism-mirrors.py WorldOfSosal.mrpack ``` -Use a new output directory for each build. Omit `--server` until the destination -server is chosen. The builder downloads a SHA-256-pinned bootstrap from the +The mirror publisher creates temporary build directories and sets each server +address and update URL automatically. The builder downloads a SHA-256-pinned bootstrap from the packwiz project's release, or accepts it via `--bootstrap /path/to/file.jar`. External mods retain their original URLs and SHA-512 checksums. Embedded mods and configuration are hosted with the release. Both required and optional client mods @@ -151,3 +153,33 @@ SHA-256 was verified before extraction. again automatically deleted it. Existing files were reused from cache, and options.txt retained its checksum. The production pack contents were unchanged. - Restored the instance's regular current/pack.toml update URL. + +## Independent BFS entry point (2026-09-19) + +- Server: `bfs.band:25568`; downloads: https://bfs.band/minecraft/. +- BFS is `bfs.poland.xray` (91.198.166.181), the host of Element. +- `minecraft-wow-tunnel-bfs` connects neuro directly to BFS. The BFS path does + not transit hectic-lab; both tunnels have independent reconnecting services. +- Shared proxy implementation: `nixos/module/generic/minecraft-public-relay.nix`. + Host settings remain in `minecraft-wow-proxy.nix` (hectic-lab) and + `minecraft-wow.nix` (BFS). Only `/minecraft/` is added to the existing BFS + nginx virtual host; Element/Matrix routes remain intact. +- Downloaded BFS ZIP seeds `bfs.band:25568` and uses the stable manifest + `https://bfs.band/minecraft/world-of-sosal/current/pack.toml`. It does not + redirect installation metadata to hectic-lab. Upstream mod and Java/loader + downloads still use their original providers (e.g. Modrinth, GitHub, Mojang). +- Existing hectic-lab instances can be migrated without reinstalling mods: + in Edit / Settings / Custom commands, replace only the manifest URL in + Pre-launch command with the BFS URL above. Change the multiplayer server + address to bfs.band:25568. New users should import the ZIP from BFS. +- `script/publish-prism-mirrors.py` builds host-specific ZIPs from one archive + and publishes both mirrors. It checks that the running neuro server's cached + archive has the same SHA-256. Each host's switch is atomic; publication across + two hosts is sequential, so rerun the command if it exits unsuccessfully. +- Both configurations were deployed; public Minecraft status/ping succeeds + on BFS (~125 ms), HTTPS serves the pack, and Element/Matrix HTTP checks pass. + +Clean installation through the BFS manifest passed: all 141 client mods and +all overrides match the source archive. A second updater run performed no +downloads and preserved options.txt. The public BFS login protocol reached +online authentication; the earlier full GUI login used hectic-lab. diff --git a/nixos/module/generic/minecraft-public-relay.nix b/nixos/module/generic/minecraft-public-relay.nix new file mode 100644 index 00000000..7aa6b64e --- /dev/null +++ b/nixos/module/generic/minecraft-public-relay.nix @@ -0,0 +1,56 @@ +{ ... }: +{ config, lib, pkgs, ... }: +let + cfg = config.services.minecraft-public-relay; +in { + options.services.minecraft-public-relay = { + enable = lib.mkEnableOption "restricted SSH relay for Minecraft"; + publicPort = lib.mkOption { type = lib.types.port; default = 25568; }; + tunnelPort = lib.mkOption { type = lib.types.port; default = 25577; }; + publicKey = lib.mkOption { + type = lib.types.str; + description = "Public SSH key of the Minecraft tunnel client"; + }; + }; + config = lib.mkIf cfg.enable { + networking.firewall.allowedTCPPorts = [ cfg.publicPort ]; + users.groups.mc-wow-relay = { }; + users.users.mc-wow-relay = { + isSystemUser = true; + group = "mc-wow-relay"; + openssh.authorizedKeys.keys = [ + "restrict,port-forwarding,permitlisten=\"127.0.0.1:${toString cfg.tunnelPort}\" ${cfg.publicKey}" + ]; + }; + services.openssh.extraConfig = '' + Match User mc-wow-relay + ClientAliveInterval 15 + ClientAliveCountMax 3 + AllowTcpForwarding remote + PermitListen 127.0.0.1:${toString cfg.tunnelPort} + AllowAgentForwarding no + X11Forwarding no + PermitTTY no + ForceCommand ${pkgs.coreutils}/bin/false + Match all + ''; + systemd.sockets.minecraft-wow-proxy = { + description = "WorldOfSosal WoW public Minecraft port"; + wantedBy = [ "sockets.target" ]; + listenStreams = [ "0.0.0.0:${toString cfg.publicPort}" ]; + }; + systemd.services.minecraft-wow-proxy = { + description = "Forward Minecraft to the neuro reverse tunnel"; + requires = [ "minecraft-wow-proxy.socket" ]; + after = [ "minecraft-wow-proxy.socket" ]; + serviceConfig = { + ExecStart = "${pkgs.systemd}/lib/systemd/systemd-socket-proxyd 127.0.0.1:${toString cfg.tunnelPort}"; + DynamicUser = true; + NoNewPrivileges = true; + ProtectSystem = "strict"; + ProtectHome = true; + PrivateTmp = true; + }; + }; + }; +} diff --git a/nixos/system/bfs.poland.xray/bfs.poland.xray.nix b/nixos/system/bfs.poland.xray/bfs.poland.xray.nix index 1f2ad527..300842a3 100644 --- a/nixos/system/bfs.poland.xray/bfs.poland.xray.nix +++ b/nixos/system/bfs.poland.xray/bfs.poland.xray.nix @@ -16,6 +16,7 @@ matrixClusterSopsFile = flake + "/sus/matrix-cluster.yaml"; in { imports = [ + ./minecraft-wow.nix self.nixosModules.xray-system self.nixosModules.matrix-cluster self.nixosModules.matrix-cluster-users diff --git a/nixos/system/bfs.poland.xray/minecraft-wow.nix b/nixos/system/bfs.poland.xray/minecraft-wow.nix new file mode 100644 index 00000000..f5f95309 --- /dev/null +++ b/nixos/system/bfs.poland.xray/minecraft-wow.nix @@ -0,0 +1,21 @@ +{ ... }: +{ + imports = [ (import ../../module/generic/minecraft-public-relay.nix { }) ]; + services.minecraft-public-relay = { + enable = true; + publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKNWWegOVTOF3EOmam32iP7sMybULMTxsXuC+cEGITQ8 minecraft-wow-relay"; + }; + systemd.tmpfiles.rules = [ "d /var/www/store/minecraft/world-of-sosal 0755 root root -" ]; + services.nginx.virtualHosts."bfs.band".locations = { + "= /minecraft".return = "302 /minecraft/world-of-sosal/"; + "= /minecraft/".return = "302 /minecraft/world-of-sosal/"; + "^~ /minecraft/" = { + root = "/var/www/store"; + extraConfig = '' + autoindex off; + add_header Cache-Control "no-cache"; + try_files $uri $uri/ =404; + ''; + }; + }; +} diff --git a/nixos/system/hectic-lab/minecraft-wow-proxy.nix b/nixos/system/hectic-lab/minecraft-wow-proxy.nix index d40198b4..9b78156e 100644 --- a/nixos/system/hectic-lab/minecraft-wow-proxy.nix +++ b/nixos/system/hectic-lab/minecraft-wow-proxy.nix @@ -1,43 +1,8 @@ -{ pkgs, ... }: +{ ... }: { - # Public entry point; the backend arrives through a restricted reverse tunnel. - networking.firewall.allowedTCPPorts = [ 25568 ]; - users.groups.mc-wow-relay = { }; - users.users.mc-wow-relay = { - isSystemUser = true; - group = "mc-wow-relay"; - openssh.authorizedKeys.keys = [ - "restrict,port-forwarding,permitlisten=\"127.0.0.1:25577\" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKNWWegOVTOF3EOmam32iP7sMybULMTxsXuC+cEGITQ8 minecraft-wow-relay" - ]; - }; - services.openssh.extraConfig = '' - Match User mc-wow-relay - ClientAliveInterval 15 - ClientAliveCountMax 3 - AllowTcpForwarding remote - PermitListen 127.0.0.1:25577 - AllowAgentForwarding no - X11Forwarding no - PermitTTY no - ForceCommand ${pkgs.coreutils}/bin/false - Match all - ''; - systemd.sockets.minecraft-wow-proxy = { - description = "WorldOfSosal WoW public Minecraft port"; - wantedBy = [ "sockets.target" ]; - listenStreams = [ "0.0.0.0:25568" ]; - }; - systemd.services.minecraft-wow-proxy = { - description = "Forward Minecraft to the neuro reverse tunnel"; - requires = [ "minecraft-wow-proxy.socket" ]; - after = [ "minecraft-wow-proxy.socket" ]; - serviceConfig = { - ExecStart = "${pkgs.systemd}/lib/systemd/systemd-socket-proxyd 127.0.0.1:25577"; - DynamicUser = true; - NoNewPrivileges = true; - ProtectSystem = "strict"; - ProtectHome = true; - PrivateTmp = true; - }; + imports = [ (import ../../module/generic/minecraft-public-relay.nix { }) ]; + services.minecraft-public-relay = { + enable = true; + publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKNWWegOVTOF3EOmam32iP7sMybULMTxsXuC+cEGITQ8 minecraft-wow-relay"; }; } diff --git a/nixos/system/neuro/minecraft/public-tunnel.nix b/nixos/system/neuro/minecraft/public-tunnel.nix index 12b4385e..21687fc6 100644 --- a/nixos/system/neuro/minecraft/public-tunnel.nix +++ b/nixos/system/neuro/minecraft/public-tunnel.nix @@ -1,5 +1,24 @@ { config, pkgs, ... }: -{ +let + mkTunnel = relay: { + description = "WorldOfSosal WoW reverse tunnel to ${relay.name}"; + startLimitIntervalSec = 0; + wantedBy = [ "multi-user.target" ]; + wants = [ "network-online.target" ]; + after = [ "network-online.target" ]; + serviceConfig = { + User = "mc-wow-tunnel"; + Group = "mc-wow-tunnel"; + ExecStart = "${pkgs.openssh}/bin/ssh -NT -i ${config.sops.secrets."minecraft/wow-tunnel-key".path} -o IPQoS=none -o Ciphers=aes256-ctr -o MACs=hmac-sha2-256-etm@openssh.com -o KexAlgorithms=curve25519-sha256 -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes -o UserKnownHostsFile=/etc/ssh/ssh_known_hosts -o ExitOnForwardFailure=yes -o ServerAliveInterval=15 -o ServerAliveCountMax=3 -o ConnectTimeout=10 -R 127.0.0.1:25577:127.0.0.1:25567 mc-wow-relay@${relay.address}"; + Restart = "always"; + RestartSec = 10; + NoNewPrivileges = true; + ProtectSystem = "strict"; + ProtectHome = true; + PrivateTmp = true; + }; + }; +in { users.groups.mc-wow-tunnel = { }; users.users.mc-wow-tunnel = { isSystemUser = true; @@ -15,22 +34,16 @@ hostNames = [ "128.140.75.58" ]; publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAFpr4DPSaJt0xeuGIfcZBJD3LsJHTdIRIs2Tt9HF+CT"; }; - systemd.services.minecraft-wow-tunnel = { - description = "WorldOfSosal WoW reverse tunnel to hectic-lab"; - startLimitIntervalSec = 0; - wantedBy = [ "multi-user.target" ]; - wants = [ "network-online.target" ]; - after = [ "network-online.target" ]; - serviceConfig = { - User = "mc-wow-tunnel"; - Group = "mc-wow-tunnel"; - ExecStart = "${pkgs.openssh}/bin/ssh -NT -i ${config.sops.secrets."minecraft/wow-tunnel-key".path} -o IPQoS=none -o Ciphers=aes256-ctr -o MACs=hmac-sha2-256-etm@openssh.com -o KexAlgorithms=curve25519-sha256 -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes -o UserKnownHostsFile=/etc/ssh/ssh_known_hosts -o ExitOnForwardFailure=yes -o ServerAliveInterval=15 -o ServerAliveCountMax=3 -o ConnectTimeout=10 -R 127.0.0.1:25577:127.0.0.1:25567 mc-wow-relay@128.140.75.58"; - Restart = "always"; - RestartSec = 10; - NoNewPrivileges = true; - ProtectSystem = "strict"; - ProtectHome = true; - PrivateTmp = true; - }; + programs.ssh.knownHosts.minecraft-wow-relay-bfs = { + hostNames = [ "91.198.166.181" ]; + publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICcCn57nlWY5QyEz17kxuAbIX9PkjPwtlGzdJyhy+SQQ"; + }; + systemd.services.minecraft-wow-tunnel = mkTunnel { + name = "hectic-lab"; + address = "128.140.75.58"; + }; + systemd.services.minecraft-wow-tunnel-bfs = mkTunnel { + name = "bfs.band"; + address = "91.198.166.181"; }; } diff --git a/script/publish-prism-mirrors.py b/script/publish-prism-mirrors.py new file mode 100644 index 00000000..93e17169 --- /dev/null +++ b/script/publish-prism-mirrors.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +"""Build and publish the same tested server pack on both independent entry points.""" +import argparse +import hashlib +from pathlib import Path +import subprocess +import sys +import tempfile +import zipfile + + +def main(): + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument('mrpack', type=Path) + ap.add_argument('--bootstrap', type=Path) + args = ap.parse_args() + archive = args.mrpack.resolve() + expected = hashlib.sha256(archive.read_bytes()).hexdigest() + # Publishing a client before deploying its server can prevent players joining. + deployed = subprocess.run([ + 'ssh', '-o', 'BatchMode=yes', 'neuro', + 'systemctl is-active --quiet minecraft-server-wowMineMap && ' + 'sha256sum /var/lib/minecraft-modpacks/worldOfSosal/WorldOfSosal.mrpack', + ], capture_output=True, text=True, check=True).stdout.split()[0] + if deployed != expected: + ap.error('Deploy this mrpack on neuro first: the server archive hash differs') + scripts = Path(__file__).resolve().parent + mirrors = [ + ('hectic-lab', 'https://store.hectic-lab.com/minecraft/world-of-sosal/', 'store.hectic-lab.com:25568'), + ('bfs.poland.xray', 'https://bfs.band/minecraft/world-of-sosal/', 'bfs.band:25568'), + ] + with tempfile.TemporaryDirectory(prefix='prism-mirrors-') as temporary: + root = Path(temporary) + bootstrap = args.bootstrap.resolve() if args.bootstrap else None + builds = [] + for host, url, server in mirrors: + output = root / host + command = [sys.executable, str(scripts / 'build-prism-pack.py'), + str(archive), str(output), '--base-url', url, '--server', server] + if bootstrap: + command += ['--bootstrap', str(bootstrap)] + subprocess.run(command, check=True) + if bootstrap is None: + bootstrap = root / 'packwiz-installer-bootstrap.jar' + with zipfile.ZipFile(output / 'WorldOfSosal-Prism.zip') as z: + bootstrap.write_bytes(z.read('.minecraft/packwiz-installer-bootstrap.jar')) + builds.append((host, url, output)) + # Each switch is atomic on its host. A failed publication exits nonzero; + # rerunning safely verifies existing releases and retries both mirrors. + for host, url, output in builds: + subprocess.run([sys.executable, str(scripts / 'publish-prism-pack.py'), + str(output), host], check=True) + print(url, flush=True) + + +if __name__ == '__main__': + main() diff --git a/script/publish-prism-pack.py b/script/publish-prism-pack.py index 9402bfdb..440002c4 100755 --- a/script/publish-prism-pack.py +++ b/script/publish-prism-pack.py @@ -46,4 +46,4 @@ mv -Tf "$stage/current" "$base/current" echo "Published $release" '''.replace('RELEASE', release) subprocess.run(['ssh', '-o', 'BatchMode=yes', host, 'sh', '-s'], input=script, text=True, check=True) -print('https://store.hectic-lab.com/minecraft/world-of-sosal/') +print(f'Published client files on {host}:/var/www/store/minecraft/world-of-sosal')