DEV Community

Zach S. for Google Cloud

Posted on

Verifying Google Cloud egress IP ranges with cloud.json and jq

When GCE workloads or Cloud NAT gateways connect to external partner endpoints, it is common to run into asymmetric reachability issues - connections from one region work normally, while connections from another region (or from a newly reserved NAT IP in the same region) time out on the initial TCP SYN. In most cases, this happens because the destination firewall uses a static IP allowlist that hasn't been updated, or relies on a stale GeoIP database (for example, many regional prefixes in asia-* and europe-* are carved out of Google's ARIN 34.0.0.0/8 block, which still shows Country: US in raw WHOIS if the firewall doesn't ingest Google's IP feeds). To verify how Google advertises your egress IPs externally, you can cross-reference your project's reserved addresses from the Method: addresses.list | Compute Engine API against cloud.json.

Google publishes cloud.json as the machine-readable feed of all customer-usable external IPv4 and IPv6 ranges by region, as documented in Configure Private Google Access | Virtual Private Cloud - Google Cloud Documentation. When you are setting up a new Cloud NAT gateway or troubleshooting an outbound block with a partner network team, you can use this feed to quickly confirm which published CIDR block covers each of your external IPs and when the feed was last updated.

Automating the check with gcloud and jq

To avoid manually searching through more than a thousand prefixes in cloud.json, you can script this check. The script below uses gcloud compute addresses list | Google Cloud SDK to grab your reserved external IPs as JSON, fetches cloud.json, and uses jq to match each address to its published CIDR range and regional scope:

#!/bin/bash

# --- Configuration (Replace with your details) ---
PROJECT="<YOUR_PROJECT_ID>"
REGION="<YOUR_REGION>"
CLOUD_IPS_URL="https://www.gstatic.com/ipranges/cloud.json"

echo "Checking external IPs in $PROJECT ($REGION) against $CLOUD_IPS_URL..."
echo "------------------------------------------------------------------------"

# Fetch reserved external IPs and match each IP against cloud.json CIDRs in jq
jq -rn \
  --argjson addrs "$(gcloud compute addresses list \
    --project="$PROJECT" \
    --regions="$REGION" \
    --filter="addressType=EXTERNAL" \
    --format=json)" \
  --argjson cloud "$(curl -s "$CLOUD_IPS_URL")" '
  # Convert an IPv4 address string into a 32-bit integer for CIDR matching
  def ip2int:
    split(".") | map(tonumber) | (.[0] * 16777216) + (.[1] * 65536) + (.[2] * 256) + .[3];

  $addrs[] as $a |
  ($a.address | ip2int) as $ip_int |
  (
    [
      $cloud.prefixes[]
      | select(has("ipv4Prefix"))
      | (.ipv4Prefix | split("/")) as $parts
      | ($parts[0] | ip2int) as $net_int
      | ($parts[1] | tonumber) as $mask
      | (pow(2; 32 - $mask)) as $size
      | select($ip_int >= $net_int and $ip_int < ($net_int + $size))
    ][0] // { ipv4Prefix: "UNLISTED", scope: "UNKNOWN" }
  ) as $match |
  "Name: \($a.name) | IP: \($a.address) | Tier: \($a.networkTier) | Prefix: \($match.ipv4Prefix) | Scope: \($match.scope)"
'
Enter fullscreen mode Exit fullscreen mode

Let's list the reserved external IP addresses in our target region first:

~  gcloud compute addresses list \
    --project="my-gcp-project" \
    --regions="us-south1" \
    --format='table(name, address, region.basename(), networkTier, status)'

NAME       ADDRESS         REGION     NETWORK_TIER  STATUS
nat-ip-a0  34.174.136.135  us-south1  PREMIUM       RESERVED
nat-ip-a1  34.174.242.159  us-south1  PREMIUM       RESERVED
nat-ip-a2  34.0.132.72     us-south1  STANDARD      RESERVED
Enter fullscreen mode Exit fullscreen mode

Running the script maps each external IP to its parent CIDR block and regional scope in cloud.json:

~  bash gce-egress-prefix-check.sh

Checking external IPs in my-gcp-project (us-south1) against https://www.gstatic.com/ipranges/cloud.json...
------------------------------------------------------------------------
Name: nat-ip-a0 | IP: 34.174.136.135 | Tier: PREMIUM | Prefix: 34.174.0.0/16 | Scope: us-south1
Name: nat-ip-a1 | IP: 34.174.242.159 | Tier: PREMIUM | Prefix: 34.174.0.0/16 | Scope: us-south1
Name: nat-ip-a2 | IP: 34.0.132.72 | Tier: STANDARD | Prefix: 34.0.128.0/19 | Scope: us-south1
Enter fullscreen mode Exit fullscreen mode

Under the hood: Reading the cloud.json payload

When you fetch cloud.json, the response includes a top-level syncToken and creationTime, followed by a prefixes array. For IPv4 ranges in us-south1, the JSON payload looks like this:

{
  "syncToken": "1791792540784",
  "creationTime": "2026-10-12T01:09:00.784842",
  "prefixes": [
    {
      "ipv4Prefix": "34.0.128.0/19",
      "service": "Google Cloud",
      "scope": "us-south1"
    },
    {
      "ipv4Prefix": "34.174.0.0/16",
      "service": "Google Cloud",
      "scope": "us-south1"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Notice that nat-ip-a0 (PREMIUM tier) and nat-ip-a2 (STANDARD tier) come from different CIDR blocks (34.174.0.0/16 vs. 34.0.128.0/19) within us-south1. Compute Engine tracks the network service tier on each address resource (networkTier), while cloud.json groups both Premium and Standard tier external ranges under the same regional scope, as described in IP addresses | Compute Engine - Google Cloud Documentation:

{
  "address": "34.0.132.72",
  "addressType": "EXTERNAL",
  "kind": "compute#address",
  "name": "nat-ip-a2",
  "networkTier": "STANDARD",
  "region": "https://www.googleapis.com/compute/v1/projects/my-gcp-project/regions/us-south1",
  "status": "RESERVED"
}
Enter fullscreen mode Exit fullscreen mode

If you are using external IPv6 ranges on dual-stack subnets, cloud.json also includes the regional /44 IPv6 blocks under the ipv6Prefix field:

{
  "ipv6Prefix": "2600:1901:8140::/44",
  "service": "Google Cloud",
  "scope": "us-south1"
}
Enter fullscreen mode Exit fullscreen mode

A few quick constraints

Next steps

Next time an external endpoint drops traffic from a new region or Cloud NAT gateway, run this script to confirm the exact ipv4Prefix and creationTime in cloud.json. If the destination firewall hasn't picked up the prefix yet, sharing the specific regional block (or your reserved /32 NAT IPs) gives the remote network team what they need to update their allowlist.

Top comments (1)

Collapse
 
m_montazeri profile image
Mohammad Montazeri •

One small dual-stack edge case in the script: select(has("ipv4Prefix")) filters the cloud.json entries, but $a.address reaches ip2int before that filter. An external IPv6 reservation in the address list would therefore reach the IPv4-only split(".") | map(tonumber) path.

For this IPv4 example, filter the address records to ipVersion == "IPV4" before calling ip2int, or explicitly handle IPv6 separately. The Compute API documents ipVersion as IPV4 or IPV6: docs.cloud.google.com/compute/docs...

A mixed IPv4/IPv6 fixture would be a useful check for that case. This is a code-reading observation; I have not run it against your project.

AI-assisted technical feedback; I work on Lisar Connect.