A client dials the OpenVPN server on a Compute Engine VM, the status window says connected, and a ping to a host inside the VPC still goes nowhere. The instinct is to add another push line, and that never fixes it. Compute Engine decides where packets go from its own route table and from the addresses a VM interface actually holds, so a tunnel pool the VM does not own is a pool nobody will deliver to.

Follow the failing packet instead of the configuration file, and the missing pieces appear in order.

Start at the packet that dies

The SYN leaves the client, lands in the tunnel, and reaches the VM as an encapsulated packet for a destination inside the VPC. OpenVPN decapsulates it and tries to forward it out of eth0, where the fabric applies one question before anything else: does this instance own the destination address. A tunnel pool carved out of someone else's subnet fails that question, and the packet is discarded before any route or firewall rule is consulted.

So the client-to-VPC direction has two separate requirements, and neither one is a push directive. The VM needs an alias range that covers the tunnel pool, which is what makes it a legitimate owner of those addresses. The VPC needs a route that names the VM as next hop for each protected prefix. Miss either one and the packet dies in a different place, while the client sees the same silence.

Read the failure in that order, because the two conditions look identical from the client side. A tunnel that reports connected while nothing answers is the signature of a topology problem, not of a bad cipher.

Does the instance own the tunnel pool

Give the VM a secondary range inside its own subnet, then make OpenVPN hand out addresses from exactly that range. On an instance whose primary address is 10.156.0.10/20, a /28 alias keeps the pool separate from the addresses other instances already hold:

gcloud compute instances network-interfaces update openvpn-vm \
  --zone=europe-west1-b \
  --aliases=eth0:10.156.1.0/28

The server directive then has to describe the same block. Its first address becomes the tunnel gateway, and the remaining addresses are handed out to clients. Two numbers describe one block here, a network and a netmask, which is the pool syntax OpenVPN still expects.

server 10.156.1.0 255.255.255.240

Confirm the address really landed on the interface rather than only inside the OpenVPN configuration. The fabric forwards for what the NIC reports, not for what the daemon believes. One command settles it:

ip -4 addr show dev eth0

A secondary address that never reaches eth0, or a pool that overlaps another subnet, appears later as intermittent drops rather than as a startup error. The daemon starts, clients connect, and only some destinations misbehave. Keep the alias range small so the pool is easy to recognise in a route table. Never carve it out of a subnet that another instance already uses.

Give the return traffic a next hop

A reply from a VPC host is addressed to a client address in the tunnel pool, which belongs to no subnet the VPC knows about. Without a route for that pool, the reply stops at the first hop that has no match, and the request may well have succeeded on the far end.

gcloud compute routes create openvpn-return \
  --network=prod-vpc \
  --destination-range=192.168.127.0/24 \
  --next-hop-instance=openvpn-vm \
  --next-hop-instance-zone=europe-west1-b \
  --priority=800

The same route is needed for any prefix that lives behind a client rather than inside the VPC. Those prefixes also need an iroute entry in that client's ccd file. That entry is server-side bookkeeping: it tells the server which client owns the prefix, and it installs nothing on the client itself.

Keep the push list for the client side of the conversation, since it only adds routes to the connecting machine. A push line for a VPC subnet and a VPC route back to the pool are two halves of the same path, and either half alone produces a tunnel that carries requests but no answers.

Firewalls, forwarding and the hops between

The VPC firewall evaluates the decapsulated packet, so an ingress rule has to admit the tunnel pool and any site prefix that reaches the VM that way. A default-deny posture fails silently here: the packet is dropped after decapsulation, and the OpenVPN log shows a healthy session.

On any Linux host that forwards rather than terminates, check that net.ipv4.ip_forward is enabled. The ownership rule applies there as well: a box that forwards for a subnet it does not hold drops the packet just as the VM would. Read the value before you blame the tunnel.

When exactly one prefix fails

Partial failure is the easiest case to read, because everything else in the path has already been proven. Start with the route list and read the destination range and next hop of the entry that should cover the prefix, then read the instance description and compare its alias range with the pool the server hands out.

gcloud compute routes list --filter=destination-range~10.164.0.0
 gcloud compute instances describe openvpn-vm --format=get(networkInterfaces[0].aliasIpRanges)

A missing route shows itself as no next hop for the prefix, while a missing alias shows itself as a packet dropped at the interface; the two look alike from the client and completely different in those two outputs. If both are present, the remaining suspect is the ccd iroute for the client that claims the prefix.

Write the four values down next to the tunnel: alias range, server pool, pushed prefixes, VPC routes. A stale alias breaks every pool address, a stale route breaks one direction, a missing push hides the prefix from the client, and a wrong iroute sends the prefix to the wrong peer. The next person to add a subnet needs all four in one place, and the note is cheaper than the evening.

Barruulee wal qabatan