@@ -0,0 +1,187 @@
1+{ config, lib, pkgs, ... }:
2+with lib;
3+let
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 if v then [("--"+k)] else []
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+ };
24+in
25+{
26+ options = {
27+ services.freeciv = {
28+ enable = mkEnableOption ''freeciv'';
29+ settings = mkOption {
30+ description = ''
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 = "Announce game in LAN using given protocol.";
40+ };
41+ options.auth = mkEnableOption "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 = "Enable database connection with given configuration.";
51+ };
52+ options.debug = mkOption {
53+ type = types.ints.between 0 3;
54+ default = 0;
55+ description = "Set debug log level.";
56+ };
57+ options.exit-on-end = mkEnableOption "exit instead of restarting when a game ends.";
58+ options.Guests = mkEnableOption "guests to login if auth is enabled";
59+ options.Newusers = mkEnableOption "new users to login if auth is enabled";
60+ options.port = mkOption {
61+ type = types.port;
62+ default = 5556;
63+ description = "Listen for clients on given port";
64+ };
65+ options.quitidle = mkOption {
66+ type = types.nullOr types.int;
67+ default = null;
68+ description = "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 = "Startup script.";
77+ };
78+ options.saves = mkOption {
79+ type = types.nullOr types.str;
80+ default = "/var/lib/freeciv/saves/";
81+ description = ''
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 "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+}