at 23.11-beta 10 kB view raw
1{ config, lib, pkgs, ... }: 2 3with lib; 4 5let 6 cfg = config.services.factorio; 7 name = "Factorio"; 8 stateDir = "/var/lib/${cfg.stateDirName}"; 9 mkSavePath = name: "${stateDir}/saves/${name}.zip"; 10 configFile = pkgs.writeText "factorio.conf" '' 11 use-system-read-write-data-directories=true 12 [path] 13 read-data=${cfg.package}/share/factorio/data 14 write-data=${stateDir} 15 ''; 16 serverSettings = { 17 name = cfg.game-name; 18 description = cfg.description; 19 visibility = { 20 public = cfg.public; 21 lan = cfg.lan; 22 }; 23 username = cfg.username; 24 password = cfg.password; 25 token = cfg.token; 26 game_password = cfg.game-password; 27 require_user_verification = cfg.requireUserVerification; 28 max_upload_in_kilobytes_per_second = 0; 29 minimum_latency_in_ticks = 0; 30 ignore_player_limit_for_returning_players = false; 31 allow_commands = "admins-only"; 32 autosave_interval = cfg.autosave-interval; 33 autosave_slots = 5; 34 afk_autokick_interval = 0; 35 auto_pause = true; 36 only_admins_can_pause_the_game = true; 37 autosave_only_on_server = true; 38 non_blocking_saving = cfg.nonBlockingSaving; 39 } // cfg.extraSettings; 40 serverSettingsFile = pkgs.writeText "server-settings.json" (builtins.toJSON (filterAttrsRecursive (n: v: v != null) serverSettings)); 41 serverAdminsFile = pkgs.writeText "server-adminlist.json" (builtins.toJSON cfg.admins); 42 modDir = pkgs.factorio-utils.mkModDirDrv cfg.mods cfg.mods-dat; 43in 44{ 45 options = { 46 services.factorio = { 47 enable = mkEnableOption (lib.mdDoc name); 48 port = mkOption { 49 type = types.port; 50 default = 34197; 51 description = lib.mdDoc '' 52 The port to which the service should bind. 53 ''; 54 }; 55 56 bind = mkOption { 57 type = types.str; 58 default = "0.0.0.0"; 59 description = lib.mdDoc '' 60 The address to which the service should bind. 61 ''; 62 }; 63 64 admins = mkOption { 65 type = types.listOf types.str; 66 default = []; 67 example = [ "username" ]; 68 description = lib.mdDoc '' 69 List of player names which will be admin. 70 ''; 71 }; 72 73 openFirewall = mkOption { 74 type = types.bool; 75 default = false; 76 description = lib.mdDoc '' 77 Whether to automatically open the specified UDP port in the firewall. 78 ''; 79 }; 80 saveName = mkOption { 81 type = types.str; 82 default = "default"; 83 description = lib.mdDoc '' 84 The name of the savegame that will be used by the server. 85 86 When not present in /var/lib/''${config.services.factorio.stateDirName}/saves, 87 a new map with default settings will be generated before starting the service. 88 ''; 89 }; 90 loadLatestSave = mkOption { 91 type = types.bool; 92 default = false; 93 description = lib.mdDoc '' 94 Load the latest savegame on startup. This overrides saveName, in that the latest 95 save will always be used even if a saved game of the given name exists. It still 96 controls the 'canonical' name of the savegame. 97 98 Set this to true to have the server automatically reload a recent autosave after 99 a crash or desync. 100 ''; 101 }; 102 # TODO Add more individual settings as nixos-options? 103 # TODO XXX The server tries to copy a newly created config file over the old one 104 # on shutdown, but fails, because it's in the nix store. When is this needed? 105 # Can an admin set options in-game and expect to have them persisted? 106 configFile = mkOption { 107 type = types.path; 108 default = configFile; 109 defaultText = literalExpression "configFile"; 110 description = lib.mdDoc '' 111 The server's configuration file. 112 113 The default file generated by this module contains lines essential to 114 the server's operation. Use its contents as a basis for any 115 customizations. 116 ''; 117 }; 118 stateDirName = mkOption { 119 type = types.str; 120 default = "factorio"; 121 description = lib.mdDoc '' 122 Name of the directory under /var/lib holding the server's data. 123 124 The configuration and map will be stored here. 125 ''; 126 }; 127 mods = mkOption { 128 type = types.listOf types.package; 129 default = []; 130 description = lib.mdDoc '' 131 Mods the server should install and activate. 132 133 The derivations in this list must "build" the mod by simply copying 134 the .zip, named correctly, into the output directory. Eventually, 135 there will be a way to pull in the most up-to-date list of 136 derivations via nixos-channel. Until then, this is for experts only. 137 ''; 138 }; 139 mods-dat = mkOption { 140 type = types.nullOr types.path; 141 default = null; 142 description = lib.mdDoc '' 143 Mods settings can be changed by specifying a dat file, in the [mod 144 settings file 145 format](https://wiki.factorio.com/Mod_settings_file_format). 146 ''; 147 }; 148 game-name = mkOption { 149 type = types.nullOr types.str; 150 default = "Factorio Game"; 151 description = lib.mdDoc '' 152 Name of the game as it will appear in the game listing. 153 ''; 154 }; 155 description = mkOption { 156 type = types.nullOr types.str; 157 default = ""; 158 description = lib.mdDoc '' 159 Description of the game that will appear in the listing. 160 ''; 161 }; 162 extraSettings = mkOption { 163 type = types.attrs; 164 default = {}; 165 example = { admins = [ "username" ];}; 166 description = lib.mdDoc '' 167 Extra game configuration that will go into server-settings.json 168 ''; 169 }; 170 public = mkOption { 171 type = types.bool; 172 default = false; 173 description = lib.mdDoc '' 174 Game will be published on the official Factorio matching server. 175 ''; 176 }; 177 lan = mkOption { 178 type = types.bool; 179 default = false; 180 description = lib.mdDoc '' 181 Game will be broadcast on LAN. 182 ''; 183 }; 184 username = mkOption { 185 type = types.nullOr types.str; 186 default = null; 187 description = lib.mdDoc '' 188 Your factorio.com login credentials. Required for games with visibility public. 189 ''; 190 }; 191 package = mkOption { 192 type = types.package; 193 default = pkgs.factorio-headless; 194 defaultText = literalExpression "pkgs.factorio-headless"; 195 example = literalExpression "pkgs.factorio-headless-experimental"; 196 description = lib.mdDoc '' 197 Factorio version to use. This defaults to the stable channel. 198 ''; 199 }; 200 password = mkOption { 201 type = types.nullOr types.str; 202 default = null; 203 description = lib.mdDoc '' 204 Your factorio.com login credentials. Required for games with visibility public. 205 ''; 206 }; 207 token = mkOption { 208 type = types.nullOr types.str; 209 default = null; 210 description = lib.mdDoc '' 211 Authentication token. May be used instead of 'password' above. 212 ''; 213 }; 214 game-password = mkOption { 215 type = types.nullOr types.str; 216 default = null; 217 description = lib.mdDoc '' 218 Game password. 219 ''; 220 }; 221 requireUserVerification = mkOption { 222 type = types.bool; 223 default = true; 224 description = lib.mdDoc '' 225 When set to true, the server will only allow clients that have a valid factorio.com account. 226 ''; 227 }; 228 autosave-interval = mkOption { 229 type = types.nullOr types.int; 230 default = null; 231 example = 10; 232 description = lib.mdDoc '' 233 Autosave interval in minutes. 234 ''; 235 }; 236 nonBlockingSaving = mkOption { 237 type = types.bool; 238 default = false; 239 description = lib.mdDoc '' 240 Highly experimental feature, enable only at your own risk of losing your saves. 241 On UNIX systems, server will fork itself to create an autosave. 242 Autosaving on connected Windows clients will be disabled regardless of autosave_only_on_server option. 243 ''; 244 }; 245 }; 246 }; 247 248 config = mkIf cfg.enable { 249 systemd.services.factorio = { 250 description = "Factorio headless server"; 251 wantedBy = [ "multi-user.target" ]; 252 after = [ "network.target" ]; 253 254 preStart = toString [ 255 "test -e ${stateDir}/saves/${cfg.saveName}.zip" 256 "||" 257 "${cfg.package}/bin/factorio" 258 "--config=${cfg.configFile}" 259 "--create=${mkSavePath cfg.saveName}" 260 (optionalString (cfg.mods != []) "--mod-directory=${modDir}") 261 ]; 262 263 serviceConfig = { 264 Restart = "always"; 265 KillSignal = "SIGINT"; 266 DynamicUser = true; 267 StateDirectory = cfg.stateDirName; 268 UMask = "0007"; 269 ExecStart = toString [ 270 "${cfg.package}/bin/factorio" 271 "--config=${cfg.configFile}" 272 "--port=${toString cfg.port}" 273 "--bind=${cfg.bind}" 274 (optionalString (!cfg.loadLatestSave) "--start-server=${mkSavePath cfg.saveName}") 275 "--server-settings=${serverSettingsFile}" 276 (optionalString cfg.loadLatestSave "--start-server-load-latest") 277 (optionalString (cfg.mods != []) "--mod-directory=${modDir}") 278 (optionalString (cfg.admins != []) "--server-adminlist=${serverAdminsFile}") 279 ]; 280 281 # Sandboxing 282 NoNewPrivileges = true; 283 PrivateTmp = true; 284 PrivateDevices = true; 285 ProtectSystem = "strict"; 286 ProtectHome = true; 287 ProtectControlGroups = true; 288 ProtectKernelModules = true; 289 ProtectKernelTunables = true; 290 RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" "AF_NETLINK" ]; 291 RestrictRealtime = true; 292 RestrictNamespaces = true; 293 MemoryDenyWriteExecute = true; 294 }; 295 }; 296 297 networking.firewall.allowedUDPPorts = optional cfg.openFirewall cfg.port; 298 }; 299}