1 { config, lib, pkgs, ... }:
4 cfg = config.services.freeciv;
5 inherit (config.users) groups;
6 rootDir = "/run/freeciv";
8 type = with lib.types; let
9 valueType = nullOr (oneOf [
14 description = "freeciv-server params";
17 generate = name: value:
20 else if isBool v then if v then [("--"+k)] else []
22 mkParams = k: v: map (mkParam k) (if isList v then v else [v]);
23 in escapeShellArgs (concatLists (concatLists (mapAttrsToList mkParams value)));
29 enable = mkEnableOption ''freeciv'';
32 Parameters of freeciv-server.
35 type = types.submodule {
36 freeformType = settingsFormat.type;
37 options.Announce = mkOption {
38 type = types.enum ["IPv4" "IPv6" "none"];
40 description = "Announce game in LAN using given protocol.";
42 options.auth = mkEnableOption "server authentication";
43 options.Database = mkOption {
44 type = types.nullOr types.str;
45 apply = pkgs.writeText "auth.conf";
49 database="/var/lib/freeciv/auth.sqlite"
51 description = "Enable database connection with given configuration.";
53 options.debug = mkOption {
54 type = types.ints.between 0 3;
56 description = "Set debug log level.";
58 options.exit-on-end = mkEnableOption "exit instead of restarting when a game ends.";
59 options.Guests = mkEnableOption "guests to login if auth is enabled";
60 options.Newusers = mkEnableOption "new users to login if auth is enabled";
61 options.port = mkOption {
64 description = "Listen for clients on given port";
66 options.quitidle = mkOption {
67 type = types.nullOr types.int;
69 description = "Quit if no players for given time in seconds.";
71 options.read = mkOption {
73 apply = v: pkgs.writeTextDir "read.serv" v + "/read";
75 /fcdb lua sqlite_createdb()
77 description = "Startup script.";
79 options.saves = mkOption {
80 type = types.nullOr types.str;
81 default = "/var/lib/freeciv/saves/";
83 Save games to given directory,
84 a sub-directory named after the starting date of the service
85 will me inserted to preserve older saves.
90 openFirewall = mkEnableOption "opening in the firewall of the port listening for clients";
93 config = mkIf cfg.enable {
94 users.groups.freeciv = {};
96 # journalctl -u freeciv.service -f -o cat &
97 # cat >/run/freeciv.stdin
98 # load saves/2020-11-14_05-22-27/freeciv-T0005-Y-3750-interrupted.sav.bz2
99 systemd.sockets.freeciv = {
100 wantedBy = [ "sockets.target" ];
102 ListenFIFO = "/run/freeciv.stdin";
103 SocketGroup = groups.freeciv.name;
108 systemd.services.freeciv = {
109 description = "Freeciv Service";
110 after = [ "network.target" ];
111 wantedBy = [ "multi-user.target" ];
112 environment.HOME = "/var/lib/freeciv";
114 Restart = "on-failure";
116 #StandardInput = "fd:freeciv.socket";
117 StandardInput = "socket";
118 StandardOutput = "journal";
119 StandardError = "journal";
120 ExecStart = pkgs.writeShellScript "freeciv-server" (''
122 savedir=$(date +%Y-%m-%d_%H-%M-%S)
123 trap "rmdir -p ${escapeShellArg cfg.settings.saves}/$savedir" EXIT
124 '' + "${pkgs.freeciv}/bin/freeciv-server"
125 + " " + optionalString (cfg.settings.saves != null)
126 (concatStringsSep " " [ "--saves" "${escapeShellArg cfg.settings.saves}/$savedir" ])
127 + " " + settingsFormat.generate "freeciv-server" (cfg.settings // { saves = null; }));
129 # Create rootDir in the host's mount namespace.
130 RuntimeDirectory = [(baseNameOf rootDir)];
131 RuntimeDirectoryMode = "755";
132 StateDirectory = [ "freeciv" ];
133 WorkingDirectory = "/var/lib/freeciv";
134 # Avoid mounting rootDir in the own rootDir of ExecStart='s mount namespace.
135 InaccessiblePaths = ["-+${rootDir}"];
136 # This is for BindPaths= and BindReadOnlyPaths=
137 # to allow traversal of directories they create in RootDirectory=.
139 RootDirectory = rootDir;
140 RootDirectoryStartOnly = true;
142 BindReadOnlyPaths = [
147 # The following options are only for optimizing:
148 # systemd-analyze security freeciv
149 AmbientCapabilities = "";
150 CapabilityBoundingSet = "";
151 # ProtectClock= adds DeviceAllow=char-rtc r
153 LockPersonality = true;
154 MemoryDenyWriteExecute = true;
155 NoNewPrivileges = true;
156 PrivateDevices = true;
157 PrivateMounts = true;
158 PrivateNetwork = mkDefault false;
162 ProtectControlGroups = true;
164 ProtectHostname = true;
165 ProtectKernelLogs = true;
166 ProtectKernelModules = true;
167 ProtectKernelTunables = true;
168 ProtectSystem = "strict";
170 RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
171 RestrictNamespaces = true;
172 RestrictRealtime = true;
173 RestrictSUIDSGID = true;
176 # Groups in @system-service which do not contain a syscall listed by:
177 # perf stat -x, 2>perf.log -e 'syscalls:sys_enter_*' freeciv-server
178 # in tests, and seem likely not necessary for freeciv-server.
179 "~@aio" "~@chown" "~@ipc" "~@keyring" "~@memlock"
180 "~@resources" "~@setuid" "~@sync" "~@timer"
182 SystemCallArchitectures = "native";
183 SystemCallErrorNumber = "EPERM";
186 networking.firewall = mkIf cfg.openFirewall
187 { allowedTCPPorts = [ cfg.settings.port ]; };
189 meta.maintainers = with lib.maintainers; [ julm ];