2024-01-25 18:09:52 +05:00
|
|
|
{ self, ... }:
|
|
|
|
{ config, lib, pkgs, ... }:
|
|
|
|
with lib;
|
|
|
|
let
|
|
|
|
cfg = config.services.papermc;
|
|
|
|
|
|
|
|
eulaFile = builtins.toFile "eula.txt" ''
|
|
|
|
# eula.txt managed by NixOS Configuration
|
|
|
|
eula=true
|
|
|
|
'';
|
|
|
|
|
|
|
|
whitelistFile = pkgs.writeText "whitelist.json"
|
|
|
|
(builtins.toJSON cfg.whitelist);
|
|
|
|
|
|
|
|
opsFile = pkgs.writeText "whitelist.json"
|
|
|
|
(builtins.toJSON cfg.ops);
|
|
|
|
|
|
|
|
cfgToString = v: if builtins.isBool v then boolToString v else toString v;
|
|
|
|
|
|
|
|
serverPropertiesFile = let
|
|
|
|
serverProperties' = if (cfg.rconPasswordFile == null) then cfg.serverProperties else
|
|
|
|
(removeAttrs cfg.serverProperties [ "rcon.password" ]);
|
|
|
|
in pkgs.writeText "server.properties" (''
|
|
|
|
# server.properties managed by NixOS configuration
|
|
|
|
'' + concatStringsSep "\n" (mapAttrsToList
|
|
|
|
(n: v: "${n}=${cfgToString v}") serverProperties') +
|
|
|
|
lib.optionalString (cfg.rconPasswordFile != null) "\nrcon.password=#rconpass#");
|
|
|
|
|
|
|
|
stopScript = pkgs.writeShellScript "minecraft-server-stop" ''
|
|
|
|
echo stop > ${config.systemd.sockets.papermc.socketConfig.ListenFIFO}
|
|
|
|
|
|
|
|
# Wait for the PID of the minecraft server to disappear before
|
|
|
|
# returning, so systemd doesn't attempt to SIGKILL it.
|
|
|
|
while kill -0 "$1" 2> /dev/null; do
|
|
|
|
sleep 1s
|
|
|
|
done
|
|
|
|
'';
|
|
|
|
|
|
|
|
defaultServerPort = 25565;
|
|
|
|
|
|
|
|
serverPort = cfg.serverProperties.server-port or defaultServerPort;
|
|
|
|
|
|
|
|
rconPort = if cfg.serverProperties.enable-rcon or false
|
|
|
|
then cfg.serverProperties."rcon.port" or 25575
|
|
|
|
else null;
|
|
|
|
|
|
|
|
queryPort = if cfg.serverProperties.enable-query or false
|
|
|
|
then cfg.serverProperties."query.port" or 25565
|
|
|
|
else null;
|
|
|
|
|
|
|
|
in {
|
|
|
|
options.services.papermc = {
|
|
|
|
enable = mkEnableOption "Enables the PaperMC service.";
|
|
|
|
|
2024-04-24 14:26:24 +05:00
|
|
|
openFirewall = mkOption {
|
2024-01-25 18:09:52 +05:00
|
|
|
type = types.bool;
|
|
|
|
default = false;
|
|
|
|
description = lib.mdDoc ''
|
|
|
|
Whether to open ports in the firewall for the server.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
eula = mkOption {
|
|
|
|
type = types.bool;
|
|
|
|
default = false;
|
|
|
|
description = lib.mdDoc ''
|
|
|
|
Whether you agree to [Mojangs EULA](https://account.mojang.com/documents/minecraft_eula).
|
|
|
|
This option must be set to `true` to run Minecraft server.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
dataDir = mkOption {
|
|
|
|
type = types.path;
|
|
|
|
default = "/var/lib/papermc";
|
|
|
|
description = lib.mdDoc ''
|
|
|
|
Directory to store Minecraft database and other state/data files.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
whitelist = mkOption {
|
|
|
|
type = types.listOf types.attrs;
|
|
|
|
default = {};
|
|
|
|
description = lib.mdDoc ''
|
|
|
|
This is a mapping from Minecraft usernames to UUIDs.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
ops = mkOption {
|
|
|
|
type = types.listOf types.attrs;
|
|
|
|
default = {};
|
|
|
|
};
|
|
|
|
|
|
|
|
serverProperties = mkOption {
|
|
|
|
type = with types; attrsOf (oneOf [ bool int str ]);
|
|
|
|
default = {
|
|
|
|
"rcon.password" = mkIf (cfg.rconPasswordFile != null) "#rconpass#";
|
|
|
|
};
|
|
|
|
example = literalExpression ''
|
|
|
|
{
|
|
|
|
server-port = 43000;
|
|
|
|
difficulty = 3;
|
|
|
|
gamemode = 1;
|
|
|
|
max-players = 5;
|
|
|
|
motd = "NixOS Minecraft server!";
|
|
|
|
white-list = true;
|
|
|
|
enable-rcon = true;
|
|
|
|
"rcon.password" = "hunter2";
|
|
|
|
}
|
|
|
|
'';
|
|
|
|
description = lib.mdDoc ''
|
|
|
|
Minecraft server properties for the server.properties file. See
|
|
|
|
<https://minecraft.gamepedia.com/Server.properties#Java_Edition_3>
|
|
|
|
for documentation on these values.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
rconPasswordFile = mkOption {
|
|
|
|
type = types.nullOr types.str;
|
|
|
|
default = null;
|
|
|
|
example = "/var/lib/secrets/papermc/rconpw";
|
|
|
|
};
|
|
|
|
|
|
|
|
package = mkPackageOption pkgs "papermc" {
|
|
|
|
example = "papermc_6_6_6";
|
|
|
|
};
|
|
|
|
|
|
|
|
jvmOpts = mkOption {
|
|
|
|
type = types.separatedString " ";
|
|
|
|
default = "-Xmx2048M -Xms2048M";
|
|
|
|
# Example options from https://minecraft.gamepedia.com/Tutorials/Server_startup_script
|
|
|
|
example = "-Xms4092M -Xmx4092M -XX:+UseG1GC -XX:+CMSIncrementalPacing "
|
|
|
|
+ "-XX:+CMSClassUnloadingEnabled -XX:ParallelGCThreads=2 "
|
|
|
|
+ "-XX:MinHeapFreeRatio=5 -XX:MaxHeapFreeRatio=10";
|
|
|
|
description = lib.mdDoc "JVM options for the Minecraft server.";
|
|
|
|
};
|
|
|
|
|
|
|
|
extraPreStart = mkOption {
|
|
|
|
type = types.lines;
|
|
|
|
default = '''';
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
config = mkIf cfg.enable {
|
|
|
|
users.users.papermc = {
|
|
|
|
description = "Minecraft server service user";
|
|
|
|
home = cfg.dataDir;
|
|
|
|
createHome = true;
|
|
|
|
isSystemUser = true;
|
|
|
|
group = "papermc";
|
|
|
|
};
|
|
|
|
users.groups.papermc = {};
|
|
|
|
|
|
|
|
systemd.sockets.papermc = {
|
|
|
|
bindsTo = [ "papermc.service" ];
|
|
|
|
socketConfig = {
|
|
|
|
ListenFIFO = "/run/papermc.stdin";
|
|
|
|
SocketMode = "0660";
|
|
|
|
SocketUser = "papermc";
|
|
|
|
SocketGroup = "papermc";
|
|
|
|
RemoveOnStop = true;
|
|
|
|
FlushPending = true;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
systemd.services.papermc = {
|
|
|
|
description = "PaperMC Service";
|
|
|
|
wantedBy = [ "multi-user.target" ];
|
|
|
|
requires = [ "papermc.socket" ];
|
|
|
|
after = [ "network.target" "papermc.socket" ];
|
|
|
|
|
|
|
|
serviceConfig = {
|
|
|
|
ExecStart = "${cfg.package}/bin/minecraft-server ${cfg.jvmOpts}";
|
|
|
|
ExecStop = "${stopScript} $MAINPID";
|
|
|
|
Restart = "always";
|
|
|
|
User = "papermc";
|
|
|
|
WorkingDirectory = cfg.dataDir;
|
|
|
|
|
|
|
|
StandardInput = "socket";
|
|
|
|
StandardOutput = "journal";
|
|
|
|
StandardError = "journal";
|
|
|
|
|
|
|
|
# Hardening
|
|
|
|
CapabilityBoundingSet = [ "" ];
|
|
|
|
DeviceAllow = [ "" ];
|
|
|
|
LockPersonality = true;
|
|
|
|
PrivateDevices = true;
|
|
|
|
PrivateTmp = true;
|
|
|
|
PrivateUsers = true;
|
|
|
|
ProtectClock = true;
|
|
|
|
ProtectControlGroups = true;
|
|
|
|
ProtectHome = true;
|
|
|
|
ProtectHostname = true;
|
|
|
|
ProtectKernelLogs = true;
|
|
|
|
ProtectKernelModules = true;
|
|
|
|
ProtectKernelTunables = true;
|
|
|
|
ProtectProc = "invisible";
|
|
|
|
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
|
|
|
|
RestrictNamespaces = true;
|
|
|
|
RestrictRealtime = true;
|
|
|
|
RestrictSUIDSGID = true;
|
|
|
|
SystemCallArchitectures = "native";
|
|
|
|
UMask = "0077";
|
|
|
|
};
|
|
|
|
|
|
|
|
preStart = let
|
|
|
|
replaceSecretBin = "${pkgs.replace-secret}/bin/replace-secret";
|
|
|
|
in ''
|
|
|
|
ln -sf ${eulaFile} eula.txt
|
|
|
|
|
|
|
|
cp -b --suffix=.stateful ${whitelistFile} whitelist.json
|
|
|
|
cp -b --suffix=.stateful ${opsFile} ops.json
|
|
|
|
cp -b --suffix=.stateful ${serverPropertiesFile} server.properties
|
|
|
|
|
|
|
|
chmod +w whitelist.json ops.json server.properties
|
|
|
|
|
|
|
|
${lib.optionalString (cfg.rconPasswordFile != null) ''
|
|
|
|
${replaceSecretBin} '#rconpass#' '${cfg.rconPasswordFile}' server.properties
|
|
|
|
''}
|
|
|
|
'' + cfg.extraPreStart;
|
|
|
|
};
|
|
|
|
|
|
|
|
networking.firewall = mkIf cfg.openFirewall ({
|
|
|
|
allowedUDPPorts = [ serverPort ];
|
|
|
|
allowedTCPPorts = [ serverPort ]
|
|
|
|
++ optional (queryPort != null) queryPort
|
|
|
|
++ optional (rconPort != null) rconPort;
|
|
|
|
});
|
|
|
|
|
|
|
|
assertions = [
|
|
|
|
{ assertion = cfg.eula;
|
|
|
|
message = "You must agree to Mojangs EULA to run minecraft-server."
|
|
|
|
+ " Read https://account.mojang.com/documents/minecraft_eula and"
|
|
|
|
+ " set `services.minecraft-server.eula` to `true` if you agree.";
|
|
|
|
}
|
|
|
|
];
|
|
|
|
};
|
|
|
|
}
|