From 9f98934a17f4106f5ca94e17eadf0141f443f7c5 Mon Sep 17 00:00:00 2001 From: arcnmx Date: Sat, 27 Apr 2024 13:30:33 -0700 Subject: [PATCH] feat(minecraft): bedrock server --- modules/nixos/minecraft-bedrock.nix | 153 +++++++++++++++++++ modules/system/exports/minecraft-bedrock.nix | 40 +++++ nixos/dnsmasq.nix | 54 +++++-- nixos/minecraft/bedrock.nix | 43 ++++++ overlays/default.nix | 2 + overlays/minecraft.nix | 57 +++++++ packages/minecraft-bedrock.nix | 40 +++++ systems/aya/default.nix | 1 + systems/aya/lxc.json | 1 + systems/aya/nixos.nix | 1 + systems/reisen/setup.sh | 2 + 11 files changed, 383 insertions(+), 11 deletions(-) create mode 100644 modules/nixos/minecraft-bedrock.nix create mode 100644 modules/system/exports/minecraft-bedrock.nix create mode 100644 nixos/minecraft/bedrock.nix create mode 100644 overlays/minecraft.nix create mode 100644 packages/minecraft-bedrock.nix diff --git a/modules/nixos/minecraft-bedrock.nix b/modules/nixos/minecraft-bedrock.nix new file mode 100644 index 00000000..21c7e0ec --- /dev/null +++ b/modules/nixos/minecraft-bedrock.nix @@ -0,0 +1,153 @@ +{ config, lib, pkgs, ... }: let + # see https://gist.github.com/datakurre/cfdf627fb23ed8ff62bb7b3520b92674 + inherit (lib.options) mkOption mkPackageOption; + inherit (lib.modules) mkIf; + inherit (lib.attrsets) mapAttrsToList; + inherit (lib.strings) concatStringsSep; + inherit (lib.trivial) boolToString; + cfg = config.services.minecraft-bedrock-server; + + cfgToString = v: if builtins.isBool v then boolToString v else toString v; + + serverPropertiesFile = pkgs.writeText "server.properties" ('' + # server.properties managed by NixOS configuration + '' + concatStringsSep "\n" (mapAttrsToList + (n: v: "${n}=${cfgToString v}") cfg.serverProperties)); +in { + options.services.minecraft-bedrock-server = with lib.types; { + enable = mkOption { + type = bool; + default = false; + description = '' + If enabled, start a Minecraft Bedrock Server. The server + data will be loaded from and saved to + . + ''; + }; + + dataDir = mkOption { + type = path; + default = "/var/lib/minecraft-bedrock"; + description = '' + Directory to store Minecraft Bedrock database and other state/data files. + ''; + }; + + serverProperties = mkOption { + type = attrsOf (oneOf [ bool int str float ]); + default = { + server-name = "Dedicated Server"; + gamemode = "survival"; + difficulty = "easy"; + allow-cheats = false; + max-players = 10; + online-mode = false; + white-list = false; + server-port = 19132; + server-portv6 = 19133; + view-distance = 32; + tick-distance = 4; + player-idle-timeout = 30; + max-threads = 8; + level-name = "Bedrock level"; + level-seed = ""; + default-player-permission-level = "member"; + texturepack-required = false; + content-log-file-enabled = false; + compression-threshold = 1; + server-authoritative-movement = "server-auth"; + player-movement-score-threshold = 20; + player-movement-distance-threshold = 0.3; + player-movement-duration-threshold-in-ms = 500; + correct-player-movement = false; + }; + example = literalExample '' + { + server-name = "Dedicated Server"; + gamemode = "survival"; + difficulty = "easy"; + allow-cheats = false; + max-players = 10; + online-mode = false; + white-list = false; + server-port = 19132; + server-portv6 = 19133; + view-distance = 32; + tick-distance = 4; + player-idle-timeout = 30; + max-threads = 8; + level-name = "Bedrock level"; + level-seed = ""; + default-player-permission-level = "member"; + texturepack-required = false; + content-log-file-enabled = false; + compression-threshold = 1; + server-authoritative-movement = "server-auth"; + player-movement-score-threshold = 20; + player-movement-distance-threshold = 0.3; + player-movement-duration-threshold-in-ms = 500; + correct-player-movement = false; + } + ''; + description = '' + Minecraft Bedrock server properties for the server.properties file. + ''; + }; + + package = mkPackageOption pkgs "minecraft-bedrock-server" { }// { + description = "Version of minecraft-bedrock-server to run."; + }; + + openFirewall = mkOption { + type = bool; + default = false; + }; + + user = mkOption { + type = str; + default = "minecraft-bedrock"; + }; + group = mkOption { + type = str; + default = cfg.user; + }; + }; + + config = mkIf cfg.enable { + users.users.${cfg.user} = { + inherit (cfg) group; + description = "Minecraft server service user"; + home = cfg.dataDir; + createHome = true; + isSystemUser = true; + }; + users.groups.${cfg.group} = {}; + + systemd.services.minecraft-bedrock-server = { + description = "Minecraft Bedrock Server Service"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + + serviceConfig = { + ExecStart = [ + "${cfg.package}/bin/bedrock_server" + ]; + Restart = "always"; + User = cfg.user; + WorkingDirectory = cfg.dataDir; + }; + + preStart = '' + cp -a -n ${cfg.package}/var/lib/* . + cp -f ${serverPropertiesFile} server.properties + chmod +w server.properties + ''; + }; + + networking.firewall = let + ports = [ cfg.serverProperties.server-port cfg.serverProperties.server-portv6 ]; + in mkIf cfg.openFirewall { + allowedUDPPorts = ports; + }; + }; +} diff --git a/modules/system/exports/minecraft-bedrock.nix b/modules/system/exports/minecraft-bedrock.nix new file mode 100644 index 00000000..e3cb216a --- /dev/null +++ b/modules/system/exports/minecraft-bedrock.nix @@ -0,0 +1,40 @@ +{lib, gensokyo-zone, ...}: let + inherit (gensokyo-zone.lib) mapAlmostOptionDefaults mkAlmostOptionDefault; + inherit (lib.modules) mkIf; + inherit (lib.attrsets) mapAttrs; +in { + config.exports.services.minecraft-bedrock-server = { config, ... }: let + mkAssertion = f: nixosConfig: let + cfg = nixosConfig.services.minecraft-bedrock-server; + in f nixosConfig cfg; + in { + nixos = { + serviceAttr = "minecraft-bedrock-server"; + assertions = mkIf config.enable [ + (mkAssertion (nixosConfig: cfg: { + assertion = config.ports.default.port == cfg.serverProperties.server-port; + message = "server-port mismatch"; + })) + (mkAssertion (nixosConfig: cfg: { + assertion = config.ports.v6.port == cfg.serverProperties.server-portv6; + message = "server-portv6 mismatch"; + })) + ]; + }; + defaults.port.listen = mkAlmostOptionDefault "lan"; + ports = mapAttrs (_: mapAlmostOptionDefaults) { + default = { + port = 19132; + transport = "udp"; + }; + tcp = { + port = config.ports.default.port; + transport = "tcp"; + }; + v6 = { + port = 19133; + transport = "udp"; + }; + }; + }; +} diff --git a/nixos/dnsmasq.nix b/nixos/dnsmasq.nix index 5923aa22..6f856713 100644 --- a/nixos/dnsmasq.nix +++ b/nixos/dnsmasq.nix @@ -1,17 +1,18 @@ { config, lib, - inputs, + access, + gensokyo-zone, ... }: let inherit (lib.options) mkOption; - inherit (lib.modules) mkIf mkBefore mkDefault mkForce; + inherit (lib.modules) mkIf mkMerge mkBefore mkDefault mkForce; inherit (lib.attrsets) filterAttrs mapAttrsToList nameValuePair listToAttrs; - inherit (lib.lists) filter concatLists; + inherit (lib.lists) filter optional singleton concatLists; inherit (lib.strings) hasPrefix replaceStrings concatStringsSep; - inherit (lib.trivial) mapNullable; + inherit (lib.trivial) mapNullable flip; cfg = config.services.dnsmasq; - inherit (inputs.self.lib) systems; + inherit (gensokyo-zone) systems; localSystems = filterAttrs (_: system: system.config.access.online.enable && system.config.network.networks.local.enable or false ) systems; @@ -27,11 +28,11 @@ address6 = system.config.network.networks.local.address6 or null; in concatStringsSep "," ([ system.config.access.fqdn - ] ++ lib.optional (address4 != null) + ] ++ optional (address4 != null) (toString (mapNullable mapDynamic4 address4)) - ++ lib.optional (address6 != null) + ++ optional (address6 != null) (toString (mapNullable mapDynamic6 address6)) - ++ lib.singleton + ++ singleton cfg.dynamic.interface ); mkHostRecordPair = network: system: let @@ -41,9 +42,9 @@ in nameValuePair (if fqdn != null then fqdn else "${network}.${system.config.access.fqdn}") (concatStringsSep "," ( - lib.optional (address4 != null) + optional (address4 != null) (toString address4) - ++ lib.optional (address6 != null) + ++ optional (address6 != null) (toString address6) )); systemHosts = filterAttrs (_: value: value != "") ( @@ -63,13 +64,38 @@ in { type = str; default = config.systemd.network.networks._00-local.name or "eth0"; }; + bedrockConnect = { + address = mkOption { + type = nullOr str; + }; + address6 = mkOption { + type = nullOr str; + }; + }; }; config = { services.dnsmasq = { enable = mkDefault true; resolveLocalQueries = mkForce false; settings = { - host-record = mapAttrsToList mkHostRecord systemHosts; + host-record = let + bedrockRecord = concatStringsSep "," ( + optional (cfg.bedrockConnect.address != null) cfg.bedrockConnect.address + ++ optional (cfg.bedrockConnect.address6 != null) cfg.bedrockConnect.address6 + ); + bedrockRecordNames = [ + # https://github.com/Pugmatt/BedrockConnect?tab=readme-ov-file#using-your-own-dns-server + "geo.hivebedrock.network" + "hivebedrock.network" + "play.inpvp.net" + "mco.lbsg.net" + "play.galaxite.net" + ]; + bedrockRecords = map (flip mkHostRecord bedrockRecord) bedrockRecordNames; + in mkMerge [ + (mapAttrsToList mkHostRecord systemHosts) + (mkIf (cfg.bedrockConnect.address != null || cfg.bedrockConnect.address6 != null) bedrockRecords) + ]; dynamic-host = mapAttrsToList mkDynamicHostRecord localSystems; server = if config.networking.nameservers' != [ ] then map (ns: ns.address) (filter filterns' config.networking.nameservers') @@ -77,6 +103,12 @@ in { ; max-cache-ttl = 60; }; + bedrockConnect = let + system = access.systemForService "minecraft-bedrock-server"; + in { + address = mkDefault (access.getAddress4For system.name "local"); + address6 = mkDefault (access.getAddress6For system.name "local"); + }; }; services.resolved = mkIf cfg.enable { extraConfig = '' diff --git a/nixos/minecraft/bedrock.nix b/nixos/minecraft/bedrock.nix new file mode 100644 index 00000000..f48361f5 --- /dev/null +++ b/nixos/minecraft/bedrock.nix @@ -0,0 +1,43 @@ +{config, lib, ...}: let + inherit (lib.modules) mkIf mkDefault; + cfg = config.services.minecraft-bedrock-server; +in { + services.minecraft-bedrock-server = { + enable = mkDefault true; + serverProperties = { + server-name = "Kat's Server"; + gamemode = "survival"; + difficulty = "easy"; + allow-cheats = false; + max-players = 10; + online-mode = true; + white-list = false; + server-port = 19132; + server-portv6 = 19133; + view-distance = 32; + tick-distance = 4; + player-idle-timeout = 30; + max-threads = 8; + level-name = "Bedrock level"; + level-seed = ""; + default-player-permission-level = "member"; + texturepack-required = false; + content-log-file-enabled = false; + compression-threshold = 1; + server-authoritative-movement = "server-auth"; + player-movement-score-threshold = 20; + player-movement-distance-threshold = 0.3; + player-movement-duration-threshold-in-ms = 500; + correct-player-movement = false; + }; + }; + users = mkIf cfg.enable { + users.${cfg.user}.uid = 913; + groups.${cfg.group}.gid = config.users.users.${cfg.user}.uid; + }; + networking.firewall.interfaces.local = let + ports = [ cfg.serverProperties.server-port cfg.serverProperties.server-portv6 ]; + in mkIf cfg.enable { + allowedUDPPorts = ports; + }; +} diff --git a/overlays/default.nix b/overlays/default.nix index 116b888d..9e0a079b 100644 --- a/overlays/default.nix +++ b/overlays/default.nix @@ -6,6 +6,7 @@ in rec { default = nixlib.composeManyExtensions [ barcodebuddy + minecraft krb5 nfs nginx @@ -13,6 +14,7 @@ in rec { ]; barcodebuddy = import ./barcodebuddy.nix; krb5 = import ./krb5.nix; + minecraft = import ./minecraft.nix; nfs = import ./nfs.nix; nginx = import ./nginx.nix; samba = import ./samba.nix; diff --git a/overlays/minecraft.nix b/overlays/minecraft.nix new file mode 100644 index 00000000..30596673 --- /dev/null +++ b/overlays/minecraft.nix @@ -0,0 +1,57 @@ +final: prev: let + inherit (final) lib; +in { + minecraft-bedrock-server-libCrypto = let + inherit (final) minecraft-bedrock-server; + in minecraft-bedrock-server.stdenv.mkDerivation { + pname = "${minecraft-bedrock-server.pname}-libcrypto"; + inherit (minecraft-bedrock-server) version src sourceRoot; + nativeBuildInputs = with final; [ + autoPatchelfHook + curl + gcc-unwrapped + openssl + unzip + ]; + installPhase = '' + install -m755 -D libCrypto.so $out/lib/libCrypto.so + ''; + fixupPhase = '' + autoPatchelf $out/lib/libCrypto.so + ''; + meta.broken = true; + }; + + minecraft-bedrock-server-patchdebug = let + # https://github.com/minecraft-linux/server-modloader/tree/master?tab=readme-ov-file#getting-mods-to-work-on-newer-versions-116 + python = final.python3.withPackages (p: [ p.lief ]); + script = '' + import lief + import sys + + lib_symbols = lief.parse(sys.argv[1]) + for s in filter(lambda e: e.exported, lib_symbols.static_symbols): + lib_symbols.add_dynamic_symbol(s) + lib_symbols.write(sys.argv[2]) + ''; + name = "minecraft-bedrock-server-patchdebug"; + in final.writeTextFile { + name = "${name}.py"; + destination = "/bin/${name}"; + executable = true; + text = '' + #!${lib.getExe python} + ${script} + ''; + meta.mainProgram = name; + }; + + minecraft-bedrock-server-patchelf = prev.patchelf.overrideDerivation (old: { + postPatch = '' + substituteInPlace src/patchelf.cc \ + --replace "32 * 1024 * 1024" "512 * 1024 * 1024" + ''; + }); + + minecraft-bedrock-server = final.callPackage ../packages/minecraft-bedrock.nix { }; +} diff --git a/packages/minecraft-bedrock.nix b/packages/minecraft-bedrock.nix new file mode 100644 index 00000000..6315d58b --- /dev/null +++ b/packages/minecraft-bedrock.nix @@ -0,0 +1,40 @@ +{ stdenv +, fetchurl +, minecraft-bedrock-server-patchelf +, minecraft-bedrock-server-patchdebug +#, minecraft-bedrock-server-libCrypto +, autoPatchelfHook +, curl, gcc-unwrapped, openssl, unzip +}: stdenv.mkDerivation rec { + pname = "minecraft-bedrock-server"; + version = "1.20.80.05"; + src = fetchurl { + url = "https://minecraft.azureedge.net/bin-linux/bedrock-server-${version}.zip"; + sha256 = "sha256-6vZx29FOXRR7Rzx82Axo3a/Em+9cpK7Hj3cuDRnW9+8="; + }; + sourceRoot = "."; + nativeBuildInputs = [ + minecraft-bedrock-server-patchelf + minecraft-bedrock-server-patchdebug + autoPatchelfHook + curl + gcc-unwrapped + #minecraft-bedrock-server-libCrypto + openssl + unzip + ]; + buildPhase = '' + minecraft-bedrock-server-patchdebug bedrock_server_symbols.debug bedrock_server_symbols_patched.debug + ''; + installPhase = '' + install -m755 -D bedrock_server $out/bin/bedrock_server + rm bedrock_server + rm server.properties + mkdir -p $out/var + cp -a . $out/var/lib + ''; + fixupPhase = '' + autoPatchelf $out/bin/bedrock_server + ''; + dontStrip = true; +} diff --git a/systems/aya/default.nix b/systems/aya/default.nix index cb8a7a98..ecc8eb06 100644 --- a/systems/aya/default.nix +++ b/systems/aya/default.nix @@ -11,6 +11,7 @@ _: { services = { sshd.enable = true; tailscale.enable = true; + minecraft-bedrock-server.enable = true; }; }; } diff --git a/systems/aya/lxc.json b/systems/aya/lxc.json index 4a81eb72..0fac5294 100644 --- a/systems/aya/lxc.json +++ b/systems/aya/lxc.json @@ -3,6 +3,7 @@ "lxc.mount.entry": [ "/rpool/shared/nix/store nix/store none bind,create=dir", "/rpool/shared/nix/var nix/var none bind,create=dir", + "/rpool/shared/minecraft/bedrock mnt/shared/minecraft/bedrock none bind,optional,create=dir", "/dev/net/tun dev/net/tun none bind,optional,create=file" ], "lxc.idmap": [ diff --git a/systems/aya/nixos.nix b/systems/aya/nixos.nix index 97891dc9..cc4fb567 100644 --- a/systems/aya/nixos.nix +++ b/systems/aya/nixos.nix @@ -8,6 +8,7 @@ nixos.nixbld nixos.tailscale nixos.github-runner.zone + nixos.minecraft.bedrock ]; nix.gc = { diff --git a/systems/reisen/setup.sh b/systems/reisen/setup.sh index 47c1941a..b35b1781 100644 --- a/systems/reisen/setup.sh +++ b/systems/reisen/setup.sh @@ -160,6 +160,8 @@ mkshared plex 100193 100193 0750 mkshared postgresql 100071 100071 0750 mkshared unifi 100990 100990 0750 mkshared zigbee2mqtt 100317 100317 0700 +mkshared minecraft 0 0 0750 +mkshared minecraft/bedrock 100913 100913 0750 ln -sf /lib/systemd/system/auth-rpcgss-module.service /etc/systemd/system/ mkdir -p /etc/systemd/system/auth-rpcgss-module.service.d