1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.mchprs;
7 settingsFormat = pkgs.formats.toml { };
8
9 whitelistFile = pkgs.writeText "whitelist.json"
10 (builtins.toJSON
11 (mapAttrsToList (n: v: { name = n; uuid = v; }) cfg.whitelist.list));
12
13 configToml =
14 (removeAttrs cfg.settings [ "address" "port" ]) //
15 {
16 bind_address = cfg.settings.address + ":" + toString cfg.settings.port;
17 whitelist = cfg.whitelist.enable;
18 };
19
20 configTomlFile = settingsFormat.generate "Config.toml" configToml;
21in
22{
23 options = {
24 services.mchprs = {
25 enable = mkEnableOption "MCHPRS";
26
27 declarativeSettings = mkOption {
28 type = types.bool;
29 default = false;
30 description = mdDoc ''
31 Whether to use a declarative configuration for MCHPRS.
32 '';
33 };
34
35 declarativeWhitelist = mkOption {
36 type = types.bool;
37 default = false;
38 description = mdDoc ''
39 Whether to use a declarative whitelist.
40 The options {option}`services.mchprs.whitelist.list`
41 will be applied if and only if set to `true`.
42 '';
43 };
44
45 dataDir = mkOption {
46 type = types.path;
47 default = "/var/lib/mchprs";
48 description = mdDoc ''
49 Directory to store MCHPRS database and other state/data files.
50 '';
51 };
52
53 openFirewall = mkOption {
54 type = types.bool;
55 default = false;
56 description = mdDoc ''
57 Whether to open ports in the firewall for the server.
58 Only has effect when
59 {option}`services.mchprs.declarativeSettings` is `true`.
60 '';
61 };
62
63 maxRuntime = mkOption {
64 type = types.str;
65 default = "infinity";
66 example = "7d";
67 description = mdDoc ''
68 Automatically restart the server after
69 {option}`services.mchprs.maxRuntime`.
70 The time span format is described here:
71 https://www.freedesktop.org/software/systemd/man/systemd.time.html#Parsing%20Time%20Spans.
72 If `null`, then the server is not restarted automatically.
73 '';
74 };
75
76 package = mkOption {
77 type = types.package;
78 default = pkgs.mchprs;
79 defaultText = literalExpression "pkgs.mchprs";
80 description = mdDoc "Version of MCHPRS to run.";
81 };
82
83 settings = mkOption {
84 type = types.submodule {
85 freeformType = settingsFormat.type;
86
87 options = {
88 port = mkOption {
89 type = types.port;
90 default = 25565;
91 description = mdDoc ''
92 Port for the server.
93 Only has effect when
94 {option}`services.mchprs.declarativeSettings` is `true`.
95 '';
96 };
97
98 address = mkOption {
99 type = types.str;
100 default = "0.0.0.0";
101 description = mdDoc ''
102 Address for the server.
103 Please use enclosing square brackets when using ipv6.
104 Only has effect when
105 {option}`services.mchprs.declarativeSettings` is `true`.
106 '';
107 };
108
109 motd = mkOption {
110 type = types.str;
111 default = "Minecraft High Performance Redstone Server";
112 description = mdDoc ''
113 Message of the day.
114 Only has effect when
115 {option}`services.mchprs.declarativeSettings` is `true`.
116 '';
117 };
118
119 chat_format = mkOption {
120 type = types.str;
121 default = "<{username}> {message}";
122 description = mdDoc ''
123 How to format chat message interpolating `username`
124 and `message` with curly braces.
125 Only has effect when
126 {option}`services.mchprs.declarativeSettings` is `true`.
127 '';
128 };
129
130 max_players = mkOption {
131 type = types.ints.positive;
132 default = 99999;
133 description = mdDoc ''
134 Maximum number of simultaneous players.
135 Only has effect when
136 {option}`services.mchprs.declarativeSettings` is `true`.
137 '';
138 };
139
140 view_distance = mkOption {
141 type = types.ints.positive;
142 default = 8;
143 description = mdDoc ''
144 Maximal distance (in chunks) between players and loaded chunks.
145 Only has effect when
146 {option}`services.mchprs.declarativeSettings` is `true`.
147 '';
148 };
149
150 bungeecord = mkOption {
151 type = types.bool;
152 default = false;
153 description = mdDoc ''
154 Enable compatibility with
155 [BungeeCord](https://github.com/SpigotMC/BungeeCord).
156 Only has effect when
157 {option}`services.mchprs.declarativeSettings` is `true`.
158 '';
159 };
160
161 schemati = mkOption {
162 type = types.bool;
163 default = false;
164 description = mdDoc ''
165 Mimic the verification and directory layout used by the
166 Open Redstone Engineers
167 [Schemati plugin](https://github.com/OpenRedstoneEngineers/Schemati).
168 Only has effect when
169 {option}`services.mchprs.declarativeSettings` is `true`.
170 '';
171 };
172
173 block_in_hitbox = mkOption {
174 type = types.bool;
175 default = true;
176 description = mdDoc ''
177 Allow placing blocks inside of players
178 (hitbox logic is simplified).
179 Only has effect when
180 {option}`services.mchprs.declarativeSettings` is `true`.
181 '';
182 };
183
184 auto_redpiler = mkOption {
185 type = types.bool;
186 default = true;
187 description = mdDoc ''
188 Use redpiler automatically.
189 Only has effect when
190 {option}`services.mchprs.declarativeSettings` is `true`.
191 '';
192 };
193 };
194 };
195 default = { };
196
197 description = mdDoc ''
198 Configuration for MCHPRS via `Config.toml`.
199 See https://github.com/MCHPR/MCHPRS/blob/master/README.md for documentation.
200 '';
201 };
202
203 whitelist = {
204 enable = mkOption {
205 type = types.bool;
206 default = false;
207 description = mdDoc ''
208 Whether or not the whitelist (in `whitelist.json`) shoud be enabled.
209 Only has effect when {option}`services.mchprs.declarativeSettings` is `true`.
210 '';
211 };
212
213 list = mkOption {
214 type =
215 let
216 minecraftUUID = types.strMatching
217 "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}" // {
218 description = "Minecraft UUID";
219 };
220 in
221 types.attrsOf minecraftUUID;
222 default = { };
223 example = literalExpression ''
224 {
225 username1 = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
226 username2 = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy";
227 };
228 '';
229 description = mdDoc ''
230 Whitelisted players, only has an effect when
231 {option}`services.mchprs.declarativeWhitelist` is
232 `true` and the whitelist is enabled
233 via {option}`services.mchprs.whitelist.enable`.
234 This is a mapping from Minecraft usernames to UUIDs.
235 You can use <https://mcuuid.net/> to get a
236 Minecraft UUID for a username.
237 '';
238 };
239 };
240 };
241 };
242
243 config = mkIf cfg.enable {
244 users.users.mchprs = {
245 description = "MCHPRS service user";
246 home = cfg.dataDir;
247 createHome = true;
248 isSystemUser = true;
249 group = "mchprs";
250 };
251 users.groups.mchprs = { };
252
253 systemd.services.mchprs = {
254 description = "MCHPRS Service";
255 wantedBy = [ "multi-user.target" ];
256 after = [ "network.target" ];
257
258 serviceConfig = {
259 ExecStart = "${lib.getExe cfg.package}";
260 Restart = "always";
261 RuntimeMaxSec = cfg.maxRuntime;
262 User = "mchprs";
263 WorkingDirectory = cfg.dataDir;
264
265 StandardOutput = "journal";
266 StandardError = "journal";
267
268 # Hardening
269 CapabilityBoundingSet = [ "" ];
270 DeviceAllow = [ "" ];
271 LockPersonality = true;
272 MemoryDenyWriteExecute = true;
273 PrivateDevices = true;
274 PrivateTmp = true;
275 PrivateUsers = true;
276 ProtectClock = true;
277 ProtectControlGroups = true;
278 ProtectHome = true;
279 ProtectHostname = true;
280 ProtectKernelLogs = true;
281 ProtectKernelModules = true;
282 ProtectKernelTunables = true;
283 ProtectProc = "invisible";
284 RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
285 RestrictNamespaces = true;
286 RestrictRealtime = true;
287 RestrictSUIDSGID = true;
288 SystemCallArchitectures = "native";
289 UMask = "0077";
290 };
291
292 preStart =
293 (if cfg.declarativeSettings then ''
294 if [ -e .declarativeSettings ]; then
295
296 # Settings were declarative before, no need to back up anything
297 cp -f ${configTomlFile} Config.toml
298
299 else
300
301 # Declarative settings for the first time, backup stateful files
302 cp -b --suffix=.stateful ${configTomlFile} Config.toml
303
304 echo "Autogenerated file that implies that this server configuration is managed declaratively by NixOS" \
305 > .declarativeSettings
306
307 fi
308 '' else ''
309 if [ -e .declarativeSettings ]; then
310 rm .declarativeSettings
311 fi
312 '') + (if cfg.declarativeWhitelist then ''
313 if [ -e .declarativeWhitelist ]; then
314
315 # Whitelist was declarative before, no need to back up anything
316 ln -sf ${whitelistFile} whitelist.json
317
318 else
319
320 # Declarative whitelist for the first time, backup stateful files
321 ln -sb --suffix=.stateful ${whitelistFile} whitelist.json
322
323 echo "Autogenerated file that implies that this server's whitelist is managed declaratively by NixOS" \
324 > .declarativeWhitelist
325
326 fi
327 '' else ''
328 if [ -e .declarativeWhitelist ]; then
329 rm .declarativeWhitelist
330 fi
331 '');
332 };
333
334 networking.firewall = mkIf (cfg.declarativeSettings && cfg.openFirewall) {
335 allowedUDPPorts = [ cfg.settings.port ];
336 allowedTCPPorts = [ cfg.settings.port ];
337 };
338 };
339
340 meta.maintainers = with maintainers; [ gdd ];
341}