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