]> Git — Sourcephile - sourcephile-nix.git/blob - nixos/modules/services/freeciv.nix
freeciv: add experimental service
[sourcephile-nix.git] / nixos / modules / services / freeciv.nix
1 { config, lib, pkgs, ... }:
2 with lib;
3 let
4 cfg = config.services.freeciv;
5 inherit (config.users) groups;
6 rootDir = "/run/freeciv";
7 settingsFormat = {
8 type = with lib.types; let
9 valueType = nullOr (oneOf [
10 bool int float str
11 (listOf valueType)
12 #(attrsOf valueType)
13 ]) // {
14 description = "freeciv-server params";
15 };
16 in valueType;
17 generate = name: value:
18 let mkParam = k: v:
19 if v == null then []
20 else if isBool v then if v then [("--"+k)] else []
21 else [("--"+k) v];
22 mkParams = k: v: map (mkParam k) (if isList v then v else [v]);
23 in escapeShellArgs (concatLists (concatLists (mapAttrsToList mkParams value)));
24 };
25 in
26 {
27 options = {
28 services.freeciv = {
29 enable = mkEnableOption ''freeciv'';
30 settings = mkOption {
31 description = ''
32 Parameters of freeciv-server.
33 '';
34 default = {};
35 type = types.submodule {
36 freeformType = settingsFormat.type;
37 options.Announce = mkOption {
38 type = types.enum ["IPv4" "IPv6" "none"];
39 default = "none";
40 description = "Announce game in LAN using given protocol.";
41 };
42 options.auth = mkEnableOption "server authentication";
43 options.Database = mkOption {
44 type = types.nullOr types.str;
45 apply = pkgs.writeText "auth.conf";
46 default = ''
47 [fcdb]
48 backend="sqlite"
49 database="/var/lib/freeciv/auth.sqlite"
50 '';
51 description = "Enable database connection with given configuration.";
52 };
53 options.debug = mkOption {
54 type = types.ints.between 0 3;
55 default = 0;
56 description = "Set debug log level.";
57 };
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 {
62 type = types.port;
63 default = 5556;
64 description = "Listen for clients on given port";
65 };
66 options.quitidle = mkOption {
67 type = types.nullOr types.int;
68 default = null;
69 description = "Quit if no players for given time in seconds.";
70 };
71 options.read = mkOption {
72 type = types.lines;
73 apply = v: pkgs.writeTextDir "read.serv" v + "/read";
74 default = ''
75 /fcdb lua sqlite_createdb()
76 '';
77 description = "Startup script.";
78 };
79 options.saves = mkOption {
80 type = types.nullOr types.str;
81 default = "/var/lib/freeciv/saves/";
82 description = ''
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.
86 '';
87 };
88 };
89 };
90 openFirewall = mkEnableOption "opening in the firewall of the port listening for clients";
91 };
92 };
93 config = mkIf cfg.enable {
94 users.groups.freeciv = {};
95 # Use with:
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" ];
101 socketConfig = {
102 ListenFIFO = "/run/freeciv.stdin";
103 SocketGroup = groups.freeciv.name;
104 SocketMode = "660";
105 RemoveOnStop = true;
106 };
107 };
108 systemd.services.freeciv = {
109 description = "Freeciv Service";
110 after = [ "network.target" ];
111 wantedBy = [ "multi-user.target" ];
112 environment.HOME = "/var/lib/freeciv";
113 serviceConfig = {
114 Restart = "on-failure";
115 RestartSec = "5s";
116 #StandardInput = "fd:freeciv.socket";
117 StandardInput = "socket";
118 StandardOutput = "journal";
119 StandardError = "journal";
120 ExecStart = pkgs.writeShellScript "freeciv-server" (''
121 set -eux
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; }));
128 DynamicUser = true;
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=.
138 UMask = "0066";
139 RootDirectory = rootDir;
140 RootDirectoryStartOnly = true;
141 MountAPIVFS = true;
142 BindReadOnlyPaths = [
143 builtins.storeDir
144 "/etc"
145 "/run"
146 ];
147 # The following options are only for optimizing:
148 # systemd-analyze security freeciv
149 AmbientCapabilities = "";
150 CapabilityBoundingSet = "";
151 # ProtectClock= adds DeviceAllow=char-rtc r
152 DeviceAllow = "";
153 LockPersonality = true;
154 MemoryDenyWriteExecute = true;
155 NoNewPrivileges = true;
156 PrivateDevices = true;
157 PrivateMounts = true;
158 PrivateNetwork = mkDefault false;
159 PrivateTmp = true;
160 PrivateUsers = true;
161 ProtectClock = true;
162 ProtectControlGroups = true;
163 ProtectHome = true;
164 ProtectHostname = true;
165 ProtectKernelLogs = true;
166 ProtectKernelModules = true;
167 ProtectKernelTunables = true;
168 ProtectSystem = "strict";
169 RemoveIPC = true;
170 RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
171 RestrictNamespaces = true;
172 RestrictRealtime = true;
173 RestrictSUIDSGID = true;
174 SystemCallFilter = [
175 "@system-service"
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"
181 ];
182 SystemCallArchitectures = "native";
183 SystemCallErrorNumber = "EPERM";
184 };
185 };
186 networking.firewall = mkIf cfg.openFirewall
187 { allowedTCPPorts = [ cfg.settings.port ]; };
188 };
189 meta.maintainers = with lib.maintainers; [ julm ];
190 }