The plan applies cleanly, the tunnel comes up, and the console calls it policy-based. Traffic still does not pass, and the search for a route-based toggle in the Terraform resource goes nowhere because no such field exists. In Classic VPN, route-based behaviour is not a mode you select. It is what you get when the local traffic selector is 0.0.0.0/0, the remote selectors list the networks you actually want, and a route points at the tunnel. Three things, and the label follows.

The myth: a tunnel mode field

The console label reflects what the API sees in the selectors. Nothing in the resource says route-based, so there is nothing to set to true.

Looking for that field consumes the first hour of the problem, while reading the selector values takes the last five minutes.

What route-based means here

Google documents Classic VPN route-based tunnels as policy-based tunnels whose local selector is 0.0.0.0/0. Only the remote ranges are named, and the local side is treated as any address. That is why the marker in your configuration is a value rather than a mode. Write both selectors explicitly and read them in the plan output:

local_traffic_selector  = ["0.0.0.0/0"]
remote_traffic_selector = ["10.20.0.0/16"]

The peer prefix in that snippet is illustrative; the shape is the part that matters.

The route is a separate object

A tunnel object does not install a route. Packets can match the tunnel's policy and still have nowhere to go, because the VPC routing table decides the next hop. The route needs the destination range, the network, the tunnel as next hop, and a priority that wins against anything broader. Forgetting it produces a tunnel that reports established and a ping that never leaves the VPC.

resource "google_compute_route" "office" {
  name                = "office-via-vpn"
  dest_range          = "10.20.0.0/16"
  network             = google_compute_network.vpc.name
  next_hop_vpn_tunnel = google_compute_vpn_tunnel.to_office.id
  priority            = 1000
}

Create the route in the same module as the tunnel, so the two cannot drift apart in review.

Mirror the selectors on the peer

The remote device has to be configured with the mirror image: its local range is your remote selector, and its remote range must cover the GCP subnets you expect to use. If either side narrows, phase 2 fails even though phase 1 succeeds. That is the most common shape of a configuration that looks right in the console and carries no traffic.

Print both sides' selectors next to each other during the change review.

Split the static address into two applies

Creating the reserved address and the tunnel that references it in one apply is a known source of churn, because the address has to exist before the forwarding rule can use it. Apply the address resource alone first, then add the gateway, forwarding rules and tunnel and apply again. Terraform reads the existing address from state on the second run and reuses it. Skip that and the tunnel can receive a fresh public address on each apply, which breaks the peer configuration every time.

Two applies now is cheaper than a peer change every release.

Verify negotiation, not the label

After applying, check three separate facts: the tunnel reports established, the route exists with the intended next hop, and the peer reports both phase 1 and phase 2 up.

A tunnel that is up with phase 2 down means the two sides disagree about selectors:

  • One side lists a subnet the other does not.
  • One side uses an aggregate the other narrowed.
  • The route exists but points at a different tunnel.

Classic VPN is a single tunnel with a lower SLA than HA VPN, and HA VPN uses two interfaces, two tunnels and BGP with different resource types. Copying a classic example into an HA configuration and expecting the selector fields to behave the same way is a separate mistake worth avoiding. The tunnel fields themselves are documented in the google_compute_vpn_tunnel reference.

Lahatsoratra mifandraika