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