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