Hosting a Minecraft server without extra hardware
Or, using Tailscale and iptables to roll your own port-forwarding
The problem
I want to play Minecraft with my friends, and I already have a server exposed to the internet. However, my server is severely underpowered and is unable to run a Minecraft server instance. On the other hand, I have a spare beefy laptop that can easily handle the load, but port-forwarding is not possible. Both the server and the laptop are on my Tailscale network. Could I somehow leverage all of this to spin up a Minecraft server with a public IP? The answer was yes—and I was surprised at how easy it all was. As a plus, the server is very playable and the latency was better than trying out random “free hosting” services.
Halfway with Tailscale
I already use Tailscale on all my devices, so of course when I spin up a Minecraft server instance on one device I can immediately connect to it from my other ones. My friends do not have Tailscale (yet!), so unfortunately node sharing is out of the picture for now, but I can still take advantage of Tailscale in that my laptop will always have a static IP relative to the server, and the server will always have a static IP relative to the public internet. So altogether the connection will be deterministic and I don’t have to resort to any dynamic shenanigans.
Let’s test the hypothesis.
$ NIXPKGS_ALLOW_UNFREE=1 nix run --impure nixpkgs#minecraft-server
Starting net.minecraft.server.Main
[22:18:53] [ServerMain/INFO]: Building unoptimized datafixer
[22:18:54] [ServerMain/INFO]: Environment: authHost='https://authserver.mojang.com', accountsHost='https://api.mojang.com', sessionHost='https://sessionserver.mojang.com', servicesHost='https://api.minecraftservices.com', name='PROD'
[22:18:54] [ServerMain/INFO]: Loaded 7 recipes
[22:18:55] [ServerMain/INFO]: Loaded 1179 advancements
[22:18:55] [Server thread/INFO]: Starting minecraft server version 1.19.1
[22:18:55] [Server thread/INFO]: Loading properties
[22:18:55] [Server thread/INFO]: Default game type: SURVIVAL
[22:18:55] [Server thread/INFO]: Generating keypair
[22:18:55] [Server thread/INFO]: Starting Minecraft server on *:25565
[22:18:55] [Server thread/INFO]: Using default channel type
[22:18:55] [Server thread/INFO]: Preparing level "world"
[22:18:55] [Server thread/INFO]: Preparing start region for dimension minecraft:overworld
[22:18:56] [Worker-Main-1/INFO]: Preparing spawn area: 0%
[22:18:56] [Worker-Main-1/INFO]: Preparing spawn area: 0%
[22:18:56] [Worker-Main-7/INFO]: Preparing spawn area: 0%
[22:18:57] [Worker-Main-7/INFO]: Preparing spawn area: 0%
[22:18:57] [Worker-Main-1/INFO]: Preparing spawn area: 83%
[22:18:57] [Server thread/INFO]: Time elapsed: 2080 ms
[22:18:57] [Server thread/INFO]: Done (2.163s)! For help, type "help"
And let’s check if Minecraft can see it if I put in the Tailscale IP…
Great success! Now we just need to expose it to the public internet.
iptables to the rescue!
iptables
essentially lets you configure the rules of the Linux
kernel firewall. Conceptually it’s quite simple. The user defines
tables and when a packet comes in, it goes through chains of
rules in the tables and you can route the packet through essentially
whatever treatment you like. Java edition Minecraft servers use TCP
port 25565 between the client and server.
NixOS configuration
It was very straightforward to enable IP forwarding and add 25565 to the list of open TCP ports for my server:
# combine with the rest of your configuration
{
boot.kernel.sysctl."net.ipv4.ip_forward" = 1;
networking.firewall.allowedTCPPorts = [ 25565 ];
}
Creating the rule
Now we can go ahead add the following commands to our firewall setup.
Let dest_ip
be the Tailscale IP of the server. The first command
adds a rule to the PREROUTING
chain which is where packets arrive
before being processed. We basically immediately forward the packet
over to the laptop pointed to by the IP address given by Tailscale.
The second command essentially lets the source IP of the packet remain
the same so the server just acts as a router.
# combine with the rest of your configuration
{
networking.firewall.extraCommands = ''
IPTABLES=${pkgs.iptables}/bin/iptables
"$IPTABLES" -t nat -A PREROUTING -p tcp --dport 25565 -j DNAT --to-destination ${dest_ip}:25565
"$IPTABLES" -t nat -A POSTROUTING -j MASQUERADE
'';
}
Now we have the following setup:
Now we rebuild the server configuration, and checking again in Minecraft, this time using the public server IP, it all works as expected!
Final touches: a DNS record
For the final touches *chef’s kiss*, adding an A record gave me a nice URL I could give people instead of an IP address.
Performance
As far as performance goes, it’s pretty good! The proxy server is on the East coast and even though the Minecraft server is on the West coast, having played on it for several hours today, my friends and I had no problems whatsoever. I pinged people through the connection and latency was acceptable (77 ms for someone in New York).
References
Xe’s post on Tailscale, NixOS and Minecraft inspired me to write this, however my requirements were different. I did not want to require my friends to install Tailscale to play on my server, and wanted to leverage the existing hardware I had access to, essentially letting me use my server as a crappy router.
Various iptables
tutorials and resources online helped me make sense
of the terminology, commands and flags.