1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.minecraft-server;
7
8 # We don't allow eula=false anyways
9 eulaFile = builtins.toFile "eula.txt" ''
10 # eula.txt managed by NixOS Configuration
11 eula=true
12 '';
13
14 whitelistFile = pkgs.writeText "whitelist.json"
15 (builtins.toJSON
16 (mapAttrsToList (n: v: { name = n; uuid = v; }) cfg.whitelist));
17
18 cfgToString = v: if builtins.isBool v then boolToString v else toString v;
19
20 serverPropertiesFile = pkgs.writeText "server.properties" (''
21 # server.properties managed by NixOS configuration
22 '' + concatStringsSep "\n" (mapAttrsToList
23 (n: v: "${n}=${cfgToString v}") cfg.serverProperties));
24
25 stopScript = pkgs.writeShellScript "minecraft-server-stop" ''
26 echo stop > ${config.systemd.sockets.minecraft-server.socketConfig.ListenFIFO}
27
28 # Wait for the PID of the minecraft server to disappear before
29 # returning, so systemd doesn't attempt to SIGKILL it.
30 while kill -0 "$1" 2> /dev/null; do
31 sleep 1s
32 done
33 '';
34
35 # To be able to open the firewall, we need to read out port values in the
36 # server properties, but fall back to the defaults when those don't exist.
37 # These defaults are from https://minecraft.gamepedia.com/Server.properties#Java_Edition_3
38 defaultServerPort = 25565;
39
40 serverPort = cfg.serverProperties.server-port or defaultServerPort;
41
42 rconPort = if cfg.serverProperties.enable-rcon or false
43 then cfg.serverProperties."rcon.port" or 25575
44 else null;
45
46 queryPort = if cfg.serverProperties.enable-query or false
47 then cfg.serverProperties."query.port" or 25565
48 else null;
49
50in {
51 options = {
52 services.minecraft-server = {
53
54 enable = mkOption {
55 type = types.bool;
56 default = false;
57 description = lib.mdDoc ''
58 If enabled, start a Minecraft Server. The server
59 data will be loaded from and saved to
60 {option}`services.minecraft-server.dataDir`.
61 '';
62 };
63
64 declarative = mkOption {
65 type = types.bool;
66 default = false;
67 description = lib.mdDoc ''
68 Whether to use a declarative Minecraft server configuration.
69 Only if set to `true`, the options
70 {option}`services.minecraft-server.whitelist` and
71 {option}`services.minecraft-server.serverProperties` will be
72 applied.
73 '';
74 };
75
76 eula = mkOption {
77 type = types.bool;
78 default = false;
79 description = lib.mdDoc ''
80 Whether you agree to
81 [
82 Mojangs EULA](https://account.mojang.com/documents/minecraft_eula). This option must be set to
83 `true` to run Minecraft server.
84 '';
85 };
86
87 dataDir = mkOption {
88 type = types.path;
89 default = "/var/lib/minecraft";
90 description = lib.mdDoc ''
91 Directory to store Minecraft database and other state/data files.
92 '';
93 };
94
95 openFirewall = mkOption {
96 type = types.bool;
97 default = false;
98 description = lib.mdDoc ''
99 Whether to open ports in the firewall for the server.
100 '';
101 };
102
103 whitelist = mkOption {
104 type = let
105 minecraftUUID = types.strMatching
106 "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}" // {
107 description = "Minecraft UUID";
108 };
109 in types.attrsOf minecraftUUID;
110 default = {};
111 description = lib.mdDoc ''
112 Whitelisted players, only has an effect when
113 {option}`services.minecraft-server.declarative` is
114 `true` and the whitelist is enabled
115 via {option}`services.minecraft-server.serverProperties` by
116 setting `white-list` to `true`.
117 This is a mapping from Minecraft usernames to UUIDs.
118 You can use <https://mcuuid.net/> to get a
119 Minecraft UUID for a username.
120 '';
121 example = literalExpression ''
122 {
123 username1 = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
124 username2 = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy";
125 };
126 '';
127 };
128
129 serverProperties = mkOption {
130 type = with types; attrsOf (oneOf [ bool int str ]);
131 default = {};
132 example = literalExpression ''
133 {
134 server-port = 43000;
135 difficulty = 3;
136 gamemode = 1;
137 max-players = 5;
138 motd = "NixOS Minecraft server!";
139 white-list = true;
140 enable-rcon = true;
141 "rcon.password" = "hunter2";
142 }
143 '';
144 description = lib.mdDoc ''
145 Minecraft server properties for the server.properties file. Only has
146 an effect when {option}`services.minecraft-server.declarative`
147 is set to `true`. See
148 <https://minecraft.gamepedia.com/Server.properties#Java_Edition_3>
149 for documentation on these values.
150 '';
151 };
152
153 package = mkOption {
154 type = types.package;
155 default = pkgs.minecraft-server;
156 defaultText = literalExpression "pkgs.minecraft-server";
157 example = literalExpression "pkgs.minecraft-server_1_12_2";
158 description = lib.mdDoc "Version of minecraft-server to run.";
159 };
160
161 jvmOpts = mkOption {
162 type = types.separatedString " ";
163 default = "-Xmx2048M -Xms2048M";
164 # Example options from https://minecraft.gamepedia.com/Tutorials/Server_startup_script
165 example = "-Xms4092M -Xmx4092M -XX:+UseG1GC -XX:+CMSIncrementalPacing "
166 + "-XX:+CMSClassUnloadingEnabled -XX:ParallelGCThreads=2 "
167 + "-XX:MinHeapFreeRatio=5 -XX:MaxHeapFreeRatio=10";
168 description = lib.mdDoc "JVM options for the Minecraft server.";
169 };
170 };
171 };
172
173 config = mkIf cfg.enable {
174
175 users.users.minecraft = {
176 description = "Minecraft server service user";
177 home = cfg.dataDir;
178 createHome = true;
179 isSystemUser = true;
180 group = "minecraft";
181 };
182 users.groups.minecraft = {};
183
184 systemd.sockets.minecraft-server = {
185 bindsTo = [ "minecraft-server.service" ];
186 socketConfig = {
187 ListenFIFO = "/run/minecraft-server.stdin";
188 SocketMode = "0660";
189 SocketUser = "minecraft";
190 SocketGroup = "minecraft";
191 RemoveOnStop = true;
192 FlushPending = true;
193 };
194 };
195
196 systemd.services.minecraft-server = {
197 description = "Minecraft Server Service";
198 wantedBy = [ "multi-user.target" ];
199 requires = [ "minecraft-server.socket" ];
200 after = [ "network.target" "minecraft-server.socket" ];
201
202 serviceConfig = {
203 ExecStart = "${cfg.package}/bin/minecraft-server ${cfg.jvmOpts}";
204 ExecStop = "${stopScript} $MAINPID";
205 Restart = "always";
206 User = "minecraft";
207 WorkingDirectory = cfg.dataDir;
208
209 StandardInput = "socket";
210 StandardOutput = "journal";
211 StandardError = "journal";
212
213 # Hardening
214 CapabilityBoundingSet = [ "" ];
215 DeviceAllow = [ "" ];
216 LockPersonality = true;
217 PrivateDevices = true;
218 PrivateTmp = true;
219 PrivateUsers = true;
220 ProtectClock = true;
221 ProtectControlGroups = true;
222 ProtectHome = true;
223 ProtectHostname = true;
224 ProtectKernelLogs = true;
225 ProtectKernelModules = true;
226 ProtectKernelTunables = true;
227 ProtectProc = "invisible";
228 RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
229 RestrictNamespaces = true;
230 RestrictRealtime = true;
231 RestrictSUIDSGID = true;
232 SystemCallArchitectures = "native";
233 UMask = "0077";
234 };
235
236 preStart = ''
237 ln -sf ${eulaFile} eula.txt
238 '' + (if cfg.declarative then ''
239
240 if [ -e .declarative ]; then
241
242 # Was declarative before, no need to back up anything
243 ln -sf ${whitelistFile} whitelist.json
244 cp -f ${serverPropertiesFile} server.properties
245
246 else
247
248 # Declarative for the first time, backup stateful files
249 ln -sb --suffix=.stateful ${whitelistFile} whitelist.json
250 cp -b --suffix=.stateful ${serverPropertiesFile} server.properties
251
252 # server.properties must have write permissions, because every time
253 # the server starts it first parses the file and then regenerates it..
254 chmod +w server.properties
255 echo "Autogenerated file that signifies that this server configuration is managed declaratively by NixOS" \
256 > .declarative
257
258 fi
259 '' else ''
260 if [ -e .declarative ]; then
261 rm .declarative
262 fi
263 '');
264 };
265
266 networking.firewall = mkIf cfg.openFirewall (if cfg.declarative then {
267 allowedUDPPorts = [ serverPort ];
268 allowedTCPPorts = [ serverPort ]
269 ++ optional (queryPort != null) queryPort
270 ++ optional (rconPort != null) rconPort;
271 } else {
272 allowedUDPPorts = [ defaultServerPort ];
273 allowedTCPPorts = [ defaultServerPort ];
274 });
275
276 assertions = [
277 { assertion = cfg.eula;
278 message = "You must agree to Mojangs EULA to run minecraft-server."
279 + " Read https://account.mojang.com/documents/minecraft_eula and"
280 + " set `services.minecraft-server.eula` to `true` if you agree.";
281 }
282 ];
283
284 };
285}