Skip to content

Commit 7a73e2b

Browse files
committed
Add VPC sample for retrieving Google APIs and services IP ranges
Adds `get_google_ip_ranges` under `vpc/` as the modernized and hardened replacement for the archived `networking-tools-python/tools/cidr` tool. This sample computes the IP address ranges (CIDRs) used exclusively for Google APIs and default domain services by calculating the set difference `(goog.json - cloud.json)`. Key improvements over the archived implementation: - Python 3.9+ baseline with full PEP 484 type annotations. - Network hardening with explicit 30s timeouts (`urlopen(url, timeout=30)`). - Safe JSON & prefix parsing with graceful recovery on malformed entries. - Replaced legacy unittest + localhost HTTP server with hermetic `pytest` unit tests (8/8 passing). - Fully compliant with `flake8`, `black`, and `nox` style standards. Output Comparison: - Normal Execution: Outputs the exact same 360+ default domain CIDR blocks as the legacy tool. - Error Handling: Fixes a legacy bug where network/parsing failures caused an unhandled `TypeError: unsupported operand type(s) for -: 'NoneType' and 'IPSet'` crash; the new implementation exits cleanly with a descriptive `ValueError`. Testing: - Live verification: Ran `python get_google_ip_ranges.py` against live `gstatic.com` endpoints. - Unit testing: `pytest -v` (8/8 tests passing). - Linter & formatter: `flake8 .` (0 errors) and `black --check .` (clean).
1 parent 19ac84e commit 7a73e2b

6 files changed

Lines changed: 461 additions & 0 deletions

File tree

