diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 978d33e7585..5c5281b730f 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -334,6 +334,7 @@
./services/games/minecraft-server.nix
./services/games/minetest-server.nix
./services/games/openarena.nix
+ ./services/games/teeworlds.nix
./services/games/terraria.nix
./services/hardware/acpid.nix
./services/hardware/actkbd.nix
diff --git a/nixos/modules/services/games/teeworlds.nix b/nixos/modules/services/games/teeworlds.nix
new file mode 100644
index 00000000000..babf989c98c
--- /dev/null
+++ b/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 teeworlds.cfg. See Teeworlds Documentation.
+ '';
+ 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/nixos/tests/teeworlds.nix b/nixos/tests/teeworlds.nix
new file mode 100644
index 00000000000..edf58896878
--- /dev/null
+++ b/nixos/tests/teeworlds.nix
@@ -0,0 +1,55 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+
+let
+ client =
+ { pkgs, ... }:
+
+ { imports = [ ./common/x11.nix ];
+ environment.systemPackages = [ pkgs.teeworlds ];
+ };
+
+in {
+ name = "teeworlds";
+ meta = with pkgs.stdenv.lib.maintainers; {
+ maintainers = [ hax404 ];
+ };
+
+ nodes =
+ { server =
+ { services.teeworlds = {
+ enable = true;
+ openPorts = true;
+ };
+ };
+
+ client1 = client;
+ client2 = client;
+ };
+
+ testScript =
+ ''
+ start_all()
+
+ server.wait_for_unit("teeworlds.service")
+ server.wait_until_succeeds("ss --numeric --udp --listening | grep -q 8303")
+
+ client1.wait_for_x()
+ client2.wait_for_x()
+
+ client1.execute("teeworlds 'player_name Alice;connect server'&")
+ server.wait_until_succeeds(
+ 'journalctl -u teeworlds -e | grep --extended-regexp -q "team_join player=\'[0-9]:Alice"'
+ )
+
+ client2.execute("teeworlds 'player_name Bob;connect server'&")
+ server.wait_until_succeeds(
+ 'journalctl -u teeworlds -e | grep --extended-regexp -q "team_join player=\'[0-9]:Bob"'
+ )
+
+ server.sleep(10) # wait for a while to get a nice screenshot
+
+ client1.screenshot("screen_client1")
+ client2.screenshot("screen_client2")
+ '';
+
+})
diff --git a/pkgs/games/teeworlds/default.nix b/pkgs/games/teeworlds/default.nix
index 3035c02e262..9ff50d533be 100644
--- a/pkgs/games/teeworlds/default.nix
+++ b/pkgs/games/teeworlds/default.nix
@@ -1,5 +1,6 @@
{ fetchFromGitHub, stdenv, cmake, pkgconfig, python3, alsaLib
, libX11, libGLU, SDL2, lua5_3, zlib, freetype, wavpack, icoutils
+, nixosTests
}:
stdenv.mkDerivation rec {
@@ -36,6 +37,8 @@ stdenv.mkDerivation rec {
install -D $src/other/teeworlds.desktop $out/share/applications/teeworlds.desktop
'';
+ passthru.tests.teeworlds = nixosTests.teeworlds;
+
meta = {
description = "Retro multiplayer shooter game";