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:

AddressExampleOwner or useFailure symptom
Loopback127.0.0.1RCON client and local probes on the same VMUnreachable from outside
Internal IPRFC 1918 addressVM NIC and VPCReachable only through internal routes
Wildcard0.0.0.0Server socket bindListens on every local IPv4 interface
External IPReserved GCP addressCloud network and access_configPublic 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.

LayerWhat movesTypical mismatch
GameWorld, plugins, whitelist, server propertiesOld IP, plugin port, version difference
HostPaths, user/group, Java, systemdOwnership, Java version, working directory
NetworkInternal/external IP, protocol, firewall targetTCP/UDP mix-up, missing tag, region mismatch
IdentityFloodgate key, RCON and Discord secretsMissing 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:

  1. Purpur opens the specified world and responds to Java status.
  2. Geyser opens the UDP listener and initializes Floodgate normally.
  3. The external Java client connects to the expected world.
  4. An external Bedrock client connects to the same world and existing identity.
  5. RCON and SSH are not accessed from public game paths.

References

Share

Share

Image preview