at master 14 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7let 8 cfg = config.services.teeworlds; 9 register = cfg.register; 10 11 bool = b: if b != null && b then "1" else "0"; 12 optionalSetting = s: setting: lib.optionalString (s != null) "${setting} ${s}"; 13 lookup = 14 attrs: key: default: 15 if attrs ? key then attrs."${key}" else default; 16 17 inactivePenaltyOptions = { 18 "spectator" = "1"; 19 "spectator/kick" = "2"; 20 "kick" = "3"; 21 }; 22 skillLevelOptions = { 23 "casual" = "0"; 24 "normal" = "1"; 25 "competitive" = "2"; 26 }; 27 tournamentModeOptions = { 28 "disable" = "0"; 29 "enable" = "1"; 30 "restrictSpectators" = "2"; 31 }; 32 33 teeworldsConf = pkgs.writeText "teeworlds.cfg" '' 34 sv_port ${toString cfg.port} 35 sv_register ${bool cfg.register} 36 sv_name ${cfg.name} 37 ${optionalSetting cfg.motd "sv_motd"} 38 ${optionalSetting cfg.password "password"} 39 ${optionalSetting cfg.rconPassword "sv_rcon_password"} 40 41 ${optionalSetting cfg.server.bindAddr "bindaddr"} 42 ${optionalSetting cfg.server.hostName "sv_hostname"} 43 sv_high_bandwidth ${bool cfg.server.enableHighBandwidth} 44 sv_inactivekick ${lookup inactivePenaltyOptions cfg.server.inactivePenalty "spectator/kick"} 45 sv_inactivekick_spec ${bool cfg.server.kickInactiveSpectators} 46 sv_inactivekick_time ${toString cfg.server.inactiveTime} 47 sv_max_clients ${toString cfg.server.maxClients} 48 sv_max_clients_per_ip ${toString cfg.server.maxClientsPerIP} 49 sv_skill_level ${lookup skillLevelOptions cfg.server.skillLevel "normal"} 50 sv_spamprotection ${bool cfg.server.enableSpamProtection} 51 52 sv_gametype ${cfg.game.gameType} 53 sv_map ${cfg.game.map} 54 sv_match_swap ${bool cfg.game.swapTeams} 55 sv_player_ready_mode ${bool cfg.game.enableReadyMode} 56 sv_player_slots ${toString cfg.game.playerSlots} 57 sv_powerups ${bool cfg.game.enablePowerups} 58 sv_scorelimit ${toString cfg.game.scoreLimit} 59 sv_strict_spectate_mode ${bool cfg.game.restrictSpectators} 60 sv_teamdamage ${bool cfg.game.enableTeamDamage} 61 sv_timelimit ${toString cfg.game.timeLimit} 62 sv_tournament_mode ${lookup tournamentModeOptions cfg.server.tournamentMode "disable"} 63 sv_vote_kick ${bool cfg.game.enableVoteKick} 64 sv_vote_kick_bantime ${toString cfg.game.voteKickBanTime} 65 sv_vote_kick_min ${toString cfg.game.voteKickMinimumPlayers} 66 67 ${optionalSetting cfg.server.bindAddr "bindaddr"} 68 ${optionalSetting cfg.server.hostName "sv_hostname"} 69 sv_high_bandwidth ${bool cfg.server.enableHighBandwidth} 70 sv_inactivekick ${lookup inactivePenaltyOptions cfg.server.inactivePenalty "spectator/kick"} 71 sv_inactivekick_spec ${bool cfg.server.kickInactiveSpectators} 72 sv_inactivekick_time ${toString cfg.server.inactiveTime} 73 sv_max_clients ${toString cfg.server.maxClients} 74 sv_max_clients_per_ip ${toString cfg.server.maxClientsPerIP} 75 sv_skill_level ${lookup skillLevelOptions cfg.server.skillLevel "normal"} 76 sv_spamprotection ${bool cfg.server.enableSpamProtection} 77 78 sv_gametype ${cfg.game.gameType} 79 sv_map ${cfg.game.map} 80 sv_match_swap ${bool cfg.game.swapTeams} 81 sv_player_ready_mode ${bool cfg.game.enableReadyMode} 82 sv_player_slots ${toString cfg.game.playerSlots} 83 sv_powerups ${bool cfg.game.enablePowerups} 84 sv_scorelimit ${toString cfg.game.scoreLimit} 85 sv_strict_spectate_mode ${bool cfg.game.restrictSpectators} 86 sv_teamdamage ${bool cfg.game.enableTeamDamage} 87 sv_timelimit ${toString cfg.game.timeLimit} 88 sv_tournament_mode ${lookup tournamentModeOptions cfg.server.tournamentMode "disable"} 89 sv_vote_kick ${bool cfg.game.enableVoteKick} 90 sv_vote_kick_bantime ${toString cfg.game.voteKickBanTime} 91 sv_vote_kick_min ${toString cfg.game.voteKickMinimumPlayers} 92 93 ${lib.concatStringsSep "\n" cfg.extraOptions} 94 ''; 95 96in 97{ 98 options = { 99 services.teeworlds = { 100 enable = lib.mkEnableOption "Teeworlds Server"; 101 102 package = lib.mkPackageOption pkgs "teeworlds-server" { }; 103 104 openPorts = lib.mkOption { 105 type = lib.types.bool; 106 default = false; 107 description = "Whether to open firewall ports for Teeworlds."; 108 }; 109 110 name = lib.mkOption { 111 type = lib.types.str; 112 default = "unnamed server"; 113 description = '' 114 Name of the server. 115 ''; 116 }; 117 118 register = lib.mkOption { 119 type = lib.types.bool; 120 example = true; 121 default = false; 122 description = '' 123 Whether the server registers as a public server in the global server list. This is disabled by default for privacy reasons. 124 ''; 125 }; 126 127 motd = lib.mkOption { 128 type = lib.types.nullOr lib.types.str; 129 default = null; 130 description = '' 131 The server's message of the day text. 132 ''; 133 }; 134 135 password = lib.mkOption { 136 type = lib.types.nullOr lib.types.str; 137 default = null; 138 description = '' 139 Password to connect to the server. 140 ''; 141 }; 142 143 rconPassword = lib.mkOption { 144 type = lib.types.nullOr lib.types.str; 145 default = null; 146 description = '' 147 Password to access the remote console. If not set, a randomly generated one is displayed in the server log. 148 ''; 149 }; 150 151 port = lib.mkOption { 152 type = lib.types.port; 153 default = 8303; 154 description = '' 155 Port the server will listen on. 156 ''; 157 }; 158 159 extraOptions = lib.mkOption { 160 type = lib.types.listOf lib.types.str; 161 default = [ ]; 162 description = '' 163 Extra configuration lines for the {file}`teeworlds.cfg`. See [Teeworlds Documentation](https://www.teeworlds.com/?page=docs&wiki=server_settings). 164 ''; 165 example = [ 166 "sv_map dm1" 167 "sv_gametype dm" 168 ]; 169 }; 170 171 server = { 172 bindAddr = lib.mkOption { 173 type = lib.types.nullOr lib.types.str; 174 default = null; 175 description = '' 176 The address the server will bind to. 177 ''; 178 }; 179 180 enableHighBandwidth = lib.mkOption { 181 type = lib.types.bool; 182 default = false; 183 description = '' 184 Whether to enable high bandwidth mode on LAN servers. This will double the amount of bandwidth required for running the server. 185 ''; 186 }; 187 188 hostName = lib.mkOption { 189 type = lib.types.nullOr lib.types.str; 190 default = null; 191 description = '' 192 Hostname for the server. 193 ''; 194 }; 195 196 inactivePenalty = lib.mkOption { 197 type = lib.types.enum [ 198 "spectator" 199 "spectator/kick" 200 "kick" 201 ]; 202 example = "spectator"; 203 default = "spectator/kick"; 204 description = '' 205 Specify what to do when a client goes inactive (see [](#opt-services.teeworlds.server.inactiveTime)). 206 207 - `spectator`: send the client into spectator mode 208 209 - `spectator/kick`: send the client into a free spectator slot, otherwise kick the client 210 211 - `kick`: kick the client 212 ''; 213 }; 214 215 kickInactiveSpectators = lib.mkOption { 216 type = lib.types.bool; 217 default = false; 218 description = '' 219 Whether to kick inactive spectators. 220 ''; 221 }; 222 223 inactiveTime = lib.mkOption { 224 type = lib.types.ints.unsigned; 225 default = 3; 226 description = '' 227 The amount of minutes a client has to idle before it is considered inactive. 228 ''; 229 }; 230 231 maxClients = lib.mkOption { 232 type = lib.types.ints.unsigned; 233 default = 12; 234 description = '' 235 The maximum amount of clients that can be connected to the server at the same time. 236 ''; 237 }; 238 239 maxClientsPerIP = lib.mkOption { 240 type = lib.types.ints.unsigned; 241 default = 12; 242 description = '' 243 The maximum amount of clients with the same IP address that can be connected to the server at the same time. 244 ''; 245 }; 246 247 skillLevel = lib.mkOption { 248 type = lib.types.enum [ 249 "casual" 250 "normal" 251 "competitive" 252 ]; 253 default = "normal"; 254 description = '' 255 The skill level shown in the server browser. 256 ''; 257 }; 258 259 enableSpamProtection = lib.mkOption { 260 type = lib.types.bool; 261 default = true; 262 description = '' 263 Whether to enable chat spam protection. 264 ''; 265 }; 266 }; 267 268 game = { 269 gameType = lib.mkOption { 270 type = lib.types.str; 271 example = "ctf"; 272 default = "dm"; 273 description = '' 274 The game type to use on the server. 275 276 The default gametypes are `dm`, `tdm`, `ctf`, `lms`, and `lts`. 277 ''; 278 }; 279 280 map = lib.mkOption { 281 type = lib.types.str; 282 example = "ctf5"; 283 default = "dm1"; 284 description = '' 285 The map to use on the server. 286 ''; 287 }; 288 289 swapTeams = lib.mkOption { 290 type = lib.types.bool; 291 default = true; 292 description = '' 293 Whether to swap teams each round. 294 ''; 295 }; 296 297 enableReadyMode = lib.mkOption { 298 type = lib.types.bool; 299 default = false; 300 description = '' 301 Whether to enable "ready mode"; where players can pause/unpause the game 302 and start the game in warmup, using their ready state. 303 ''; 304 }; 305 306 playerSlots = lib.mkOption { 307 type = lib.types.ints.unsigned; 308 default = 8; 309 description = '' 310 The amount of slots to reserve for players (as opposed to spectators). 311 ''; 312 }; 313 314 enablePowerups = lib.mkOption { 315 type = lib.types.bool; 316 default = true; 317 description = '' 318 Whether to allow powerups such as the ninja. 319 ''; 320 }; 321 322 scoreLimit = lib.mkOption { 323 type = lib.types.ints.unsigned; 324 example = 400; 325 default = 20; 326 description = '' 327 The score limit needed to win a round. 328 ''; 329 }; 330 331 restrictSpectators = lib.mkOption { 332 type = lib.types.bool; 333 default = false; 334 description = '' 335 Whether to restrict access to information such as health, ammo and armour in spectator mode. 336 ''; 337 }; 338 339 enableTeamDamage = lib.mkOption { 340 type = lib.types.bool; 341 default = false; 342 description = '' 343 Whether to enable team damage; whether to allow team mates to inflict damage on one another. 344 ''; 345 }; 346 347 timeLimit = lib.mkOption { 348 type = lib.types.ints.unsigned; 349 default = 0; 350 description = '' 351 Time limit of the game. In cases of equal points, there will be sudden death. 352 Setting this to 0 disables a time limit. 353 ''; 354 }; 355 356 tournamentMode = lib.mkOption { 357 type = lib.types.enum [ 358 "disable" 359 "enable" 360 "restrictSpectators" 361 ]; 362 default = "disable"; 363 description = '' 364 Whether to enable tournament mode. In tournament mode, players join as spectators. 365 If this is set to `restrictSpectators`, tournament mode is enabled but spectator chat is restricted. 366 ''; 367 }; 368 369 enableVoteKick = lib.mkOption { 370 type = lib.types.bool; 371 default = true; 372 description = '' 373 Whether to enable voting to kick players. 374 ''; 375 }; 376 377 voteKickBanTime = lib.mkOption { 378 type = lib.types.ints.unsigned; 379 default = 5; 380 description = '' 381 The amount of minutes that a player is banned for if they get kicked by a vote. 382 ''; 383 }; 384 385 voteKickMinimumPlayers = lib.mkOption { 386 type = lib.types.ints.unsigned; 387 default = 5; 388 description = '' 389 The minimum amount of players required to start a kick vote. 390 ''; 391 }; 392 }; 393 394 environmentFile = lib.mkOption { 395 type = lib.types.nullOr lib.types.path; 396 default = null; 397 example = "/var/lib/teeworlds/teeworlds.env"; 398 description = '' 399 Environment file as defined in {manpage}`systemd.exec(5)`. 400 401 Secrets may be passed to the service without adding them to the world-readable 402 Nix store, by specifying placeholder variables as the option value in Nix and 403 setting these variables accordingly in the environment file. 404 405 ``` 406 # snippet of teeworlds-related config 407 services.teeworlds.password = "$TEEWORLDS_PASSWORD"; 408 ``` 409 410 ``` 411 # content of the environment file 412 TEEWORLDS_PASSWORD=verysecretpassword 413 ``` 414 415 Note that this file needs to be available on the host on which 416 `teeworlds` is running. 417 ''; 418 }; 419 420 }; 421 }; 422 423 config = lib.mkIf cfg.enable { 424 networking.firewall = lib.mkIf cfg.openPorts { 425 allowedUDPPorts = [ cfg.port ]; 426 }; 427 428 systemd.services.teeworlds = { 429 description = "Teeworlds Server"; 430 wantedBy = [ "multi-user.target" ]; 431 after = [ "network.target" ]; 432 433 serviceConfig = { 434 DynamicUser = true; 435 RuntimeDirectory = "teeworlds"; 436 RuntimeDirectoryMode = "0700"; 437 EnvironmentFile = lib.mkIf (cfg.environmentFile != null) [ cfg.environmentFile ]; 438 ExecStartPre = '' 439 ${pkgs.envsubst}/bin/envsubst \ 440 -i ${teeworldsConf} \ 441 -o /run/teeworlds/teeworlds.yaml 442 ''; 443 ExecStart = "${lib.getExe cfg.package} -f /run/teeworlds/teeworlds.yaml"; 444 445 # Hardening 446 CapabilityBoundingSet = false; 447 PrivateDevices = true; 448 PrivateUsers = true; 449 ProtectHome = true; 450 ProtectKernelLogs = true; 451 ProtectKernelModules = true; 452 ProtectKernelTunables = true; 453 RestrictAddressFamilies = [ 454 "AF_INET" 455 "AF_INET6" 456 ]; 457 RestrictNamespaces = true; 458 SystemCallArchitectures = "native"; 459 }; 460 }; 461 }; 462}