2020-06-18 07:06:33 +00:00
|
|
|
{ config, lib, pkgs, ... }:
|
|
|
|
|
|
|
|
with lib;
|
|
|
|
|
|
|
|
let
|
|
|
|
cfg = config.services.teeworlds;
|
|
|
|
register = cfg.register;
|
|
|
|
|
2024-02-29 20:09:43 +00:00
|
|
|
bool = b: if b != null && b then "1" else "0";
|
|
|
|
optionalSetting = s: setting: optionalString (s != null) "${setting} ${s}";
|
|
|
|
lookup = attrs: key: default: if attrs ? key then attrs."${key}" else default;
|
|
|
|
|
|
|
|
inactivePenaltyOptions = {
|
|
|
|
"spectator" = "1";
|
|
|
|
"spectator/kick" = "2";
|
|
|
|
"kick" = "3";
|
|
|
|
};
|
|
|
|
skillLevelOptions = {
|
|
|
|
"casual" = "0";
|
|
|
|
"normal" = "1";
|
|
|
|
"competitive" = "2";
|
|
|
|
};
|
|
|
|
tournamentModeOptions = {
|
|
|
|
"disable" = "0";
|
|
|
|
"enable" = "1";
|
|
|
|
"restrictSpectators" = "2";
|
|
|
|
};
|
|
|
|
|
2020-06-18 07:06:33 +00:00
|
|
|
teeworldsConf = pkgs.writeText "teeworlds.cfg" ''
|
|
|
|
sv_port ${toString cfg.port}
|
2024-02-29 20:09:43 +00:00
|
|
|
sv_register ${bool cfg.register}
|
|
|
|
sv_name ${cfg.name}
|
|
|
|
${optionalSetting cfg.motd "sv_motd"}
|
|
|
|
${optionalSetting cfg.password "password"}
|
|
|
|
${optionalSetting cfg.rconPassword "sv_rcon_password"}
|
|
|
|
|
|
|
|
${optionalSetting cfg.server.bindAddr "bindaddr"}
|
|
|
|
${optionalSetting cfg.server.hostName "sv_hostname"}
|
|
|
|
sv_high_bandwidth ${bool cfg.server.enableHighBandwidth}
|
|
|
|
sv_inactivekick ${lookup inactivePenaltyOptions cfg.server.inactivePenalty "spectator/kick"}
|
|
|
|
sv_inactivekick_spec ${bool cfg.server.kickInactiveSpectators}
|
|
|
|
sv_inactivekick_time ${toString cfg.server.inactiveTime}
|
|
|
|
sv_max_clients ${toString cfg.server.maxClients}
|
|
|
|
sv_max_clients_per_ip ${toString cfg.server.maxClientsPerIP}
|
|
|
|
sv_skill_level ${lookup skillLevelOptions cfg.server.skillLevel "normal"}
|
|
|
|
sv_spamprotection ${bool cfg.server.enableSpamProtection}
|
|
|
|
|
|
|
|
sv_gametype ${cfg.game.gameType}
|
|
|
|
sv_map ${cfg.game.map}
|
|
|
|
sv_match_swap ${bool cfg.game.swapTeams}
|
|
|
|
sv_player_ready_mode ${bool cfg.game.enableReadyMode}
|
|
|
|
sv_player_slots ${toString cfg.game.playerSlots}
|
|
|
|
sv_powerups ${bool cfg.game.enablePowerups}
|
|
|
|
sv_scorelimit ${toString cfg.game.scoreLimit}
|
|
|
|
sv_strict_spectate_mode ${bool cfg.game.restrictSpectators}
|
|
|
|
sv_teamdamage ${bool cfg.game.enableTeamDamage}
|
|
|
|
sv_timelimit ${toString cfg.game.timeLimit}
|
|
|
|
sv_tournament_mode ${lookup tournamentModeOptions cfg.server.tournamentMode "disable"}
|
|
|
|
sv_vote_kick ${bool cfg.game.enableVoteKick}
|
|
|
|
sv_vote_kick_bantime ${toString cfg.game.voteKickBanTime}
|
|
|
|
sv_vote_kick_min ${toString cfg.game.voteKickMinimumPlayers}
|
|
|
|
|
|
|
|
${optionalSetting cfg.server.bindAddr "bindaddr"}
|
|
|
|
${optionalSetting cfg.server.hostName "sv_hostname"}
|
|
|
|
sv_high_bandwidth ${bool cfg.server.enableHighBandwidth}
|
|
|
|
sv_inactivekick ${lookup inactivePenaltyOptions cfg.server.inactivePenalty "spectator/kick"}
|
|
|
|
sv_inactivekick_spec ${bool cfg.server.kickInactiveSpectators}
|
|
|
|
sv_inactivekick_time ${toString cfg.server.inactiveTime}
|
|
|
|
sv_max_clients ${toString cfg.server.maxClients}
|
|
|
|
sv_max_clients_per_ip ${toString cfg.server.maxClientsPerIP}
|
|
|
|
sv_skill_level ${lookup skillLevelOptions cfg.server.skillLevel "normal"}
|
|
|
|
sv_spamprotection ${bool cfg.server.enableSpamProtection}
|
|
|
|
|
|
|
|
sv_gametype ${cfg.game.gameType}
|
|
|
|
sv_map ${cfg.game.map}
|
|
|
|
sv_match_swap ${bool cfg.game.swapTeams}
|
|
|
|
sv_player_ready_mode ${bool cfg.game.enableReadyMode}
|
|
|
|
sv_player_slots ${toString cfg.game.playerSlots}
|
|
|
|
sv_powerups ${bool cfg.game.enablePowerups}
|
|
|
|
sv_scorelimit ${toString cfg.game.scoreLimit}
|
|
|
|
sv_strict_spectate_mode ${bool cfg.game.restrictSpectators}
|
|
|
|
sv_teamdamage ${bool cfg.game.enableTeamDamage}
|
|
|
|
sv_timelimit ${toString cfg.game.timeLimit}
|
|
|
|
sv_tournament_mode ${lookup tournamentModeOptions cfg.server.tournamentMode "disable"}
|
|
|
|
sv_vote_kick ${bool cfg.game.enableVoteKick}
|
|
|
|
sv_vote_kick_bantime ${toString cfg.game.voteKickBanTime}
|
|
|
|
sv_vote_kick_min ${toString cfg.game.voteKickMinimumPlayers}
|
|
|
|
|
2020-06-18 07:06:33 +00:00
|
|
|
${concatStringsSep "\n" cfg.extraOptions}
|
|
|
|
'';
|
|
|
|
|
|
|
|
in
|
|
|
|
{
|
|
|
|
options = {
|
|
|
|
services.teeworlds = {
|
2022-09-09 14:08:57 +00:00
|
|
|
enable = mkEnableOption (lib.mdDoc "Teeworlds Server");
|
2020-06-18 07:06:33 +00:00
|
|
|
|
2024-02-29 20:09:43 +00:00
|
|
|
package = mkPackageOptionMD pkgs "teeworlds-server" { };
|
|
|
|
|
2020-06-18 07:06:33 +00:00
|
|
|
openPorts = mkOption {
|
|
|
|
type = types.bool;
|
|
|
|
default = false;
|
2024-02-29 20:09:43 +00:00
|
|
|
description = lib.mdDoc "Whether to open firewall ports for Teeworlds.";
|
2020-06-18 07:06:33 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
name = mkOption {
|
2024-02-29 20:09:43 +00:00
|
|
|
type = types.str;
|
|
|
|
default = "unnamed server";
|
2022-08-12 12:06:08 +00:00
|
|
|
description = lib.mdDoc ''
|
2024-02-29 20:09:43 +00:00
|
|
|
Name of the server.
|
2020-06-18 07:06:33 +00:00
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
register = mkOption {
|
|
|
|
type = types.bool;
|
|
|
|
example = true;
|
|
|
|
default = false;
|
2022-08-12 12:06:08 +00:00
|
|
|
description = lib.mdDoc ''
|
2024-02-29 20:09:43 +00:00
|
|
|
Whether the server registers as a public server in the global server list. This is disabled by default for privacy reasons.
|
2020-06-18 07:06:33 +00:00
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
motd = mkOption {
|
|
|
|
type = types.nullOr types.str;
|
|
|
|
default = null;
|
2022-08-12 12:06:08 +00:00
|
|
|
description = lib.mdDoc ''
|
2024-02-29 20:09:43 +00:00
|
|
|
The server's message of the day text.
|
2020-06-18 07:06:33 +00:00
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
password = mkOption {
|
|
|
|
type = types.nullOr types.str;
|
|
|
|
default = null;
|
2022-08-12 12:06:08 +00:00
|
|
|
description = lib.mdDoc ''
|
2020-06-18 07:06:33 +00:00
|
|
|
Password to connect to the server.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
rconPassword = mkOption {
|
|
|
|
type = types.nullOr types.str;
|
|
|
|
default = null;
|
2022-08-12 12:06:08 +00:00
|
|
|
description = lib.mdDoc ''
|
2020-06-18 07:06:33 +00:00
|
|
|
Password to access the remote console. If not set, a randomly generated one is displayed in the server log.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
port = mkOption {
|
2022-11-21 17:40:18 +00:00
|
|
|
type = types.port;
|
2020-06-18 07:06:33 +00:00
|
|
|
default = 8303;
|
2022-08-12 12:06:08 +00:00
|
|
|
description = lib.mdDoc ''
|
2020-06-18 07:06:33 +00:00
|
|
|
Port the server will listen on.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
extraOptions = mkOption {
|
|
|
|
type = types.listOf types.str;
|
|
|
|
default = [];
|
2022-09-09 14:08:57 +00:00
|
|
|
description = lib.mdDoc ''
|
|
|
|
Extra configuration lines for the {file}`teeworlds.cfg`. See [Teeworlds Documentation](https://www.teeworlds.com/?page=docs&wiki=server_settings).
|
2020-06-18 07:06:33 +00:00
|
|
|
'';
|
|
|
|
example = [ "sv_map dm1" "sv_gametype dm" ];
|
|
|
|
};
|
2024-02-29 20:09:43 +00:00
|
|
|
|
|
|
|
server = {
|
|
|
|
bindAddr = mkOption {
|
|
|
|
type = types.nullOr types.str;
|
|
|
|
default = null;
|
|
|
|
description = lib.mdDoc ''
|
|
|
|
The address the server will bind to.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
enableHighBandwidth = mkOption {
|
|
|
|
type = types.bool;
|
|
|
|
default = false;
|
|
|
|
description = lib.mdDoc ''
|
|
|
|
Whether to enable high bandwidth mode on LAN servers. This will double the amount of bandwidth required for running the server.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
hostName = mkOption {
|
|
|
|
type = types.nullOr types.str;
|
|
|
|
default = null;
|
|
|
|
description = lib.mdDoc ''
|
|
|
|
Hostname for the server.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
inactivePenalty = mkOption {
|
|
|
|
type = types.enum [ "spectator" "spectator/kick" "kick" ];
|
|
|
|
example = "spectator";
|
|
|
|
default = "spectator/kick";
|
|
|
|
description = lib.mdDoc ''
|
|
|
|
Specify what to do when a client goes inactive (see [](#opt-services.teeworlds.server.inactiveTime)).
|
|
|
|
|
|
|
|
- `spectator`: send the client into spectator mode
|
|
|
|
|
|
|
|
- `spectator/kick`: send the client into a free spectator slot, otherwise kick the client
|
|
|
|
|
|
|
|
- `kick`: kick the client
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
kickInactiveSpectators = mkOption {
|
|
|
|
type = types.bool;
|
|
|
|
default = false;
|
|
|
|
description = lib.mdDoc ''
|
|
|
|
Whether to kick inactive spectators.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
inactiveTime = mkOption {
|
|
|
|
type = types.ints.unsigned;
|
|
|
|
default = 3;
|
|
|
|
description = lib.mdDoc ''
|
|
|
|
The amount of minutes a client has to idle before it is considered inactive.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
maxClients = mkOption {
|
|
|
|
type = types.ints.unsigned;
|
|
|
|
default = 12;
|
|
|
|
description = lib.mdDoc ''
|
|
|
|
The maximum amount of clients that can be connected to the server at the same time.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
maxClientsPerIP = mkOption {
|
|
|
|
type = types.ints.unsigned;
|
|
|
|
default = 12;
|
|
|
|
description = lib.mdDoc ''
|
|
|
|
The maximum amount of clients with the same IP address that can be connected to the server at the same time.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
skillLevel = mkOption {
|
|
|
|
type = types.enum [ "casual" "normal" "competitive" ];
|
|
|
|
default = "normal";
|
|
|
|
description = lib.mdDoc ''
|
|
|
|
The skill level shown in the server browser.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
enableSpamProtection = mkOption {
|
|
|
|
type = types.bool;
|
|
|
|
default = true;
|
|
|
|
description = lib.mdDoc ''
|
|
|
|
Whether to enable chat spam protection.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
game = {
|
|
|
|
gameType = mkOption {
|
|
|
|
type = types.str;
|
|
|
|
example = "ctf";
|
|
|
|
default = "dm";
|
|
|
|
description = lib.mdDoc ''
|
|
|
|
The game type to use on the server.
|
|
|
|
|
|
|
|
The default gametypes are `dm`, `tdm`, `ctf`, `lms`, and `lts`.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
map = mkOption {
|
|
|
|
type = types.str;
|
|
|
|
example = "ctf5";
|
|
|
|
default = "dm1";
|
|
|
|
description = lib.mdDoc ''
|
|
|
|
The map to use on the server.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
swapTeams = mkOption {
|
|
|
|
type = types.bool;
|
|
|
|
default = true;
|
|
|
|
description = lib.mdDoc ''
|
|
|
|
Whether to swap teams each round.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
enableReadyMode = mkOption {
|
|
|
|
type = types.bool;
|
|
|
|
default = false;
|
|
|
|
description = lib.mdDoc ''
|
|
|
|
Whether to enable "ready mode"; where players can pause/unpause the game
|
|
|
|
and start the game in warmup, using their ready state.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
playerSlots = mkOption {
|
|
|
|
type = types.ints.unsigned;
|
|
|
|
default = 8;
|
|
|
|
description = lib.mdDoc ''
|
|
|
|
The amount of slots to reserve for players (as opposed to spectators).
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
enablePowerups = mkOption {
|
|
|
|
type = types.bool;
|
|
|
|
default = true;
|
|
|
|
description = lib.mdDoc ''
|
|
|
|
Whether to allow powerups such as the ninja.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
scoreLimit = mkOption {
|
|
|
|
type = types.ints.unsigned;
|
|
|
|
example = 400;
|
|
|
|
default = 20;
|
|
|
|
description = lib.mdDoc ''
|
|
|
|
The score limit needed to win a round.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
restrictSpectators = mkOption {
|
|
|
|
type = types.bool;
|
|
|
|
default = false;
|
|
|
|
description = lib.mdDoc ''
|
|
|
|
Whether to restrict access to information such as health, ammo and armour in spectator mode.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
enableTeamDamage = mkOption {
|
|
|
|
type = types.bool;
|
|
|
|
default = false;
|
|
|
|
description = lib.mdDoc ''
|
|
|
|
Whether to enable team damage; whether to allow team mates to inflict damage on one another.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
timeLimit = mkOption {
|
|
|
|
type = types.ints.unsigned;
|
|
|
|
default = 0;
|
|
|
|
description = lib.mdDoc ''
|
|
|
|
Time limit of the game. In cases of equal points, there will be sudden death.
|
|
|
|
Setting this to 0 disables a time limit.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
tournamentMode = mkOption {
|
|
|
|
type = types.enum [ "disable" "enable" "restrictSpectators" ];
|
|
|
|
default = "disable";
|
|
|
|
description = lib.mdDoc ''
|
|
|
|
Whether to enable tournament mode. In tournament mode, players join as spectators.
|
|
|
|
If this is set to `restrictSpectators`, tournament mode is enabled but spectator chat is restricted.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
enableVoteKick = mkOption {
|
|
|
|
type = types.bool;
|
|
|
|
default = true;
|
|
|
|
description = lib.mdDoc ''
|
|
|
|
Whether to enable voting to kick players.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
voteKickBanTime = mkOption {
|
|
|
|
type = types.ints.unsigned;
|
|
|
|
default = 5;
|
|
|
|
description = lib.mdDoc ''
|
|
|
|
The amount of minutes that a player is banned for if they get kicked by a vote.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
voteKickMinimumPlayers = mkOption {
|
|
|
|
type = types.ints.unsigned;
|
|
|
|
default = 5;
|
|
|
|
description = lib.mdDoc ''
|
|
|
|
The minimum amount of players required to start a kick vote.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
};
|
2020-06-18 07:06:33 +00:00
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
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;
|
2024-02-29 20:09:43 +00:00
|
|
|
ExecStart = "${cfg.package}/bin/teeworlds_srv -f ${teeworldsConf}";
|
2020-06-18 07:06:33 +00:00
|
|
|
|
|
|
|
# Hardening
|
|
|
|
CapabilityBoundingSet = false;
|
|
|
|
PrivateDevices = true;
|
|
|
|
PrivateUsers = true;
|
|
|
|
ProtectHome = true;
|
|
|
|
ProtectKernelLogs = true;
|
|
|
|
ProtectKernelModules = true;
|
|
|
|
ProtectKernelTunables = true;
|
|
|
|
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
|
|
|
|
RestrictNamespaces = true;
|
|
|
|
SystemCallArchitectures = "native";
|
|
|
|
};
|
|
|
|
};
|
|
|
|
};
|
|
|
|
}
|