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