aboutsummaryrefslogtreecommitdiff
path: root/infra/libkookie/nixpkgs/nixos/modules/services/games
diff options
context:
space:
mode:
authorMx Kookie <kookie@spacekookie.de>2020-10-31 19:35:09 +0100
committerMx Kookie <kookie@spacekookie.de>2020-10-31 19:35:09 +0100
commitc4625b175f8200f643fd6e11010932ea44c78433 (patch)
treebce3f89888c8ac3991fa5569a878a9eab6801ccc /infra/libkookie/nixpkgs/nixos/modules/services/games
parent49f735974dd103039ddc4cb576bb76555164a9e7 (diff)
parentd661aa56a8843e991261510c1bb28fdc2f6975ae (diff)
Add 'infra/libkookie/' from commit 'd661aa56a8843e991261510c1bb28fdc2f6975ae'
git-subtree-dir: infra/libkookie git-subtree-mainline: 49f735974dd103039ddc4cb576bb76555164a9e7 git-subtree-split: d661aa56a8843e991261510c1bb28fdc2f6975ae
Diffstat (limited to 'infra/libkookie/nixpkgs/nixos/modules/services/games')
-rw-r--r--infra/libkookie/nixpkgs/nixos/modules/services/games/factorio.nix242
-rw-r--r--infra/libkookie/nixpkgs/nixos/modules/services/games/minecraft-server.nix234
-rw-r--r--infra/libkookie/nixpkgs/nixos/modules/services/games/minetest-server.nix107
-rw-r--r--infra/libkookie/nixpkgs/nixos/modules/services/games/openarena.nix56
-rw-r--r--infra/libkookie/nixpkgs/nixos/modules/services/games/teeworlds.nix119
-rw-r--r--infra/libkookie/nixpkgs/nixos/modules/services/games/terraria.nix155
6 files changed, 913 insertions, 0 deletions
diff --git a/infra/libkookie/nixpkgs/nixos/modules/services/games/factorio.nix b/infra/libkookie/nixpkgs/nixos/modules/services/games/factorio.nix
new file mode 100644
index 000000000000..4b2e1a3c07f0
--- /dev/null
+++ b/infra/libkookie/nixpkgs/nixos/modules/services/games/factorio.nix
@@ -0,0 +1,242 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+ cfg = config.services.factorio;
+ name = "Factorio";
+ stateDir = "/var/lib/${cfg.stateDirName}";
+ mkSavePath = name: "${stateDir}/saves/${name}.zip";
+ configFile = pkgs.writeText "factorio.conf" ''
+ use-system-read-write-data-directories=true
+ [path]
+ read-data=${cfg.package}/share/factorio/data
+ write-data=${stateDir}
+ '';
+ serverSettings = {
+ name = cfg.game-name;
+ description = cfg.description;
+ visibility = {
+ public = cfg.public;
+ lan = cfg.lan;
+ };
+ username = cfg.username;
+ password = cfg.password;
+ token = cfg.token;
+ game_password = cfg.game-password;
+ require_user_verification = cfg.requireUserVerification;
+ max_upload_in_kilobytes_per_second = 0;
+ minimum_latency_in_ticks = 0;
+ ignore_player_limit_for_returning_players = false;
+ allow_commands = "admins-only";
+ autosave_interval = cfg.autosave-interval;
+ autosave_slots = 5;
+ afk_autokick_interval = 0;
+ auto_pause = true;
+ only_admins_can_pause_the_game = true;
+ autosave_only_on_server = true;
+ admins = [];
+ } // cfg.extraSettings;
+ serverSettingsFile = pkgs.writeText "server-settings.json" (builtins.toJSON (filterAttrsRecursive (n: v: v != null) serverSettings));
+ modDir = pkgs.factorio-utils.mkModDirDrv cfg.mods;
+in
+{
+ options = {
+ services.factorio = {
+ enable = mkEnableOption name;
+ port = mkOption {
+ type = types.int;
+ default = 34197;
+ description = ''
+ The port to which the service should bind.
+
+ This option will also open up the UDP port in the firewall configuration.
+ '';
+ };
+ saveName = mkOption {
+ type = types.str;
+ default = "default";
+ description = ''
+ The name of the savegame that will be used by the server.
+
+ When not present in ${stateDir}/saves, a new map with default
+ settings will be generated before starting the service.
+ '';
+ };
+ # TODO Add more individual settings as nixos-options?
+ # TODO XXX The server tries to copy a newly created config file over the old one
+ # on shutdown, but fails, because it's in the nix store. When is this needed?
+ # Can an admin set options in-game and expect to have them persisted?
+ configFile = mkOption {
+ type = types.path;
+ default = configFile;
+ defaultText = "configFile";
+ description = ''
+ The server's configuration file.
+
+ The default file generated by this module contains lines essential to
+ the server's operation. Use its contents as a basis for any
+ customizations.
+ '';
+ };
+ stateDirName = mkOption {
+ type = types.str;
+ default = "factorio";
+ description = ''
+ Name of the directory under /var/lib holding the server's data.
+
+ The configuration and map will be stored here.
+ '';
+ };
+ mods = mkOption {
+ type = types.listOf types.package;
+ default = [];
+ description = ''
+ Mods the server should install and activate.
+
+ The derivations in this list must "build" the mod by simply copying
+ the .zip, named correctly, into the output directory. Eventually,
+ there will be a way to pull in the most up-to-date list of
+ derivations via nixos-channel. Until then, this is for experts only.
+ '';
+ };
+ game-name = mkOption {
+ type = types.nullOr types.str;
+ default = "Factorio Game";
+ description = ''
+ Name of the game as it will appear in the game listing.
+ '';
+ };
+ description = mkOption {
+ type = types.nullOr types.str;
+ default = "";
+ description = ''
+ Description of the game that will appear in the listing.
+ '';
+ };
+ extraSettings = mkOption {
+ type = types.attrs;
+ default = {};
+ example = { admins = [ "username" ];};
+ description = ''
+ Extra game configuration that will go into server-settings.json
+ '';
+ };
+ public = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Game will be published on the official Factorio matching server.
+ '';
+ };
+ lan = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Game will be broadcast on LAN.
+ '';
+ };
+ username = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = ''
+ Your factorio.com login credentials. Required for games with visibility public.
+ '';
+ };
+ package = mkOption {
+ type = types.package;
+ default = pkgs.factorio-headless;
+ defaultText = "pkgs.factorio-headless";
+ example = "pkgs.factorio-headless-experimental";
+ description = ''
+ Factorio version to use. This defaults to the stable channel.
+ '';
+ };
+ password = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = ''
+ Your factorio.com login credentials. Required for games with visibility public.
+ '';
+ };
+ token = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = ''
+ Authentication token. May be used instead of 'password' above.
+ '';
+ };
+ game-password = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = ''
+ Game password.
+ '';
+ };
+ requireUserVerification = mkOption {
+ type = types.bool;
+ default = true;
+ description = ''
+ When set to true, the server will only allow clients that have a valid factorio.com account.
+ '';
+ };
+ autosave-interval = mkOption {
+ type = types.nullOr types.int;
+ default = null;
+ example = 10;
+ description = ''
+ Autosave interval in minutes.
+ '';
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ systemd.services.factorio = {
+ description = "Factorio headless server";
+ wantedBy = [ "multi-user.target" ];
+ after = [ "network.target" ];
+
+ preStart = toString [
+ "test -e ${stateDir}/saves/${cfg.saveName}.zip"
+ "||"
+ "${cfg.package}/bin/factorio"
+ "--config=${cfg.configFile}"
+ "--create=${mkSavePath cfg.saveName}"
+ (optionalString (cfg.mods != []) "--mod-directory=${modDir}")
+ ];
+
+ serviceConfig = {
+ Restart = "always";
+ KillSignal = "SIGINT";
+ DynamicUser = true;
+ StateDirectory = cfg.stateDirName;
+ UMask = "0007";
+ ExecStart = toString [
+ "${cfg.package}/bin/factorio"
+ "--config=${cfg.configFile}"
+ "--port=${toString cfg.port}"
+ "--start-server=${mkSavePath cfg.saveName}"
+ "--server-settings=${serverSettingsFile}"
+ (optionalString (cfg.mods != []) "--mod-directory=${modDir}")
+ ];
+
+ # Sandboxing
+ NoNewPrivileges = true;
+ PrivateTmp = true;
+ PrivateDevices = true;
+ ProtectSystem = "strict";
+ ProtectHome = true;
+ ProtectControlGroups = true;
+ ProtectKernelModules = true;
+ ProtectKernelTunables = true;
+ RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" "AF_NETLINK" ];
+ RestrictRealtime = true;
+ RestrictNamespaces = true;
+ MemoryDenyWriteExecute = true;
+ };
+ };
+
+ networking.firewall.allowedUDPPorts = [ cfg.port ];
+ };
+}
diff --git a/infra/libkookie/nixpkgs/nixos/modules/services/games/minecraft-server.nix b/infra/libkookie/nixpkgs/nixos/modules/services/games/minecraft-server.nix
new file mode 100644
index 000000000000..eb9288fca586
--- /dev/null
+++ b/infra/libkookie/nixpkgs/nixos/modules/services/games/minecraft-server.nix
@@ -0,0 +1,234 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+ cfg = config.services.minecraft-server;
+
+ # We don't allow eula=false anyways
+ eulaFile = builtins.toFile "eula.txt" ''
+ # eula.txt managed by NixOS Configuration
+ eula=true
+ '';
+
+ whitelistFile = pkgs.writeText "whitelist.json"
+ (builtins.toJSON
+ (mapAttrsToList (n: v: { name = n; uuid = v; }) cfg.whitelist));
+
+ 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));
+
+
+ # To be able to open the firewall, we need to read out port values in the
+ # server properties, but fall back to the defaults when those don't exist.
+ # These defaults are from https://minecraft.gamepedia.com/Server.properties#Java_Edition_3
+ 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.minecraft-server = {
+
+ enable = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ If enabled, start a Minecraft Server. The server
+ data will be loaded from and saved to
+ <option>services.minecraft-server.dataDir</option>.
+ '';
+ };
+
+ declarative = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Whether to use a declarative Minecraft server configuration.
+ Only if set to <literal>true</literal>, the options
+ <option>services.minecraft-server.whitelist</option> and
+ <option>services.minecraft-server.serverProperties</option> will be
+ applied.
+ '';
+ };
+
+ eula = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Whether you agree to
+ <link xlink:href="https://account.mojang.com/documents/minecraft_eula">
+ Mojangs EULA</link>. This option must be set to
+ <literal>true</literal> to run Minecraft server.
+ '';
+ };
+
+ dataDir = mkOption {
+ type = types.path;
+ default = "/var/lib/minecraft";
+ description = ''
+ Directory to store Minecraft database and other state/data files.
+ '';
+ };
+
+ openFirewall = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Whether to open ports in the firewall for the server.
+ '';
+ };
+
+ whitelist = mkOption {
+ type = let
+ minecraftUUID = types.strMatching
+ "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}" // {
+ description = "Minecraft UUID";
+ };
+ in types.attrsOf minecraftUUID;
+ default = {};
+ description = ''
+ Whitelisted players, only has an effect when
+ <option>services.minecraft-server.declarative</option> is
+ <literal>true</literal> and the whitelist is enabled
+ via <option>services.minecraft-server.serverProperties</option> by
+ setting <literal>white-list</literal> to <literal>true</literal>.
+ This is a mapping from Minecraft usernames to UUIDs.
+ You can use <link xlink:href="https://mcuuid.net/"/> to get a
+ Minecraft UUID for a username.
+ '';
+ example = literalExample ''
+ {
+ username1 = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
+ username2 = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy";
+ };
+ '';
+ };
+
+ serverProperties = mkOption {
+ type = with types; attrsOf (oneOf [ bool int str ]);
+ default = {};
+ example = literalExample ''
+ {
+ server-port = 43000;
+ difficulty = 3;
+ gamemode = 1;
+ max-players = 5;
+ motd = "NixOS Minecraft server!";
+ white-list = true;
+ enable-rcon = true;
+ "rcon.password" = "hunter2";
+ }
+ '';
+ description = ''
+ Minecraft server properties for the server.properties file. Only has
+ an effect when <option>services.minecraft-server.declarative</option>
+ is set to <literal>true</literal>. See
+ <link xlink:href="https://minecraft.gamepedia.com/Server.properties#Java_Edition_3"/>
+ for documentation on these values.
+ '';
+ };
+
+ package = mkOption {
+ type = types.package;
+ default = pkgs.minecraft-server;
+ defaultText = "pkgs.minecraft-server";
+ example = literalExample "pkgs.minecraft-server_1_12_2";
+ description = "Version of minecraft-server to run.";
+ };
+
+ jvmOpts = mkOption {
+ type = types.separatedString " ";
+ default = "-Xmx2048M -Xms2048M";
+ # Example options from https://minecraft.gamepedia.com/Tutorials/Server_startup_script
+ example = "-Xmx2048M -Xms4092M -XX:+UseG1GC -XX:+CMSIncrementalPacing "
+ + "-XX:+CMSClassUnloadingEnabled -XX:ParallelGCThreads=2 "
+ + "-XX:MinHeapFreeRatio=5 -XX:MaxHeapFreeRatio=10";
+ description = "JVM options for the Minecraft server.";
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+
+ users.users.minecraft = {
+ description = "Minecraft server service user";
+ home = cfg.dataDir;
+ createHome = true;
+ uid = config.ids.uids.minecraft;
+ };
+
+ systemd.services.minecraft-server = {
+ description = "Minecraft Server Service";
+ wantedBy = [ "multi-user.target" ];
+ after = [ "network.target" ];
+
+ serviceConfig = {
+ ExecStart = "${cfg.package}/bin/minecraft-server ${cfg.jvmOpts}";
+ Restart = "always";
+ User = "minecraft";
+ WorkingDirectory = cfg.dataDir;
+ };
+
+ preStart = ''
+ ln -sf ${eulaFile} eula.txt
+ '' + (if cfg.declarative then ''
+
+ if [ -e .declarative ]; then
+
+ # Was declarative before, no need to back up anything
+ ln -sf ${whitelistFile} whitelist.json
+ cp -f ${serverPropertiesFile} server.properties
+
+ else
+
+ # Declarative for the first time, backup stateful files
+ ln -sb --suffix=.stateful ${whitelistFile} whitelist.json
+ cp -b --suffix=.stateful ${serverPropertiesFile} server.properties
+
+ # server.properties must have write permissions, because every time
+ # the server starts it first parses the file and then regenerates it..
+ chmod +w server.properties
+ echo "Autogenerated file that signifies that this server configuration is managed declaratively by NixOS" \
+ > .declarative
+
+ fi
+ '' else ''
+ if [ -e .declarative ]; then
+ rm .declarative
+ fi
+ '');
+ };
+
+ networking.firewall = mkIf cfg.openFirewall (if cfg.declarative then {
+ allowedUDPPorts = [ serverPort ];
+ allowedTCPPorts = [ serverPort ]
+ ++ optional (queryPort != null) queryPort
+ ++ optional (rconPort != null) rconPort;
+ } else {
+ allowedUDPPorts = [ defaultServerPort ];
+ allowedTCPPorts = [ defaultServerPort ];
+ });
+
+ 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.";
+ }
+ ];
+
+ };
+}
diff --git a/infra/libkookie/nixpkgs/nixos/modules/services/games/minetest-server.nix b/infra/libkookie/nixpkgs/nixos/modules/services/games/minetest-server.nix
new file mode 100644
index 000000000000..f52079fc1ef6
--- /dev/null
+++ b/infra/libkookie/nixpkgs/nixos/modules/services/games/minetest-server.nix
@@ -0,0 +1,107 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+ cfg = config.services.minetest-server;
+ flag = val: name: if val != null then "--${name} ${val} " else "";
+ flags = [
+ (flag cfg.gameId "gameid")
+ (flag cfg.world "world")
+ (flag cfg.configPath "config")
+ (flag cfg.logPath "logfile")
+ (flag cfg.port "port")
+ ];
+in
+{
+ options = {
+ services.minetest-server = {
+ enable = mkOption {
+ type = types.bool;
+ default = false;
+ description = "If enabled, starts a Minetest Server.";
+ };
+
+ gameId = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = ''
+ Id of the game to use. To list available games run
+ `minetestserver --gameid list`.
+
+ If only one game exists, this option can be null.
+ '';
+ };
+
+ world = mkOption {
+ type = types.nullOr types.path;
+ default = null;
+ description = ''
+ Name of the world to use. To list available worlds run
+ `minetestserver --world list`.
+
+ If only one world exists, this option can be null.
+ '';
+ };
+
+ configPath = mkOption {
+ type = types.nullOr types.path;
+ default = null;
+ description = ''
+ Path to the config to use.
+
+ If set to null, the config of the running user will be used:
+ `~/.minetest/minetest.conf`.
+ '';
+ };
+
+ logPath = mkOption {
+ type = types.nullOr types.path;
+ default = null;
+ description = ''
+ Path to logfile for logging.
+
+ If set to null, logging will be output to stdout which means
+ all output will be catched by systemd.
+ '';
+ };
+
+ port = mkOption {
+ type = types.nullOr types.int;
+ default = null;
+ description = ''
+ Port number to bind to.
+
+ If set to null, the default 30000 will be used.
+ '';
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ users.users.minetest = {
+ description = "Minetest Server Service user";
+ home = "/var/lib/minetest";
+ createHome = true;
+ uid = config.ids.uids.minetest;
+ group = "minetest";
+ };
+ users.groups.minetest.gid = config.ids.gids.minetest;
+
+ systemd.services.minetest-server = {
+ description = "Minetest Server Service";
+ wantedBy = [ "multi-user.target" ];
+ after = [ "network.target" ];
+
+ serviceConfig.Restart = "always";
+ serviceConfig.User = "minetest";
+ serviceConfig.Group = "minetest";
+
+ script = ''
+ cd /var/lib/minetest
+
+ exec ${pkgs.minetest}/bin/minetest --server ${concatStrings flags}
+ '';
+ };
+ };
+}
diff --git a/infra/libkookie/nixpkgs/nixos/modules/services/games/openarena.nix b/infra/libkookie/nixpkgs/nixos/modules/services/games/openarena.nix
new file mode 100644
index 000000000000..8c014d78809b
--- /dev/null
+++ b/infra/libkookie/nixpkgs/nixos/modules/services/games/openarena.nix
@@ -0,0 +1,56 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+ cfg = config.services.openarena;
+in
+{
+ options = {
+ services.openarena = {
+ enable = mkEnableOption "OpenArena";
+
+ openPorts = mkOption {
+ type = types.bool;
+ default = false;
+ description = "Whether to open firewall ports for OpenArena";
+ };
+
+ extraFlags = mkOption {
+ type = types.listOf types.str;
+ default = [];
+ description = ''Extra flags to pass to <command>oa_ded</command>'';
+ example = [
+ "+set dedicated 2"
+ "+set sv_hostname 'My NixOS OpenArena Server'"
+ # Load a map. Mandatory for clients to be able to connect.
+ "+map oa_dm1"
+ ];
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ networking.firewall = mkIf cfg.openPorts {
+ allowedUDPPorts = [ 27960 ];
+ };
+
+ systemd.services.openarena = {
+ description = "OpenArena";
+ wantedBy = [ "multi-user.target" ];
+ after = [ "network.target" ];
+
+ serviceConfig = {
+ DynamicUser = true;
+ StateDirectory = "openarena";
+ ExecStart = "${pkgs.openarena}/bin/oa_ded +set fs_basepath ${pkgs.openarena}/openarena-0.8.8 +set fs_homepath /var/lib/openarena ${concatStringsSep " " cfg.extraFlags}";
+ Restart = "on-failure";
+
+ # Hardening
+ CapabilityBoundingSet = "";
+ NoNewPrivileges = true;
+ PrivateDevices = true;
+ };
+ };
+ };
+}
diff --git a/infra/libkookie/nixpkgs/nixos/modules/services/games/teeworlds.nix b/infra/libkookie/nixpkgs/nixos/modules/services/games/teeworlds.nix
new file mode 100644
index 000000000000..babf989c98ca
--- /dev/null
+++ b/infra/libkookie/nixpkgs/nixos/modules/services/games/teeworlds.nix
@@ -0,0 +1,119 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+ cfg = config.services.teeworlds;
+ register = cfg.register;
+
+ teeworldsConf = pkgs.writeText "teeworlds.cfg" ''
+ sv_port ${toString cfg.port}
+ sv_register ${if cfg.register then "1" else "0"}
+ ${optionalString (cfg.name != null) "sv_name ${cfg.name}"}
+ ${optionalString (cfg.motd != null) "sv_motd ${cfg.motd}"}
+ ${optionalString (cfg.password != null) "password ${cfg.password}"}
+ ${optionalString (cfg.rconPassword != null) "sv_rcon_password ${cfg.rconPassword}"}
+ ${concatStringsSep "\n" cfg.extraOptions}
+ '';
+
+in
+{
+ options = {
+ services.teeworlds = {
+ enable = mkEnableOption "Teeworlds Server";
+
+ openPorts = mkOption {
+ type = types.bool;
+ default = false;
+ description = "Whether to open firewall ports for Teeworlds";
+ };
+
+ name = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = ''
+ Name of the server. Defaults to 'unnamed server'.
+ '';
+ };
+
+ register = mkOption {
+ type = types.bool;
+ example = true;
+ default = false;
+ description = ''
+ Whether the server registers as public server in the global server list. This is disabled by default because of privacy.
+ '';
+ };
+
+ motd = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = ''
+ Set the server message of the day text.
+ '';
+ };
+
+ password = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = ''
+ Password to connect to the server.
+ '';
+ };
+
+ rconPassword = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = ''
+ Password to access the remote console. If not set, a randomly generated one is displayed in the server log.
+ '';
+ };
+
+ port = mkOption {
+ type = types.int;
+ default = 8303;
+ description = ''
+ Port the server will listen on.
+ '';
+ };
+
+ extraOptions = mkOption {
+ type = types.listOf types.str;
+ default = [];
+ description = ''
+ Extra configuration lines for the <filename>teeworlds.cfg</filename>. See <link xlink:href="https://www.teeworlds.com/?page=docs&amp;wiki=server_settings">Teeworlds Documentation</link>.
+ '';
+ example = [ "sv_map dm1" "sv_gametype dm" ];
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ networking.firewall = mkIf cfg.openPorts {
+ allowedUDPPorts = [ cfg.port ];
+ };
+
+ systemd.services.teeworlds = {
+ description = "Teeworlds Server";
+ wantedBy = [ "multi-user.target" ];
+ after = [ "network.target" ];
+
+ serviceConfig = {
+ DynamicUser = true;
+ ExecStart = "${pkgs.teeworlds}/bin/teeworlds_srv -f ${teeworldsConf}";
+
+ # Hardening
+ CapabilityBoundingSet = false;
+ PrivateDevices = true;
+ PrivateUsers = true;
+ ProtectHome = true;
+ ProtectKernelLogs = true;
+ ProtectKernelModules = true;
+ ProtectKernelTunables = true;
+ RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+ RestrictNamespaces = true;
+ SystemCallArchitectures = "native";
+ };
+ };
+ };
+}
diff --git a/infra/libkookie/nixpkgs/nixos/modules/services/games/terraria.nix b/infra/libkookie/nixpkgs/nixos/modules/services/games/terraria.nix
new file mode 100644
index 000000000000..34c8ff137d6a
--- /dev/null
+++ b/infra/libkookie/nixpkgs/nixos/modules/services/games/terraria.nix
@@ -0,0 +1,155 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+ cfg = config.services.terraria;
+ worldSizeMap = { small = 1; medium = 2; large = 3; };
+ valFlag = name: val: optionalString (val != null) "-${name} \"${escape ["\\" "\""] (toString val)}\"";
+ boolFlag = name: val: optionalString val "-${name}";
+ flags = [
+ (valFlag "port" cfg.port)
+ (valFlag "maxPlayers" cfg.maxPlayers)
+ (valFlag "password" cfg.password)
+ (valFlag "motd" cfg.messageOfTheDay)
+ (valFlag "world" cfg.worldPath)
+ (valFlag "autocreate" (builtins.getAttr cfg.autoCreatedWorldSize worldSizeMap))
+ (valFlag "banlist" cfg.banListPath)
+ (boolFlag "secure" cfg.secure)
+ (boolFlag "noupnp" cfg.noUPnP)
+ ];
+ stopScript = pkgs.writeScript "terraria-stop" ''
+ #!${pkgs.runtimeShell}
+
+ if ! [ -d "/proc/$1" ]; then
+ exit 0
+ fi
+
+ ${getBin pkgs.tmux}/bin/tmux -S ${cfg.dataDir}/terraria.sock send-keys Enter exit Enter
+ ${getBin pkgs.coreutils}/bin/tail --pid="$1" -f /dev/null
+ '';
+in
+{
+ options = {
+ services.terraria = {
+ enable = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ If enabled, starts a Terraria server. The server can be connected to via <literal>tmux -S ${cfg.dataDir}/terraria.sock attach</literal>
+ for administration by users who are a part of the <literal>terraria</literal> group (use <literal>C-b d</literal> shortcut to detach again).
+ '';
+ };
+
+ port = mkOption {
+ type = types.int;
+ default = 7777;
+ description = ''
+ Specifies the port to listen on.
+ '';
+ };
+
+ maxPlayers = mkOption {
+ type = types.int;
+ default = 255;
+ description = ''
+ Sets the max number of players (between 1 and 255).
+ '';
+ };
+
+ password = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = ''
+ Sets the server password. Leave <literal>null</literal> for no password.
+ '';
+ };
+
+ messageOfTheDay = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = ''
+ Set the server message of the day text.
+ '';
+ };
+
+ worldPath = mkOption {
+ type = types.nullOr types.path;
+ default = null;
+ description = ''
+ The path to the world file (<literal>.wld</literal>) which should be loaded.
+ If no world exists at this path, one will be created with the size
+ specified by <literal>autoCreatedWorldSize</literal>.
+ '';
+ };
+
+ autoCreatedWorldSize = mkOption {
+ type = types.enum [ "small" "medium" "large" ];
+ default = "medium";
+ description = ''
+ Specifies the size of the auto-created world if <literal>worldPath</literal> does not
+ point to an existing world.
+ '';
+ };
+
+ banListPath = mkOption {
+ type = types.nullOr types.path;
+ default = null;
+ description = ''
+ The path to the ban list.
+ '';
+ };
+
+ secure = mkOption {
+ type = types.bool;
+ default = false;
+ description = "Adds additional cheat protection to the server.";
+ };
+
+ noUPnP = mkOption {
+ type = types.bool;
+ default = false;
+ description = "Disables automatic Universal Plug and Play.";
+ };
+ dataDir = mkOption {
+ type = types.str;
+ default = "/var/lib/terraria";
+ example = "/srv/terraria";
+ description = "Path to variable state data directory for terraria.";
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ users.users.terraria = {
+ description = "Terraria server service user";
+ home = cfg.dataDir;
+ createHome = true;
+ uid = config.ids.uids.terraria;
+ };
+
+ users.groups.terraria = {
+ gid = config.ids.gids.terraria;
+ members = [ "terraria" ];
+ };
+
+ systemd.services.terraria = {
+ description = "Terraria Server Service";
+ wantedBy = [ "multi-user.target" ];
+ after = [ "network.target" ];
+
+ serviceConfig = {
+ User = "terraria";
+ Type = "forking";
+ GuessMainPID = true;
+ ExecStart = "${getBin pkgs.tmux}/bin/tmux -S ${cfg.dataDir}/terraria.sock new -d ${pkgs.terraria-server}/bin/TerrariaServer ${concatStringsSep " " flags}";
+ ExecStop = "${stopScript} $MAINPID";
+ };
+
+ postStart = ''
+ ${pkgs.coreutils}/bin/chmod 660 ${cfg.dataDir}/terraria.sock
+ ${pkgs.coreutils}/bin/chgrp terraria ${cfg.dataDir}/terraria.sock
+ '';
+ };
+ };
+}