vpc/get_google_ip_ranges/README.md

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# Google Cloud VPC: Get Google APIs and Services IP Ranges
2+
3+
This sample shows how to retrieve Google's public IP range feeds and compute the IP address ranges (CIDRs) used exclusively for Google APIs and default domain services (such as `*.googleapis.com`).
4+
5+
It calculates the IP set difference `(goog.json - cloud.json)` to separate default Google API domains from customer Google Cloud resource IPs, which is useful for configuring VPC firewall rules, custom routing, and [Private Google Access](https://cloud.google.com/vpc/docs/private-google-access).
6+
7+
## Setup
8+
9+
1. Create and activate a Python virtual environment:
10+
11+
```bash
12+
python3 -m venv env
13+
source env/bin/activate
14+
```
15+
16+
2. Install the dependencies needed to run the sample:
17+
18+
```bash
19+
pip install -r requirements.txt
20+
```
21+
22+
## How to Run
23+
24+
Run the sample script directly with Python:
25+
26+
```bash
27+
python get_google_ip_ranges.py
28+
```
29+
30+
### Example Output
31+
32+
```text
33+
Fetched https://www.gstatic.com/ipranges/goog.json (published: 2026-08-11T13:04:46.50592)
34+
Fetched https://www.gstatic.com/ipranges/cloud.json (published: 2026-08-11T13:04:46.50592)
35+
IP ranges for Google APIs and services default domains:
36+
8.8.4.0/24
37+
8.8.8.0/24
38+
8.35.200.0/21
39+
...
40+
2600:1900::/34
41+
2600:1901:1::/48
42+
```
43+
44+
## Testing
45+
46+
### Run Tests with Pytest
47+
48+
1. Install test dependencies:
49+
50+
```bash
51+
pip install -r requirements-test.txt
52+
```
53+
54+
2. Run the test suite:
55+
56+
```bash
57+
pytest
58+
```
59+
60+
### Run Tests and Linting with Nox
61+
62+
This sample uses [Nox](https://github.com/wntrblm/nox) for automated testing and linting.
63+
64+
1. Install `nox`:
65+
66+
```bash
67+
pip install nox
68+
```
69+
70+
2. Run code style linting:
71+
72+
```bash
73+
nox -s lint
74+
```
75+
76+
3. Run code formatting with Black:
77+
78+
```bash
79+
nox -s blacken
80+
```
81+
82+
4. Run tests against a specific Python version:
83+
84+
```bash
85+
nox -s py-3.13
86+
```
87+
88+
## References
89+
90+
* [Google Cloud Sample Authoring Guide](https://github.com/GoogleCloudPlatform/python-docs-samples/blob/main/AUTHORING_GUIDE.md)
91+
* [Private Google Access Configuration](https://cloud.google.com/vpc/docs/configure-private-google-access#ip-addr-defaults)
92+
* [Google IP Address Ranges](https://cloud.google.com/vpc/docs/private-google-access#ip-addr-defaults)
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# [START vpc_get_google_ip_ranges]
16+
from __future__ import annotations
17+
18+
import json
19+
from urllib.error import URLError
20+
from urllib.request import urlopen
21+
22+
import netaddr
23+
24+
# Google publishes two official IP range feeds:
25+
# - goog: Complete list of all Google-owned IP ranges (Search, YouTube, APIs, etc.)
26+
# - cloud: External IP ranges allocated for customer Google Cloud resources
27+
IPRANGE_URLS: dict[str, str] = {
28+
"goog": "https://www.gstatic.com/ipranges/goog.json",
29+
"cloud": "https://www.gstatic.com/ipranges/cloud.json",
30+
}
31+
32+
33+
def get_data(url: str) -> netaddr.IPSet | None:
34+
"""Fetches JSON IP range data from a URL and converts prefixes into an IPSet.
35+
36+
Args:
37+
url: The public URL of the JSON IP ranges feed.
38+
39+
Returns:
40+
A netaddr.IPSet containing all parsed IPv4 and IPv6 network prefixes,
41+
or None if the network request or JSON parsing fails.
42+
"""
43+
# Step 1: Download and parse the JSON feed
44+
try:
45+
with urlopen(url, timeout=30) as response:
46+
data = json.load(response)
47+
except (URLError, json.JSONDecodeError, TimeoutError) as error:
48+
print(f"ERROR: Failed to fetch or parse {url}: {error}")
49+
return None
50+
51+
# Verify root payload is a JSON object
52+
if not isinstance(data, dict):
53+
print(f"ERROR: Expected JSON object from {url}, got {type(data).__name__}")
54+
return None
55+
56+
creation_time = data.get("creationTime", "Unknown")
57+
print(f"Fetched {url} (published: {creation_time})")
58+
59+
# Step 2: Extract both IPv4 and IPv6 CIDR prefixes into an IPSet
60+
cidr_set = netaddr.IPSet()
61+
prefixes = data.get("prefixes", [])
62+
if isinstance(prefixes, list):
63+
for prefix_entry in prefixes:
64+
if not isinstance(prefix_entry, dict):
65+
continue
66+
try:
67+
if "ipv4Prefix" in prefix_entry:
68+
cidr_set.add(prefix_entry["ipv4Prefix"])
69+
if "ipv6Prefix" in prefix_entry:
70+
cidr_set.add(prefix_entry["ipv6Prefix"])
71+
except (netaddr.AddrFormatError, ValueError) as error:
72+
print(f"WARNING: Skipping invalid prefix in {url}: {error}")
73+
74+
return cidr_set
75+
76+
77+
def main() -> None:
78+
"""Calculates and displays CIDR ranges for Google APIs and default domain services.
79+
80+
By subtracting customer Google Cloud IP ranges ('cloud') from all Google IP
81+
ranges ('goog'), we isolate the IP ranges used exclusively for Google APIs
82+
and default services (such as *.googleapis.com).
83+
"""
84+
# Step 1: Fetch and parse IP ranges for both 'goog' and 'cloud' feeds
85+
cidrs: dict[str, netaddr.IPSet] = {}
86+
for feed_name, url in IPRANGE_URLS.items():
87+
feed_cidrs = get_data(url)
88+
89+
if feed_cidrs is None:
90+
raise ValueError(f"ERROR: Could not process data from {url}")
91+
92+
cidrs[feed_name] = feed_cidrs
93+
94+
# Step 2: Compute set difference (goog - cloud)
95+
# This removes customer cloud IPs, leaving only default Google APIs/services
96+
default_domain_cidrs = cidrs["goog"] - cidrs["cloud"]
97+
98+
# Step 3: Print the aggregated CIDR list
99+
print("IP ranges for Google APIs and services default domains:")
100+
for cidr in default_domain_cidrs.iter_cidrs():
101+
print(cidr)
102+
103+
104+
if __name__ == "__main__":
105+
main()
106+
# [END vpc_get_google_ip_ranges]

0 commit comments

Comments
 (0)