External Connectivity After a Server Migration
A layered diagnostic order from public IP and local bind addresses through VPC firewall rules, Java TCP, Bedrock UDP, Geyser, and Floodgate.
Immediately after I moved the world and configuration from Windows to a GCP VM, Minecraft failed to
create a listener. server.properties still contained the old environment’s address in
server-ip. I had treated the public address entered by clients and the local address bound by the
server process as if they were the same value.
A GCP external IPv4 address delivers Internet traffic to the VM, but the guest OS network interface does not directly own that address. If Minecraft tries to bind it, the kernel can reject the address before firewall rules are even relevant.
Public Addresses and Bind Addresses
Unless there is a reason to bind to one local interface, leave the server address empty.
server-ip=
server-port=25565
enable-rcon=true
rcon.port=25575
An empty server-ip listens on all available local interfaces. On Linux, 0.0.0.0:25565 means all
local IPv4 interfaces; it does not by itself expose the service to the Internet. Public reachability
still depends on routing, the external address, and VPC firewall policy.
flowchart LR accTitle: Address path from external address to Minecraft process accDescr: A client connects to the static external IP. Google Cloud forwards the traffic to the VM's internal interface, where the guest kernel delivers it to the wildcard-bound Minecraft process. C[Internet client] -->|Static external IP:25565| EDGE[Google Cloud edge] EDGE --> FW[VPC ingress firewall] FW -->|VM network tag| NIC[VM NIC and internal IP] NIC --> K[Guest kernel] K -->|0.0.0.0:25565 bind| MC[Purpur]
Responsibilities for each address are divided as follows:
| Address | Example | Owner or use | Failure symptom |
|---|---|---|---|
| Loopback | 127.0.0.1 | RCON client and local probes on the same VM | Unreachable from outside |
| Internal IP | RFC 1918 address | VM NIC and VPC | Reachable only through internal routes |
| Wildcard | 0.0.0.0 | Server socket bind | Listens on every local IPv4 interface |
| External IP | Reserved GCP address | Cloud network and access_config | Public destination disappears when detached |
The external IP does not belong in Minecraft configuration. Terraform’s nat_ip associates it with
the VM at the cloud-network layer; Minecraft opens sockets only inside the guest.
resource "google_compute_address" "minecraft" {
name = "minecraft-public-ip"
region = var.region
}
network_interface {
network = var.network
access_config {
nat_ip = google_compute_address.minecraft.address
}
}
A regional external address must match the region containing the VM’s zone. Moving the VM to another region means the existing address cannot simply follow it, expanding the migration to DNS records and addresses saved by players.
Separate Transport Paths for Java and Bedrock
Java Edition connected directly to Purpur over TCP. Bedrock Edition connected to Geyser over UDP, which translated the session into the Java protocol. Floodgate handled Bedrock authentication and identity mapping.
flowchart LR accTitle: Java and Bedrock crossplay path accDescr: The Java client connects directly to Purpur via TCP, and the Bedrock client connects to Geyser via UDP and joins the same Purpur world through Floodgate authentication. J[Java client] -->|TCP 25565| JFW[Java firewall rule] JFW --> P[Purpur Java listener] B[Bedrock client] -->|UDP 19132| BFW[Bedrock firewall rule] BFW --> G[Geyser UDP listener] G --> F[Floodgate identity] F --> P P --> W[same world]
Although both paths use the same external IP, their protocol and destination port differ. A working Java connection does not verify the Bedrock path, and a TCP port check cannot prove that the UDP listener exists.
resource "google_compute_firewall" "minecraft_bedrock" {
name = "minecraft-bedrock"
network = var.network
source_ranges = ["0.0.0.0/0"]
target_tags = ["minecraft-server"]
allow {
protocol = "udp"
ports = ["19132"]
}
}
target_tags must exactly match a tag attached to the VM; a correctly named rule with the right port
still does nothing when the target tag differs. source_ranges determines who may connect. A public
game server may expose its game ports broadly, but RCON, SSH, and administrative map ports should
not inherit the same policy.
Even in Geyser settings, bind and upstream are divided.
bedrock:
address: 0.0.0.0
port: 19132
remote:
address: 127.0.0.1
port: 25565
auth-type: floodgate
The Bedrock listener binds to every local interface so it can receive external UDP. Because the
upstream Java server runs on the same VM, Geyser reaches it over loopback. Putting the public IP in
remote.address needlessly sends traffic through an external path and makes it depend on NAT or
firewall behavior.
Checking the Path from the Inside Out
A client’s “connection failed” message collapses many possible faults into one symptom. Checking from the innermost process outward narrows the hypothesis at every step.
flowchart TD
accTitle: Minecraft external connection failure diagnosis procedure
accDescr: Diagnose service state, logs, local listeners, protocol responses, firewall rules, the external address, and finally an outside client.
A{Is systemd active?} -- No --> AL[Check unit and JVM logs]
A -- Yes --> B{Bound to the expected port?}
B -- No --> BL[Check server-ip and port conflicts]
B -- Yes --> C{Local protocol response?}
C -- No --> CL[Check world and plugin initialization]
C -- Yes --> D{Firewall tag, protocol, and port match?}
D -- No --> DL[Fix VPC rule]
D -- Yes --> E{External IP attached to this VM?}
E -- No --> EL[Check address, region, and access_config]
E -- Yes --> F[Test Java and Bedrock outside the VM]
Inside the VM, check different sockets with the following command.
systemctl is-active minecraft.service
ss -lntp '( sport = :25565 )'
ss -lnup '( sport = :19132 )'
journalctl -u minecraft.service -b --no-pager | tail -n 100
TCP LISTEN means that the Java socket is open. UDP may appear as UNCONN because it has no
connection state. Also verify that the process name and PID belong to the expected Java process;
another program may have claimed the port.
A local protocol probe checks whether Minecraft can respond: use a Java server-list ping and a Bedrock status request. Repeat both checks from outside the VM against the static external IP. If the local probe succeeds and the external probe fails, the likely fault lies in the GCP firewall, address attachment, upstream network, or client path rather than in the application.
Firewall Rule Scope
VPC firewalls are stateful, so response traffic for an accepted ingress connection does not require a separate reverse egress rule. A Java TCP rule does not cover Bedrock UDP, however, and an IPv4 rule does not automatically authorize an IPv6 path.
When reviewing a rule, look at the next value rather than the name.
- Direction: Ingress or egress
- action and priority: Is there a higher priority deny?
- protocol: TCP, UDP, or both
- destination port: Is this the port that Minecraft actually binds to?
- source range: Is it a public user or management range?
- target: Does the network tag or service account match the VM?
Because only the Discord bot and backup scripts on the same VM used RCON, it needed no public VPC
ingress rule. The guest firewall should limit it to local callers. Standard server.properties
does not expose a dedicated RCON bind address, and setting server-ip=127.0.0.1 would also bind the
game listener to loopback. When stronger isolation is needed, place a loopback proxy, IAP or SSH
tunnel, or host firewall in front of RCON. If remote administration is unavoidable, use a VPN or a
narrow administrator range separate from the public game ports. RCON should not be treated as an
encrypted management API.
Configuration Drift After a Migration
Moving the world directory does not reproduce the server environment. Four layers of configuration must move together.
| Layer | What moves | Typical mismatch |
|---|---|---|
| Game | World, plugins, whitelist, server properties | Old IP, plugin port, version difference |
| Host | Paths, user/group, Java, systemd | Ownership, Java version, working directory |
| Network | Internal/external IP, protocol, firewall target | TCP/UDP mix-up, missing tag, region mismatch |
| Identity | Floodgate key, RCON and Discord secrets | Missing key, exposed metadata, wrong permissions |
When moving Geyser and Floodgate, verify the Floodgate key’s ownership and the plugin configuration.
key.pem authenticates data passed between Floodgate components. A missing or mismatched key can
break authentication and identity forwarding, but it does not generate a Bedrock player’s
Java-side UUID. The default UUID derives from the XUID, so continuity must be checked alongside
account-linking and username-prefix settings. Neither real user data nor key material belongs in
documentation or Terraform state.
The migration is complete only after all of the following have been observed:
- Purpur opens the specified world and responds to Java status.
- Geyser opens the UDP listener and initializes Floodgate normally.
- The external Java client connects to the expected world.
- An external Bedrock client connects to the same world and existing identity.
- RCON and SSH are not accessed from public game paths.
References
Share
No comments yet. Be the first to leave one.
Pending review