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