From d736901442fd35b67e63292c8904a784defdc126 Mon Sep 17 00:00:00 2001 From: Shubham Rai Date: Mon, 12 Oct 2026 18:25:46 +0530 Subject: [PATCH 1/7] feat: real EU gateway data, 42 clusters on gateway.truefoundry.ai Replaces the placeholder scaffold with the LLMGateway Clusters fleet. The wiki renders paired NAT addresses as one unseparated string (18.119.53.2363.130.37.65). Several split more than one way with every candidate parsing as valid IPv4, so each was resolved against AWS ip-ranges.json, GCP cloud.json and Azure service tags, accepting a split only when both addresses fall inside the region the row claims. Two do not split the way they look: tfy-aws-fra-gtw -> 3.65.93.12 + 63.181.139.3 tfy-aws-hnd-gtw -> 54.238.73.1 + 18.178.26.39 Two wiki region codes are also wrong and corrected here: mad is eu-south-2 (not ap-south-2) and zrh is eu-central-2 (not eu-central-1). Adds a status field: 15 clusters are live with a published endpoint, 27 have NAT addresses allocated but no endpoint yet. Both are published so customers can allowlist ahead of a region activating. hostname is now required only for live entries, and must agree with its airport_code. Control-plane IPs for both planes, and the US gateway fleet, are still to come; those files are absent rather than stubbed. --- CONTRIBUTING.md | 12 +- README.md | 125 ++++---- data/eu/control-plane.json | 19 -- data/eu/gateway.json | 556 +++++++++++++++++++++++++++++++++- data/us/control-plane.json | 19 -- data/us/gateway.json | 21 -- dist/eu.json | 601 ++++++++++++++++++++++++++++++++++++- dist/eu.txt | 86 +++++- dist/nat-ips.json | 601 +++++++++++++++++++++++++++++++++++-- dist/nat-ips.txt | 86 +++++- schema/nat-ips.schema.json | 32 +- scripts/build.py | 15 +- scripts/release_notes.py | 31 +- scripts/validate.py | 6 + 14 files changed, 2037 insertions(+), 173 deletions(-) delete mode 100644 data/eu/control-plane.json delete mode 100644 data/us/control-plane.json delete mode 100644 data/us/gateway.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 38770e9..7b42b96 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,7 +22,7 @@ Anything security-sensitive goes to security@truefoundry.com, not a public issue ### Adding a regional gateway PoP -Gateway entries need both an `airport_code` (lowercase IATA) and a `hostname`: +Gateway entries need an `airport_code` (lowercase IATA) and a `status`. A `live` entry also needs a `hostname`, whose first label must match the airport code: ```json { @@ -31,11 +31,19 @@ Gateway entries need both an `airport_code` (lowercase IATA) and a `hostname`: "region": "asia-south1", "continent": "ap", "airport_code": "bom", - "hostname": "bom.us.truefoundry.cloud", + "status": "live", + "cluster": "tfy-gcp-bom-gtw", + "hostname": "bom.gateway.truefoundry.ai", "nat_ips": ["34.100.0.1/32"] } ``` +Use `"status": "provisioned"` and omit `hostname` while the NAT addresses are allocated but the endpoint isn't published. Flip to `live` and add the hostname when it goes into service — that flip is itself a release, which is what tells customers the region is active. + +### Transcribing from the wiki + +The *LLMGateway Clusters* page renders paired NAT addresses as one unseparated string (`18.119.53.2363.130.37.65`). Several of those split more than one way and **all candidates parse as valid IPv4**, so do not eyeball them. Resolve each against the provider's published ranges — AWS `ip-ranges.json`, GCP `cloud.json`, Azure service tags — and accept a split only when both addresses fall inside the region the row claims. Two entries currently in `data/` do not split the way they look; see the Provenance section in the README. + ### Timing Publish **before** the IPs go live. Customers need lead time to get a firewall change through their own approval process. Retiring an IP is lower risk, but still announce it ahead of the cutover. diff --git a/README.md b/README.md index ea41a36..6fe7669 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # TrueFoundry NAT IPs -The outbound (egress) IP addresses TrueFoundry connects **from**. If you restrict inbound traffic to your model endpoints, webhooks, databases, or private APIs, allowlist the IPs for your control plane. +The outbound (egress) IP addresses TrueFoundry connects **from**. If you restrict inbound traffic to your model endpoints, webhooks, databases, or private APIs, allowlist these. Published as machine-readable JSON with a [GitHub Release](https://github.com/truefoundry/nat-ips/releases) for every change, so you can subscribe and automate. -> **IPs change.** New regions get added and cloud NAT gateways get renumbered. Subscribe to releases rather than copying the list once. +> **IPs change.** Regions get added and cloud NAT gateways get renumbered. Subscribe to releases rather than copying the list once. --- @@ -15,7 +15,6 @@ Always-latest URLs — these follow the newest release: | What | URL | | --- | --- | | Everything | `https://github.com/truefoundry/nat-ips/releases/latest/download/nat-ips.json` | -| US control plane only | `https://github.com/truefoundry/nat-ips/releases/latest/download/us.json` | | EU control plane only | `https://github.com/truefoundry/nat-ips/releases/latest/download/eu.json` | | Plain text, one CIDR per line | swap `.json` → `.txt` in any URL above | @@ -25,70 +24,53 @@ Pin to an exact version by replacing `latest/download` with `download/v2026.10.1 # Every IP, ready to paste into a firewall rule curl -sL https://github.com/truefoundry/nat-ips/releases/latest/download/nat-ips.txt | grep -v '^#' -# Just the EU gateway PoPs -curl -sL https://github.com/truefoundry/nat-ips/releases/latest/download/eu.json \ - | jq -r '.entries[] | select(.type == "gateway") | .nat_ips[]' +# Only regions currently serving traffic +curl -sL https://github.com/truefoundry/nat-ips/releases/latest/download/nat-ips.json \ + | jq -r '.entries[] | select(.status == "live") | .nat_ips[]' ``` -## Which IPs do I need? +## Coverage -Pick by the TrueFoundry domain you were onboarded onto. - -| Your setup | Allowlist | +| Plane | Status | | --- | --- | -| Control plane at `*.us.truefoundry.cloud` | `us.json` | -| Control plane at `*.eu.truefoundry.cloud` | `eu.json` | -| Not sure, or you use both | `nat-ips.json` | - -Within a control plane there are two kinds of entry: +| AI Gateway — `*.gateway.truefoundry.ai` | **Published**, 42 regions | +| EU control plane — `*.eu.truefoundry.cloud` | Not yet published | +| US control plane — `*.us.truefoundry.cloud` | Not yet published | -- **`control_plane`** — the TrueFoundry platform itself. Covers deployments, webhooks, and integrations calling out to your systems. -- **`gateway`** — regional AI Gateway points of presence. Traffic to your self-hosted or private model endpoints comes from these. Each has a `hostname` (for example `mad.us.truefoundry.cloud`) and an `airport_code`. +If you call models through the AI Gateway, what's published today is what you need. Control-plane egress IPs are coming; watch releases to be notified. -**If you only serve models through the AI Gateway, the `gateway` entries alone are enough.** Most customers need both. +## live vs provisioned -## Subscribe to changes +Every gateway entry carries a `status`: -Pick whichever fits how you work: +- **`live`** — the regional endpoint is serving traffic today. Has a `hostname` such as `mad.gateway.truefoundry.ai`. +- **`provisioned`** — the NAT addresses are allocated and reserved for a region that isn't published yet. No `hostname`. -**1. GitHub notifications** — click **Watch → Custom → Releases** at the top of this repo. Everyone on your team who wants alerts should do this individually. +**Allowlist both.** A provisioned IP sends you no traffic until that region activates, so permitting it early costs nothing — and it means a new region doesn't turn into an urgent firewall change on your side. -**2. RSS / Atom** — point any feed reader, Slack RSS app, or internal bot at: - -``` -https://github.com/truefoundry/nat-ips/releases.atom -``` - -In Slack: `/feed subscribe https://github.com/truefoundry/nat-ips/releases.atom` - -**3. Poll from automation** — check the `version` field and act when it changes: - -```bash -VERSION=$(curl -sL https://github.com/truefoundry/nat-ips/releases/latest/download/nat-ips.json | jq -r .version) -``` - -Every release note lists the exact IPs **added** and **removed** versus the previous release, so a diff is never required on your side. - -> Add newly published IPs promptly. Removing retired IPs is optional and can wait for your next maintenance window — stale entries are harmless. +Currently 15 live, 27 provisioned. ## Data format ```json { "version": "2026.10.12", - "generation_timestamp": "2026-10-12T09:14:03Z", - "entry_count": 4, - "ip_count": 8, + "generation_timestamp": "2026-10-12T12:52:47Z", + "entry_count": 42, + "ip_count": 84, + "live_count": 15, + "provisioned_count": 27, "entries": [ { - "control_plane": "us", + "control_plane": "eu", "type": "gateway", - "host": "mad.us.truefoundry.cloud", - "gateway_host": "us.truefoundry.cloud", "cloud": "gcp", "region": "europe-southwest1", "continent": "eu", + "status": "live", "airport_code": "mad", + "gateway_host": "gateway.truefoundry.ai", + "host": "mad.gateway.truefoundry.ai", "nat_ips": ["34.175.44.33/32", "34.175.219.109/32"] } ] @@ -99,34 +81,58 @@ Every release note lists the exact IPs **added** and **removed** versus the prev | --- | --- | | `version` | CalVer release, matching the git tag `v` | | `generation_timestamp` | UTC build time, ISO 8601 | -| `control_plane` | `us` or `eu` | +| `control_plane` | `us` or `eu` — which control plane manages this cluster | | `type` | `control_plane` or `gateway` | -| `host` | The endpoint this entry serves. Wildcard for control planes, regional FQDN for gateways | -| `gateway_host` | Gateway entries only — the global entry point that routes to this PoP | +| `status` | Gateway entries only — `live` or `provisioned` | | `cloud` / `region` | Where the egress NAT lives | +| `continent` | `na`, `sa`, `eu`, `ap`, `me`, `af`, `oc` | | `airport_code` | Gateway entries only — IATA code for the PoP | +| `gateway_host` | Gateway entries only — the global entry point | +| `host` | The endpoint this entry serves. Absent on `provisioned` entries, which have none yet | | `nat_ips` | IPv4 CIDRs. Single addresses are written `/32` | +> `airport_code` is **not unique**. Two clusters in the same metro on different clouds share a code — `sfo` is both AWS `us-west-1` and Azure `westus`. Key on `cluster`/`region`, not the airport code. + Entries are sorted deterministically and IPs are sorted numerically, so a `git diff` between versions is readable. JSON Schema for the source files: [`schema/nat-ips.schema.json`](schema/nat-ips.schema.json). +## Subscribe to changes + +**1. GitHub notifications** — **Watch → Custom → Releases** at the top of this repo. Each person who wants alerts does this individually. + +**2. RSS / Atom** — point a feed reader, the Slack RSS app, or an internal bot at: + +``` +https://github.com/truefoundry/nat-ips/releases.atom +``` + +In Slack: `/feed subscribe https://github.com/truefoundry/nat-ips/releases.atom` + +**3. Poll from automation** — check `version` and act when it changes: + +```bash +VERSION=$(curl -sL https://github.com/truefoundry/nat-ips/releases/latest/download/nat-ips.json | jq -r .version) +``` + +Every release note lists the exact IPs **added** and **removed** versus the previous release, so you never need to diff on your side. + +> Add newly published IPs promptly. Removing retired IPs is optional and can wait for your next maintenance window — stale entries are harmless. + --- ## For TrueFoundry engineers -Source of truth is `data//.json`. Everything in `dist/` is generated — never edit it by hand. +Source of truth is `data//.json`. Everything in `dist/` is generated — never edit it by hand. ``` data/ -├── us/ -│ ├── control-plane.json # *.us.truefoundry.cloud -│ └── gateway.json # us.truefoundry.cloud + regional PoPs └── eu/ - ├── control-plane.json # *.eu.truefoundry.cloud - └── gateway.json # eu.truefoundry.cloud + regional PoPs + └── gateway.json # 42 clusters on *.gateway.truefoundry.ai ``` +Still to add: `data/eu/control-plane.json`, `data/us/control-plane.json`, `data/us/gateway.json`. + To add or change IPs: 1. Edit the relevant file under `data/`. @@ -140,12 +146,23 @@ uv run scripts/validate.py uv run scripts/build.py --version 0.0.0-local ``` -Validation rejects malformed CIDRs, host bits set on a prefix, private or reserved addresses, overlapping ranges within an entry, gateway entries missing a `hostname` or `airport_code`, and a `control_plane` that disagrees with its directory. Duplicate IPs across entries are reported as warnings. +Validation rejects malformed CIDRs, host bits set on a prefix, private or reserved addresses, overlapping ranges within an entry, `live` entries missing a `hostname`, a `hostname` whose leading label disagrees with its `airport_code`, gateway entries missing `airport_code` or `status`, and a `control_plane` that disagrees with its directory. Duplicate IPs across entries are reported as warnings. To preview a release without publishing: **Actions → Release → Run workflow → dry run**. > The release workflow pushes a commit to `main`. If you enable branch protection, allow `github-actions[bot]` to bypass it, or the release step will fail. +### Provenance + +The gateway data was transcribed from the internal *LLMGateway Clusters* wiki page, where paired NAT addresses render as one unseparated string (`18.119.53.2363.130.37.65`). Those splits are ambiguous — several parse more than one way — so each was resolved against the provider's published ranges (AWS `ip-ranges.json`, GCP `cloud.json`, Azure service tags) and accepted only when both addresses fall inside the claimed region. Two rows do not split the way they look: + +- `tfy-aws-fra-gtw` → `3.65.93.12` + `63.181.139.3` (not `3.65.93.126` + `3.181.139.3`) +- `tfy-aws-hnd-gtw` → `54.238.73.1` + `18.178.26.39` (not `54.238.73.11` + `8.178.26.39`) + +Two region codes on the wiki are also wrong and are corrected here: `tfy-aws-mad-gtw` is `eu-south-2` (wiki says `ap-south-2`) and `tfy-aws-zrh-gtw` is `eu-central-2` (wiki says `eu-central-1`). + +**Re-verify after any bulk edit** rather than trusting a copy-paste from the wiki. + ## Reporting a problem Something wrong or missing? [Open an issue](https://github.com/truefoundry/nat-ips/issues) or contact TrueFoundry support. For anything security-sensitive, email security@truefoundry.com instead of filing a public issue. diff --git a/data/eu/control-plane.json b/data/eu/control-plane.json deleted file mode 100644 index 064a191..0000000 --- a/data/eu/control-plane.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "$schema": "../../schema/nat-ips.schema.json", - "plane": "control_plane", - "control_plane": "eu", - "host_pattern": "*.eu.truefoundry.cloud", - "entries": [ - { - "type": "control_plane", - "cloud": "aws", - "region": "eu-west-1", - "continent": "eu", - "nat_ips": [ - "34.175.219.109/32", - "34.175.44.33/32" - ], - "notes": "PLACEHOLDER - not real data, replace before making this repo public" - } - ] -} diff --git a/data/eu/gateway.json b/data/eu/gateway.json index 051bc0b..d7bc1f7 100644 --- a/data/eu/gateway.json +++ b/data/eu/gateway.json @@ -2,20 +2,568 @@ "$schema": "../../schema/nat-ips.schema.json", "plane": "gateway", "control_plane": "eu", - "host": "eu.truefoundry.cloud", + "host": "gateway.truefoundry.ai", "entries": [ + { + "type": "gateway", + "cloud": "aws", + "region": "us-east-1", + "continent": "na", + "airport_code": "orf", + "status": "live", + "cluster": "tfy-aws-orf-gtw", + "hostname": "orf.gateway.truefoundry.ai", + "nat_ips": [ + "52.4.51.133/32", + "54.204.114.10/32" + ] + }, + { + "type": "gateway", + "cloud": "aws", + "region": "us-east-2", + "continent": "na", + "airport_code": "cle", + "status": "provisioned", + "cluster": "tfy-aws-cle-gte", + "nat_ips": [ + "18.119.53.236/32", + "3.130.37.65/32" + ] + }, + { + "type": "gateway", + "cloud": "aws", + "region": "us-west-1", + "continent": "na", + "airport_code": "sfo", + "status": "provisioned", + "cluster": "tfy-aws-sfo-gtw", + "nat_ips": [ + "13.52.112.39/32", + "54.193.213.45/32" + ] + }, + { + "type": "gateway", + "cloud": "aws", + "region": "us-west-2", + "continent": "na", + "airport_code": "pdx", + "status": "provisioned", + "cluster": "tfy-aws-pdx-gtw", + "nat_ips": [ + "44.236.36.218/32", + "34.210.236.98/32" + ] + }, + { + "type": "gateway", + "cloud": "aws", + "region": "mx-central-1", + "continent": "na", + "airport_code": "mex", + "status": "provisioned", + "cluster": "tfy-aws-mex-gtw", + "nat_ips": [ + "78.13.15.252/32", + "78.13.183.82/32" + ] + }, + { + "type": "gateway", + "cloud": "gcp", + "region": "southamerica-east1", + "continent": "sa", + "airport_code": "gru", + "status": "live", + "cluster": "tfy-gcp-gru-gtw", + "hostname": "gru.gateway.truefoundry.ai", + "nat_ips": [ + "34.39.220.236/32", + "34.39.220.106/32" + ] + }, + { + "type": "gateway", + "cloud": "aws", + "region": "sa-east-1", + "continent": "sa", + "airport_code": "gru", + "status": "provisioned", + "cluster": "tfy-aws-gru-gtw", + "nat_ips": [ + "56.126.81.140/32", + "56.126.18.155/32" + ] + }, + { + "type": "gateway", + "cloud": "gcp", + "region": "northamerica-northeast2", + "continent": "na", + "airport_code": "yyz", + "status": "live", + "cluster": "tfy-gcp-yyz-gtw", + "hostname": "yyz.gateway.truefoundry.ai", + "nat_ips": [ + "34.130.196.232/32", + "34.130.234.128/32" + ] + }, + { + "type": "gateway", + "cloud": "aws", + "region": "ca-central-1", + "continent": "na", + "airport_code": "yyz", + "status": "provisioned", + "cluster": "tfy-aws-yyz-gtw", + "nat_ips": [ + "16.52.203.122/32", + "15.156.30.166/32" + ] + }, + { + "type": "gateway", + "cloud": "aws", + "region": "ca-west-1", + "continent": "na", + "airport_code": "yyc", + "status": "provisioned", + "cluster": "tfy-aws-yyc-gtw", + "nat_ips": [ + "40.177.86.240/32", + "40.176.147.21/32" + ] + }, + { + "type": "gateway", + "cloud": "azure", + "region": "westus", + "continent": "na", + "airport_code": "sfo", + "status": "live", + "cluster": "tfy-az-sfo-gtw", + "hostname": "sfo.gateway.truefoundry.ai", + "nat_ips": [ + "40.75.129.73/32", + "20.245.207.53/32" + ] + }, + { + "type": "gateway", + "cloud": "gcp", + "region": "us-south1", + "continent": "na", + "airport_code": "dfw", + "status": "live", + "cluster": "tfy-gcp-dfw-gtw", + "hostname": "dfw.gateway.truefoundry.ai", + "nat_ips": [ + "34.174.18.136/32", + "34.174.240.150/32" + ] + }, + { + "type": "gateway", + "cloud": "aws", + "region": "eu-west-2", + "continent": "eu", + "airport_code": "lhr", + "status": "live", + "cluster": "tfy-aws-lhr-gtw", + "hostname": "lhr.gateway.truefoundry.ai", + "nat_ips": [ + "18.130.148.118/32", + "13.42.115.230/32" + ] + }, + { + "type": "gateway", + "cloud": "aws", + "region": "eu-west-3", + "continent": "eu", + "airport_code": "cdg", + "status": "provisioned", + "cluster": "tfy-aws-cdg-gtw", + "nat_ips": [ + "13.39.61.233/32", + "15.237.200.183/32" + ] + }, + { + "type": "gateway", + "cloud": "aws", + "region": "eu-west-1", + "continent": "eu", + "airport_code": "dub", + "status": "provisioned", + "cluster": "tfy-aws-dub-gtw", + "nat_ips": [ + "54.72.115.226/32", + "54.246.5.151/32" + ] + }, { "type": "gateway", "cloud": "gcp", "region": "europe-southwest1", "continent": "eu", "airport_code": "mad", - "hostname": "mad.eu.truefoundry.cloud", + "status": "live", + "cluster": "tfy-gcp-mad-gtw", + "hostname": "mad.gateway.truefoundry.ai", "nat_ips": [ "34.175.219.109/32", "34.175.44.33/32" - ], - "notes": "PLACEHOLDER - not real data, replace before making this repo public" + ] + }, + { + "type": "gateway", + "cloud": "aws", + "region": "eu-south-2", + "continent": "eu", + "airport_code": "mad", + "status": "provisioned", + "cluster": "tfy-aws-mad-gtw", + "nat_ips": [ + "51.48.180.27/32", + "15.216.64.85/32" + ] + }, + { + "type": "gateway", + "cloud": "aws", + "region": "eu-south-1", + "continent": "eu", + "airport_code": "mxp", + "status": "provisioned", + "cluster": "tfy-aws-mxp-gtw", + "nat_ips": [ + "51.118.102.128/32", + "18.102.33.104/32" + ] + }, + { + "type": "gateway", + "cloud": "aws", + "region": "eu-central-1", + "continent": "eu", + "airport_code": "fra", + "status": "provisioned", + "cluster": "tfy-aws-fra-gtw", + "nat_ips": [ + "3.65.93.12/32", + "63.181.139.3/32" + ] + }, + { + "type": "gateway", + "cloud": "aws", + "region": "eu-central-2", + "continent": "eu", + "airport_code": "zrh", + "status": "provisioned", + "cluster": "tfy-aws-zrh-gtw", + "nat_ips": [ + "51.96.196.104/32", + "51.96.80.198/32" + ] + }, + { + "type": "gateway", + "cloud": "azure", + "region": "swedencentral", + "continent": "eu", + "airport_code": "gvx", + "status": "live", + "cluster": "tfy-az-gvx-gtw", + "hostname": "gvx.gateway.truefoundry.ai", + "nat_ips": [ + "4.165.129.123/32", + "4.165.99.208/32" + ] + }, + { + "type": "gateway", + "cloud": "aws", + "region": "eu-north-1", + "continent": "eu", + "airport_code": "arn", + "status": "provisioned", + "cluster": "tfy-aws-arn-gtw", + "nat_ips": [ + "13.48.213.79/32", + "13.62.133.235/32" + ] + }, + { + "type": "gateway", + "cloud": "aws", + "region": "af-south-1", + "continent": "af", + "airport_code": "cpt", + "status": "live", + "cluster": "tfy-aws-cpt-gtw", + "hostname": "cpt.gateway.truefoundry.ai", + "nat_ips": [ + "13.244.129.178/32", + "13.246.115.110/32" + ] + }, + { + "type": "gateway", + "cloud": "gcp", + "region": "me-central1", + "continent": "me", + "airport_code": "dia", + "status": "live", + "cluster": "tfy-gcp-dia-gtw", + "hostname": "dia.gateway.truefoundry.ai", + "nat_ips": [ + "34.18.22.15/32", + "34.18.192.62/32" + ] + }, + { + "type": "gateway", + "cloud": "aws", + "region": "me-south-1", + "continent": "me", + "airport_code": "bah", + "status": "provisioned", + "cluster": "tfy-aws-bah-gtw", + "nat_ips": [ + "157.175.127.180/32", + "157.241.96.125/32" + ] + }, + { + "type": "gateway", + "cloud": "aws", + "region": "me-central-1", + "continent": "me", + "airport_code": "dxb", + "status": "provisioned", + "cluster": "tfy-aws-dxb-gtw", + "nat_ips": [ + "40.172.236.59/32", + "3.29.12.147/32" + ] + }, + { + "type": "gateway", + "cloud": "aws", + "region": "il-central-1", + "continent": "me", + "airport_code": "tlv", + "status": "provisioned", + "cluster": "tfy-aws-tlv-gtw", + "nat_ips": [ + "51.84.87.166/32", + "51.16.181.62/32" + ] + }, + { + "type": "gateway", + "cloud": "aws", + "region": "ap-south-1", + "continent": "ap", + "airport_code": "bom", + "status": "live", + "cluster": "tfy-aws-bom-gtw", + "hostname": "bom.gateway.truefoundry.ai", + "nat_ips": [ + "13.126.176.139/32", + "15.207.191.126/32" + ] + }, + { + "type": "gateway", + "cloud": "aws", + "region": "ap-south-2", + "continent": "ap", + "airport_code": "hyd", + "status": "provisioned", + "cluster": "tfy-aws-hyd-gtw", + "nat_ips": [ + "16.112.48.100/32", + "40.192.126.53/32" + ] + }, + { + "type": "gateway", + "cloud": "aws", + "region": "ap-east-1", + "continent": "ap", + "airport_code": "hkg", + "status": "provisioned", + "cluster": "tfy-aws-hkg-gtw", + "nat_ips": [ + "16.163.170.188/32", + "18.166.19.152/32" + ] + }, + { + "type": "gateway", + "cloud": "aws", + "region": "ap-east-2", + "continent": "ap", + "airport_code": "tpe", + "status": "provisioned", + "cluster": "tfy-aws-tpe-gtw", + "nat_ips": [ + "43.213.3.104/32", + "43.212.202.18/32" + ] + }, + { + "type": "gateway", + "cloud": "aws", + "region": "ap-southeast-7", + "continent": "ap", + "airport_code": "bkk", + "status": "provisioned", + "cluster": "tfy-aws-bkk-gtw", + "nat_ips": [ + "43.210.99.107/32", + "43.209.246.149/32" + ] + }, + { + "type": "gateway", + "cloud": "aws", + "region": "ap-southeast-3", + "continent": "ap", + "airport_code": "cgk", + "status": "provisioned", + "cluster": "tfy-aws-cgk-gtw", + "nat_ips": [ + "16.79.100.152/32", + "15.232.2.7/32" + ] + }, + { + "type": "gateway", + "cloud": "aws", + "region": "ap-southeast-1", + "continent": "ap", + "airport_code": "sin", + "status": "live", + "cluster": "tfy-aws-sin-gtw", + "hostname": "sin.gateway.truefoundry.ai", + "nat_ips": [ + "13.251.96.74/32", + "18.143.12.175/32" + ] + }, + { + "type": "gateway", + "cloud": "aws", + "region": "ap-southeast-4", + "continent": "oc", + "airport_code": "mel", + "status": "live", + "cluster": "tfy-aws-mel-gtw", + "hostname": "mel.gateway.truefoundry.ai", + "nat_ips": [ + "16.51.101.207/32", + "16.51.12.109/32" + ] + }, + { + "type": "gateway", + "cloud": "aws", + "region": "ap-southeast-6", + "continent": "oc", + "airport_code": "akl", + "status": "provisioned", + "cluster": "tfy-aws-akl-gtw", + "nat_ips": [ + "3.103.30.44/32", + "3.102.184.172/32" + ] + }, + { + "type": "gateway", + "cloud": "aws", + "region": "ap-southeast-5", + "continent": "ap", + "airport_code": "kul", + "status": "provisioned", + "cluster": "tfy-aws-kul-gtw", + "nat_ips": [ + "56.68.75.208/32", + "56.68.6.121/32" + ] + }, + { + "type": "gateway", + "cloud": "aws", + "region": "ap-southeast-2", + "continent": "oc", + "airport_code": "syd", + "status": "live", + "cluster": "tfy-aws-syd-gtw", + "hostname": "syd.gateway.truefoundry.ai", + "nat_ips": [ + "13.237.191.204/32", + "3.105.148.45/32" + ] + }, + { + "type": "gateway", + "cloud": "aws", + "region": "ap-northeast-1", + "continent": "ap", + "airport_code": "hnd", + "status": "provisioned", + "cluster": "tfy-aws-hnd-gtw", + "nat_ips": [ + "54.238.73.1/32", + "18.178.26.39/32" + ] + }, + { + "type": "gateway", + "cloud": "aws", + "region": "ap-northeast-2", + "continent": "ap", + "airport_code": "icn", + "status": "provisioned", + "cluster": "tfy-aws-icn-gtw", + "nat_ips": [ + "43.200.144.76/32", + "43.200.33.159/32" + ] + }, + { + "type": "gateway", + "cloud": "aws", + "region": "ap-northeast-3", + "continent": "ap", + "airport_code": "kix", + "status": "provisioned", + "cluster": "tfy-aws-kix-gtw", + "nat_ips": [ + "15.152.62.45/32", + "15.168.90.90/32" + ] + }, + { + "type": "gateway", + "cloud": "azure", + "region": "australiacentral", + "continent": "oc", + "airport_code": "cbr", + "status": "live", + "cluster": "tfy-az-cbr-gtw", + "hostname": "cbr.gateway.truefoundry.ai", + "nat_ips": [ + "20.28.68.101/32", + "20.53.28.45/32" + ] } ] } diff --git a/data/us/control-plane.json b/data/us/control-plane.json deleted file mode 100644 index 8584b7e..0000000 --- a/data/us/control-plane.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "$schema": "../../schema/nat-ips.schema.json", - "plane": "control_plane", - "control_plane": "us", - "host_pattern": "*.us.truefoundry.cloud", - "entries": [ - { - "type": "control_plane", - "cloud": "aws", - "region": "us-east-1", - "continent": "na", - "nat_ips": [ - "34.175.219.109/32", - "34.175.44.33/32" - ], - "notes": "PLACEHOLDER - not real data, replace before making this repo public" - } - ] -} diff --git a/data/us/gateway.json b/data/us/gateway.json deleted file mode 100644 index a696723..0000000 --- a/data/us/gateway.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "$schema": "../../schema/nat-ips.schema.json", - "plane": "gateway", - "control_plane": "us", - "host": "us.truefoundry.cloud", - "entries": [ - { - "type": "gateway", - "cloud": "gcp", - "region": "europe-southwest1", - "continent": "eu", - "airport_code": "mad", - "hostname": "mad.us.truefoundry.cloud", - "nat_ips": [ - "34.175.219.109/32", - "34.175.44.33/32" - ], - "notes": "PLACEHOLDER - not real data, replace before making this repo public" - } - ] -} diff --git a/dist/eu.json b/dist/eu.json index c887327..e8e4056 100644 --- a/dist/eu.json +++ b/dist/eu.json @@ -1,36 +1,613 @@ { - "version": "0.0.0-unreleased", - "generation_timestamp": "2026-10-12T12:23:55Z", - "entry_count": 2, - "ip_count": 2, + "version": "2026.10.12", + "generation_timestamp": "2026-10-12T12:52:47Z", + "entry_count": 42, + "ip_count": 84, + "live_count": 15, + "provisioned_count": 27, "entries": [ { "control_plane": "eu", - "type": "control_plane", - "host": "*.eu.truefoundry.cloud", + "type": "gateway", + "cloud": "aws", + "region": "ap-southeast-6", + "continent": "oc", + "status": "provisioned", + "airport_code": "akl", + "gateway_host": "gateway.truefoundry.ai", + "nat_ips": [ + "3.102.184.172/32", + "3.103.30.44/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "eu-north-1", + "continent": "eu", + "status": "provisioned", + "airport_code": "arn", + "gateway_host": "gateway.truefoundry.ai", + "nat_ips": [ + "13.48.213.79/32", + "13.62.133.235/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "me-south-1", + "continent": "me", + "status": "provisioned", + "airport_code": "bah", + "gateway_host": "gateway.truefoundry.ai", + "nat_ips": [ + "157.175.127.180/32", + "157.241.96.125/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "ap-southeast-7", + "continent": "ap", + "status": "provisioned", + "airport_code": "bkk", + "gateway_host": "gateway.truefoundry.ai", + "nat_ips": [ + "43.209.246.149/32", + "43.210.99.107/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "ap-south-1", + "continent": "ap", + "status": "live", + "airport_code": "bom", + "gateway_host": "gateway.truefoundry.ai", + "host": "bom.gateway.truefoundry.ai", + "nat_ips": [ + "13.126.176.139/32", + "15.207.191.126/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "azure", + "region": "australiacentral", + "continent": "oc", + "status": "live", + "airport_code": "cbr", + "gateway_host": "gateway.truefoundry.ai", + "host": "cbr.gateway.truefoundry.ai", + "nat_ips": [ + "20.28.68.101/32", + "20.53.28.45/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "eu-west-3", + "continent": "eu", + "status": "provisioned", + "airport_code": "cdg", + "gateway_host": "gateway.truefoundry.ai", + "nat_ips": [ + "13.39.61.233/32", + "15.237.200.183/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "ap-southeast-3", + "continent": "ap", + "status": "provisioned", + "airport_code": "cgk", + "gateway_host": "gateway.truefoundry.ai", + "nat_ips": [ + "15.232.2.7/32", + "16.79.100.152/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "us-east-2", + "continent": "na", + "status": "provisioned", + "airport_code": "cle", + "gateway_host": "gateway.truefoundry.ai", + "nat_ips": [ + "3.130.37.65/32", + "18.119.53.236/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "af-south-1", + "continent": "af", + "status": "live", + "airport_code": "cpt", + "gateway_host": "gateway.truefoundry.ai", + "host": "cpt.gateway.truefoundry.ai", + "nat_ips": [ + "13.244.129.178/32", + "13.246.115.110/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "gcp", + "region": "us-south1", + "continent": "na", + "status": "live", + "airport_code": "dfw", + "gateway_host": "gateway.truefoundry.ai", + "host": "dfw.gateway.truefoundry.ai", + "nat_ips": [ + "34.174.18.136/32", + "34.174.240.150/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "gcp", + "region": "me-central1", + "continent": "me", + "status": "live", + "airport_code": "dia", + "gateway_host": "gateway.truefoundry.ai", + "host": "dia.gateway.truefoundry.ai", + "nat_ips": [ + "34.18.22.15/32", + "34.18.192.62/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", "cloud": "aws", "region": "eu-west-1", "continent": "eu", - "notes": "PLACEHOLDER - not real data, replace before making this repo public", + "status": "provisioned", + "airport_code": "dub", + "gateway_host": "gateway.truefoundry.ai", "nat_ips": [ - "34.175.44.33/32", - "34.175.219.109/32" + "54.72.115.226/32", + "54.246.5.151/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "me-central-1", + "continent": "me", + "status": "provisioned", + "airport_code": "dxb", + "gateway_host": "gateway.truefoundry.ai", + "nat_ips": [ + "3.29.12.147/32", + "40.172.236.59/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "eu-central-1", + "continent": "eu", + "status": "provisioned", + "airport_code": "fra", + "gateway_host": "gateway.truefoundry.ai", + "nat_ips": [ + "3.65.93.12/32", + "63.181.139.3/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "sa-east-1", + "continent": "sa", + "status": "provisioned", + "airport_code": "gru", + "gateway_host": "gateway.truefoundry.ai", + "nat_ips": [ + "56.126.18.155/32", + "56.126.81.140/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "gcp", + "region": "southamerica-east1", + "continent": "sa", + "status": "live", + "airport_code": "gru", + "gateway_host": "gateway.truefoundry.ai", + "host": "gru.gateway.truefoundry.ai", + "nat_ips": [ + "34.39.220.106/32", + "34.39.220.236/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "azure", + "region": "swedencentral", + "continent": "eu", + "status": "live", + "airport_code": "gvx", + "gateway_host": "gateway.truefoundry.ai", + "host": "gvx.gateway.truefoundry.ai", + "nat_ips": [ + "4.165.99.208/32", + "4.165.129.123/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "ap-east-1", + "continent": "ap", + "status": "provisioned", + "airport_code": "hkg", + "gateway_host": "gateway.truefoundry.ai", + "nat_ips": [ + "16.163.170.188/32", + "18.166.19.152/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "ap-northeast-1", + "continent": "ap", + "status": "provisioned", + "airport_code": "hnd", + "gateway_host": "gateway.truefoundry.ai", + "nat_ips": [ + "18.178.26.39/32", + "54.238.73.1/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "ap-south-2", + "continent": "ap", + "status": "provisioned", + "airport_code": "hyd", + "gateway_host": "gateway.truefoundry.ai", + "nat_ips": [ + "16.112.48.100/32", + "40.192.126.53/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "ap-northeast-2", + "continent": "ap", + "status": "provisioned", + "airport_code": "icn", + "gateway_host": "gateway.truefoundry.ai", + "nat_ips": [ + "43.200.33.159/32", + "43.200.144.76/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "ap-northeast-3", + "continent": "ap", + "status": "provisioned", + "airport_code": "kix", + "gateway_host": "gateway.truefoundry.ai", + "nat_ips": [ + "15.152.62.45/32", + "15.168.90.90/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "ap-southeast-5", + "continent": "ap", + "status": "provisioned", + "airport_code": "kul", + "gateway_host": "gateway.truefoundry.ai", + "nat_ips": [ + "56.68.6.121/32", + "56.68.75.208/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "eu-west-2", + "continent": "eu", + "status": "live", + "airport_code": "lhr", + "gateway_host": "gateway.truefoundry.ai", + "host": "lhr.gateway.truefoundry.ai", + "nat_ips": [ + "13.42.115.230/32", + "18.130.148.118/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "eu-south-2", + "continent": "eu", + "status": "provisioned", + "airport_code": "mad", + "gateway_host": "gateway.truefoundry.ai", + "nat_ips": [ + "15.216.64.85/32", + "51.48.180.27/32" ] }, { "control_plane": "eu", "type": "gateway", - "host": "mad.eu.truefoundry.cloud", "cloud": "gcp", "region": "europe-southwest1", "continent": "eu", - "gateway_host": "eu.truefoundry.cloud", + "status": "live", "airport_code": "mad", - "notes": "PLACEHOLDER - not real data, replace before making this repo public", + "gateway_host": "gateway.truefoundry.ai", + "host": "mad.gateway.truefoundry.ai", "nat_ips": [ "34.175.44.33/32", "34.175.219.109/32" ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "ap-southeast-4", + "continent": "oc", + "status": "live", + "airport_code": "mel", + "gateway_host": "gateway.truefoundry.ai", + "host": "mel.gateway.truefoundry.ai", + "nat_ips": [ + "16.51.12.109/32", + "16.51.101.207/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "mx-central-1", + "continent": "na", + "status": "provisioned", + "airport_code": "mex", + "gateway_host": "gateway.truefoundry.ai", + "nat_ips": [ + "78.13.15.252/32", + "78.13.183.82/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "eu-south-1", + "continent": "eu", + "status": "provisioned", + "airport_code": "mxp", + "gateway_host": "gateway.truefoundry.ai", + "nat_ips": [ + "18.102.33.104/32", + "51.118.102.128/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "us-east-1", + "continent": "na", + "status": "live", + "airport_code": "orf", + "gateway_host": "gateway.truefoundry.ai", + "host": "orf.gateway.truefoundry.ai", + "nat_ips": [ + "52.4.51.133/32", + "54.204.114.10/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "us-west-2", + "continent": "na", + "status": "provisioned", + "airport_code": "pdx", + "gateway_host": "gateway.truefoundry.ai", + "nat_ips": [ + "34.210.236.98/32", + "44.236.36.218/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "us-west-1", + "continent": "na", + "status": "provisioned", + "airport_code": "sfo", + "gateway_host": "gateway.truefoundry.ai", + "nat_ips": [ + "13.52.112.39/32", + "54.193.213.45/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "azure", + "region": "westus", + "continent": "na", + "status": "live", + "airport_code": "sfo", + "gateway_host": "gateway.truefoundry.ai", + "host": "sfo.gateway.truefoundry.ai", + "nat_ips": [ + "20.245.207.53/32", + "40.75.129.73/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "ap-southeast-1", + "continent": "ap", + "status": "live", + "airport_code": "sin", + "gateway_host": "gateway.truefoundry.ai", + "host": "sin.gateway.truefoundry.ai", + "nat_ips": [ + "13.251.96.74/32", + "18.143.12.175/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "ap-southeast-2", + "continent": "oc", + "status": "live", + "airport_code": "syd", + "gateway_host": "gateway.truefoundry.ai", + "host": "syd.gateway.truefoundry.ai", + "nat_ips": [ + "3.105.148.45/32", + "13.237.191.204/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "il-central-1", + "continent": "me", + "status": "provisioned", + "airport_code": "tlv", + "gateway_host": "gateway.truefoundry.ai", + "nat_ips": [ + "51.16.181.62/32", + "51.84.87.166/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "ap-east-2", + "continent": "ap", + "status": "provisioned", + "airport_code": "tpe", + "gateway_host": "gateway.truefoundry.ai", + "nat_ips": [ + "43.212.202.18/32", + "43.213.3.104/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "ca-west-1", + "continent": "na", + "status": "provisioned", + "airport_code": "yyc", + "gateway_host": "gateway.truefoundry.ai", + "nat_ips": [ + "40.176.147.21/32", + "40.177.86.240/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "ca-central-1", + "continent": "na", + "status": "provisioned", + "airport_code": "yyz", + "gateway_host": "gateway.truefoundry.ai", + "nat_ips": [ + "15.156.30.166/32", + "16.52.203.122/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "gcp", + "region": "northamerica-northeast2", + "continent": "na", + "status": "live", + "airport_code": "yyz", + "gateway_host": "gateway.truefoundry.ai", + "host": "yyz.gateway.truefoundry.ai", + "nat_ips": [ + "34.130.196.232/32", + "34.130.234.128/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "eu-central-2", + "continent": "eu", + "status": "provisioned", + "airport_code": "zrh", + "gateway_host": "gateway.truefoundry.ai", + "nat_ips": [ + "51.96.80.198/32", + "51.96.196.104/32" + ] } ] } diff --git a/dist/eu.txt b/dist/eu.txt index aeea4a1..bf44df2 100644 --- a/dist/eu.txt +++ b/dist/eu.txt @@ -1,6 +1,88 @@ # TrueFoundry outbound NAT IPs -# version: 0.0.0-unreleased -# generated: 2026-10-12T12:23:55Z +# version: 2026.10.12 +# generated: 2026-10-12T12:52:47Z # https://github.com/truefoundry/nat-ips +3.29.12.147/32 +3.65.93.12/32 +3.102.184.172/32 +3.103.30.44/32 +3.105.148.45/32 +3.130.37.65/32 +4.165.99.208/32 +4.165.129.123/32 +13.39.61.233/32 +13.42.115.230/32 +13.48.213.79/32 +13.52.112.39/32 +13.62.133.235/32 +13.126.176.139/32 +13.237.191.204/32 +13.244.129.178/32 +13.246.115.110/32 +13.251.96.74/32 +15.152.62.45/32 +15.156.30.166/32 +15.168.90.90/32 +15.207.191.126/32 +15.216.64.85/32 +15.232.2.7/32 +15.237.200.183/32 +16.51.12.109/32 +16.51.101.207/32 +16.52.203.122/32 +16.79.100.152/32 +16.112.48.100/32 +16.163.170.188/32 +18.102.33.104/32 +18.119.53.236/32 +18.130.148.118/32 +18.143.12.175/32 +18.166.19.152/32 +18.178.26.39/32 +20.28.68.101/32 +20.53.28.45/32 +20.245.207.53/32 +34.18.22.15/32 +34.18.192.62/32 +34.39.220.106/32 +34.39.220.236/32 +34.130.196.232/32 +34.130.234.128/32 +34.174.18.136/32 +34.174.240.150/32 34.175.44.33/32 34.175.219.109/32 +34.210.236.98/32 +40.75.129.73/32 +40.172.236.59/32 +40.176.147.21/32 +40.177.86.240/32 +40.192.126.53/32 +43.200.33.159/32 +43.200.144.76/32 +43.209.246.149/32 +43.210.99.107/32 +43.212.202.18/32 +43.213.3.104/32 +44.236.36.218/32 +51.16.181.62/32 +51.48.180.27/32 +51.84.87.166/32 +51.96.80.198/32 +51.96.196.104/32 +51.118.102.128/32 +52.4.51.133/32 +54.72.115.226/32 +54.193.213.45/32 +54.204.114.10/32 +54.238.73.1/32 +54.246.5.151/32 +56.68.6.121/32 +56.68.75.208/32 +56.126.18.155/32 +56.126.81.140/32 +63.181.139.3/32 +78.13.15.252/32 +78.13.183.82/32 +157.175.127.180/32 +157.241.96.125/32 diff --git a/dist/nat-ips.json b/dist/nat-ips.json index 242a7c4..e8e4056 100644 --- a/dist/nat-ips.json +++ b/dist/nat-ips.json @@ -1,63 +1,612 @@ { - "version": "0.0.0-unreleased", - "generation_timestamp": "2026-10-12T12:23:55Z", - "entry_count": 4, - "ip_count": 2, + "version": "2026.10.12", + "generation_timestamp": "2026-10-12T12:52:47Z", + "entry_count": 42, + "ip_count": 84, + "live_count": 15, + "provisioned_count": 27, "entries": [ { "control_plane": "eu", - "type": "control_plane", - "host": "*.eu.truefoundry.cloud", + "type": "gateway", + "cloud": "aws", + "region": "ap-southeast-6", + "continent": "oc", + "status": "provisioned", + "airport_code": "akl", + "gateway_host": "gateway.truefoundry.ai", + "nat_ips": [ + "3.102.184.172/32", + "3.103.30.44/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "eu-north-1", + "continent": "eu", + "status": "provisioned", + "airport_code": "arn", + "gateway_host": "gateway.truefoundry.ai", + "nat_ips": [ + "13.48.213.79/32", + "13.62.133.235/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "me-south-1", + "continent": "me", + "status": "provisioned", + "airport_code": "bah", + "gateway_host": "gateway.truefoundry.ai", + "nat_ips": [ + "157.175.127.180/32", + "157.241.96.125/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "ap-southeast-7", + "continent": "ap", + "status": "provisioned", + "airport_code": "bkk", + "gateway_host": "gateway.truefoundry.ai", + "nat_ips": [ + "43.209.246.149/32", + "43.210.99.107/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "ap-south-1", + "continent": "ap", + "status": "live", + "airport_code": "bom", + "gateway_host": "gateway.truefoundry.ai", + "host": "bom.gateway.truefoundry.ai", + "nat_ips": [ + "13.126.176.139/32", + "15.207.191.126/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "azure", + "region": "australiacentral", + "continent": "oc", + "status": "live", + "airport_code": "cbr", + "gateway_host": "gateway.truefoundry.ai", + "host": "cbr.gateway.truefoundry.ai", + "nat_ips": [ + "20.28.68.101/32", + "20.53.28.45/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "eu-west-3", + "continent": "eu", + "status": "provisioned", + "airport_code": "cdg", + "gateway_host": "gateway.truefoundry.ai", + "nat_ips": [ + "13.39.61.233/32", + "15.237.200.183/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "ap-southeast-3", + "continent": "ap", + "status": "provisioned", + "airport_code": "cgk", + "gateway_host": "gateway.truefoundry.ai", + "nat_ips": [ + "15.232.2.7/32", + "16.79.100.152/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "us-east-2", + "continent": "na", + "status": "provisioned", + "airport_code": "cle", + "gateway_host": "gateway.truefoundry.ai", + "nat_ips": [ + "3.130.37.65/32", + "18.119.53.236/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "af-south-1", + "continent": "af", + "status": "live", + "airport_code": "cpt", + "gateway_host": "gateway.truefoundry.ai", + "host": "cpt.gateway.truefoundry.ai", + "nat_ips": [ + "13.244.129.178/32", + "13.246.115.110/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "gcp", + "region": "us-south1", + "continent": "na", + "status": "live", + "airport_code": "dfw", + "gateway_host": "gateway.truefoundry.ai", + "host": "dfw.gateway.truefoundry.ai", + "nat_ips": [ + "34.174.18.136/32", + "34.174.240.150/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "gcp", + "region": "me-central1", + "continent": "me", + "status": "live", + "airport_code": "dia", + "gateway_host": "gateway.truefoundry.ai", + "host": "dia.gateway.truefoundry.ai", + "nat_ips": [ + "34.18.22.15/32", + "34.18.192.62/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", "cloud": "aws", "region": "eu-west-1", "continent": "eu", - "notes": "PLACEHOLDER - not real data, replace before making this repo public", + "status": "provisioned", + "airport_code": "dub", + "gateway_host": "gateway.truefoundry.ai", "nat_ips": [ - "34.175.44.33/32", - "34.175.219.109/32" + "54.72.115.226/32", + "54.246.5.151/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "me-central-1", + "continent": "me", + "status": "provisioned", + "airport_code": "dxb", + "gateway_host": "gateway.truefoundry.ai", + "nat_ips": [ + "3.29.12.147/32", + "40.172.236.59/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "eu-central-1", + "continent": "eu", + "status": "provisioned", + "airport_code": "fra", + "gateway_host": "gateway.truefoundry.ai", + "nat_ips": [ + "3.65.93.12/32", + "63.181.139.3/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "sa-east-1", + "continent": "sa", + "status": "provisioned", + "airport_code": "gru", + "gateway_host": "gateway.truefoundry.ai", + "nat_ips": [ + "56.126.18.155/32", + "56.126.81.140/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "gcp", + "region": "southamerica-east1", + "continent": "sa", + "status": "live", + "airport_code": "gru", + "gateway_host": "gateway.truefoundry.ai", + "host": "gru.gateway.truefoundry.ai", + "nat_ips": [ + "34.39.220.106/32", + "34.39.220.236/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "azure", + "region": "swedencentral", + "continent": "eu", + "status": "live", + "airport_code": "gvx", + "gateway_host": "gateway.truefoundry.ai", + "host": "gvx.gateway.truefoundry.ai", + "nat_ips": [ + "4.165.99.208/32", + "4.165.129.123/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "ap-east-1", + "continent": "ap", + "status": "provisioned", + "airport_code": "hkg", + "gateway_host": "gateway.truefoundry.ai", + "nat_ips": [ + "16.163.170.188/32", + "18.166.19.152/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "ap-northeast-1", + "continent": "ap", + "status": "provisioned", + "airport_code": "hnd", + "gateway_host": "gateway.truefoundry.ai", + "nat_ips": [ + "18.178.26.39/32", + "54.238.73.1/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "ap-south-2", + "continent": "ap", + "status": "provisioned", + "airport_code": "hyd", + "gateway_host": "gateway.truefoundry.ai", + "nat_ips": [ + "16.112.48.100/32", + "40.192.126.53/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "ap-northeast-2", + "continent": "ap", + "status": "provisioned", + "airport_code": "icn", + "gateway_host": "gateway.truefoundry.ai", + "nat_ips": [ + "43.200.33.159/32", + "43.200.144.76/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "ap-northeast-3", + "continent": "ap", + "status": "provisioned", + "airport_code": "kix", + "gateway_host": "gateway.truefoundry.ai", + "nat_ips": [ + "15.152.62.45/32", + "15.168.90.90/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "ap-southeast-5", + "continent": "ap", + "status": "provisioned", + "airport_code": "kul", + "gateway_host": "gateway.truefoundry.ai", + "nat_ips": [ + "56.68.6.121/32", + "56.68.75.208/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "eu-west-2", + "continent": "eu", + "status": "live", + "airport_code": "lhr", + "gateway_host": "gateway.truefoundry.ai", + "host": "lhr.gateway.truefoundry.ai", + "nat_ips": [ + "13.42.115.230/32", + "18.130.148.118/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "eu-south-2", + "continent": "eu", + "status": "provisioned", + "airport_code": "mad", + "gateway_host": "gateway.truefoundry.ai", + "nat_ips": [ + "15.216.64.85/32", + "51.48.180.27/32" ] }, { "control_plane": "eu", "type": "gateway", - "host": "mad.eu.truefoundry.cloud", "cloud": "gcp", "region": "europe-southwest1", "continent": "eu", - "gateway_host": "eu.truefoundry.cloud", + "status": "live", "airport_code": "mad", - "notes": "PLACEHOLDER - not real data, replace before making this repo public", + "gateway_host": "gateway.truefoundry.ai", + "host": "mad.gateway.truefoundry.ai", "nat_ips": [ "34.175.44.33/32", "34.175.219.109/32" ] }, { - "control_plane": "us", - "type": "control_plane", - "host": "*.us.truefoundry.cloud", + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "ap-southeast-4", + "continent": "oc", + "status": "live", + "airport_code": "mel", + "gateway_host": "gateway.truefoundry.ai", + "host": "mel.gateway.truefoundry.ai", + "nat_ips": [ + "16.51.12.109/32", + "16.51.101.207/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "mx-central-1", + "continent": "na", + "status": "provisioned", + "airport_code": "mex", + "gateway_host": "gateway.truefoundry.ai", + "nat_ips": [ + "78.13.15.252/32", + "78.13.183.82/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "eu-south-1", + "continent": "eu", + "status": "provisioned", + "airport_code": "mxp", + "gateway_host": "gateway.truefoundry.ai", + "nat_ips": [ + "18.102.33.104/32", + "51.118.102.128/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", "cloud": "aws", "region": "us-east-1", "continent": "na", - "notes": "PLACEHOLDER - not real data, replace before making this repo public", + "status": "live", + "airport_code": "orf", + "gateway_host": "gateway.truefoundry.ai", + "host": "orf.gateway.truefoundry.ai", "nat_ips": [ - "34.175.44.33/32", - "34.175.219.109/32" + "52.4.51.133/32", + "54.204.114.10/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "us-west-2", + "continent": "na", + "status": "provisioned", + "airport_code": "pdx", + "gateway_host": "gateway.truefoundry.ai", + "nat_ips": [ + "34.210.236.98/32", + "44.236.36.218/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "us-west-1", + "continent": "na", + "status": "provisioned", + "airport_code": "sfo", + "gateway_host": "gateway.truefoundry.ai", + "nat_ips": [ + "13.52.112.39/32", + "54.193.213.45/32" ] }, { - "control_plane": "us", + "control_plane": "eu", + "type": "gateway", + "cloud": "azure", + "region": "westus", + "continent": "na", + "status": "live", + "airport_code": "sfo", + "gateway_host": "gateway.truefoundry.ai", + "host": "sfo.gateway.truefoundry.ai", + "nat_ips": [ + "20.245.207.53/32", + "40.75.129.73/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "ap-southeast-1", + "continent": "ap", + "status": "live", + "airport_code": "sin", + "gateway_host": "gateway.truefoundry.ai", + "host": "sin.gateway.truefoundry.ai", + "nat_ips": [ + "13.251.96.74/32", + "18.143.12.175/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "ap-southeast-2", + "continent": "oc", + "status": "live", + "airport_code": "syd", + "gateway_host": "gateway.truefoundry.ai", + "host": "syd.gateway.truefoundry.ai", + "nat_ips": [ + "3.105.148.45/32", + "13.237.191.204/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "il-central-1", + "continent": "me", + "status": "provisioned", + "airport_code": "tlv", + "gateway_host": "gateway.truefoundry.ai", + "nat_ips": [ + "51.16.181.62/32", + "51.84.87.166/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "ap-east-2", + "continent": "ap", + "status": "provisioned", + "airport_code": "tpe", + "gateway_host": "gateway.truefoundry.ai", + "nat_ips": [ + "43.212.202.18/32", + "43.213.3.104/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "ca-west-1", + "continent": "na", + "status": "provisioned", + "airport_code": "yyc", + "gateway_host": "gateway.truefoundry.ai", + "nat_ips": [ + "40.176.147.21/32", + "40.177.86.240/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "ca-central-1", + "continent": "na", + "status": "provisioned", + "airport_code": "yyz", + "gateway_host": "gateway.truefoundry.ai", + "nat_ips": [ + "15.156.30.166/32", + "16.52.203.122/32" + ] + }, + { + "control_plane": "eu", "type": "gateway", - "host": "mad.us.truefoundry.cloud", "cloud": "gcp", - "region": "europe-southwest1", + "region": "northamerica-northeast2", + "continent": "na", + "status": "live", + "airport_code": "yyz", + "gateway_host": "gateway.truefoundry.ai", + "host": "yyz.gateway.truefoundry.ai", + "nat_ips": [ + "34.130.196.232/32", + "34.130.234.128/32" + ] + }, + { + "control_plane": "eu", + "type": "gateway", + "cloud": "aws", + "region": "eu-central-2", "continent": "eu", - "gateway_host": "us.truefoundry.cloud", - "airport_code": "mad", - "notes": "PLACEHOLDER - not real data, replace before making this repo public", + "status": "provisioned", + "airport_code": "zrh", + "gateway_host": "gateway.truefoundry.ai", "nat_ips": [ - "34.175.44.33/32", - "34.175.219.109/32" + "51.96.80.198/32", + "51.96.196.104/32" ] } ] diff --git a/dist/nat-ips.txt b/dist/nat-ips.txt index aeea4a1..bf44df2 100644 --- a/dist/nat-ips.txt +++ b/dist/nat-ips.txt @@ -1,6 +1,88 @@ # TrueFoundry outbound NAT IPs -# version: 0.0.0-unreleased -# generated: 2026-10-12T12:23:55Z +# version: 2026.10.12 +# generated: 2026-10-12T12:52:47Z # https://github.com/truefoundry/nat-ips +3.29.12.147/32 +3.65.93.12/32 +3.102.184.172/32 +3.103.30.44/32 +3.105.148.45/32 +3.130.37.65/32 +4.165.99.208/32 +4.165.129.123/32 +13.39.61.233/32 +13.42.115.230/32 +13.48.213.79/32 +13.52.112.39/32 +13.62.133.235/32 +13.126.176.139/32 +13.237.191.204/32 +13.244.129.178/32 +13.246.115.110/32 +13.251.96.74/32 +15.152.62.45/32 +15.156.30.166/32 +15.168.90.90/32 +15.207.191.126/32 +15.216.64.85/32 +15.232.2.7/32 +15.237.200.183/32 +16.51.12.109/32 +16.51.101.207/32 +16.52.203.122/32 +16.79.100.152/32 +16.112.48.100/32 +16.163.170.188/32 +18.102.33.104/32 +18.119.53.236/32 +18.130.148.118/32 +18.143.12.175/32 +18.166.19.152/32 +18.178.26.39/32 +20.28.68.101/32 +20.53.28.45/32 +20.245.207.53/32 +34.18.22.15/32 +34.18.192.62/32 +34.39.220.106/32 +34.39.220.236/32 +34.130.196.232/32 +34.130.234.128/32 +34.174.18.136/32 +34.174.240.150/32 34.175.44.33/32 34.175.219.109/32 +34.210.236.98/32 +40.75.129.73/32 +40.172.236.59/32 +40.176.147.21/32 +40.177.86.240/32 +40.192.126.53/32 +43.200.33.159/32 +43.200.144.76/32 +43.209.246.149/32 +43.210.99.107/32 +43.212.202.18/32 +43.213.3.104/32 +44.236.36.218/32 +51.16.181.62/32 +51.48.180.27/32 +51.84.87.166/32 +51.96.80.198/32 +51.96.196.104/32 +51.118.102.128/32 +52.4.51.133/32 +54.72.115.226/32 +54.193.213.45/32 +54.204.114.10/32 +54.238.73.1/32 +54.246.5.151/32 +56.68.6.121/32 +56.68.75.208/32 +56.126.18.155/32 +56.126.81.140/32 +63.181.139.3/32 +78.13.15.252/32 +78.13.183.82/32 +157.175.127.180/32 +157.241.96.125/32 diff --git a/schema/nat-ips.schema.json b/schema/nat-ips.schema.json index a671c3b..51420cb 100644 --- a/schema/nat-ips.schema.json +++ b/schema/nat-ips.schema.json @@ -56,12 +56,20 @@ "airport_code": { "type": "string", "pattern": "^[a-z]{3}$", - "description": "IATA code for the PoP. Required for gateway entries." + "description": "IATA code for the PoP. Required for gateway entries. Not unique: two clusters in the same metro on different clouds share a code." + }, + "status": { + "enum": ["live", "provisioned"], + "description": "Required for gateway entries. 'live' means the regional endpoint is serving; 'provisioned' means the NAT addresses are allocated and reserved but the endpoint is not published yet. Allowlist both — a provisioned IP costs nothing and avoids a rush when the region is activated." }, "hostname": { "type": "string", "format": "hostname", - "description": "Regional gateway endpoint customers resolve. Required for gateway entries." + "description": "Regional gateway endpoint customers resolve, e.g. mad.gateway.truefoundry.ai. Required when status is 'live'." + }, + "cluster": { + "type": "string", + "description": "Internal cluster name, so this file can be reconciled against terraform state." }, "notes": { "type": "string" @@ -84,7 +92,25 @@ "required": ["type"] }, "then": { - "required": ["airport_code", "hostname"] + "required": ["airport_code", "status"] + } + }, + { + "if": { + "properties": { "status": { "const": "live" } }, + "required": ["status"] + }, + "then": { + "required": ["hostname"] + } + }, + { + "if": { + "properties": { "type": { "const": "control_plane" } }, + "required": ["type"] + }, + "then": { + "not": { "required": ["status"] } } } ] diff --git a/scripts/build.py b/scripts/build.py index e5aab5a..15652dd 100644 --- a/scripts/build.py +++ b/scripts/build.py @@ -44,15 +44,22 @@ def load_entries() -> list[dict]: entry = { "control_plane": control_plane, "type": source["type"], - # host is always the endpoint a customer actually talks to. - "host": source.get("hostname") or doc.get("host_pattern") or doc.get("host"), "cloud": source["cloud"], "region": source["region"], "continent": source["continent"], } + if source["type"] == "gateway": - entry["gateway_host"] = doc["host"] + entry["status"] = source["status"] entry["airport_code"] = source["airport_code"] + entry["gateway_host"] = doc["host"] + # A provisioned cluster has no published endpoint yet, so it gets no host. + # Inventing one would tell customers to resolve a name that does not exist. + if "hostname" in source: + entry["host"] = source["hostname"] + else: + entry["host"] = doc.get("host_pattern") or doc.get("host") + if "notes" in source: entry["notes"] = source["notes"] @@ -101,6 +108,8 @@ def envelope(subset: list[dict]) -> dict: "generation_timestamp": timestamp, "entry_count": len(subset), "ip_count": len(flat_ip_list(subset)), + "live_count": sum(1 for e in subset if e.get("status") != "provisioned"), + "provisioned_count": sum(1 for e in subset if e.get("status") == "provisioned"), "entries": subset, } diff --git a/scripts/release_notes.py b/scripts/release_notes.py index d137eae..c6dd035 100644 --- a/scripts/release_notes.py +++ b/scripts/release_notes.py @@ -39,6 +39,16 @@ def describe(entry: dict) -> str: return " · ".join(bits) +def host_label(entry: dict) -> str: + """A provisioned cluster has no endpoint, so say that rather than print empty backticks.""" + host = entry.get("host") + if host: + return f"`{host}`" + if entry.get("status") == "provisioned": + return "_provisioned, no endpoint yet_" + return "_no endpoint_" + + def ip_sort(cidr: str) -> ipaddress.IPv4Network: return ipaddress.IPv4Network(cidr) @@ -51,11 +61,11 @@ def render_changed(title: str, cidrs: list[str], index: dict[str, list[dict]]) - buckets: dict[tuple[str, str], set[str]] = defaultdict(set) for cidr in cidrs: for entry in index[cidr]: - buckets[(describe(entry), entry.get("host", ""))].add(cidr) + buckets[(describe(entry), host_label(entry))].add(cidr) lines = [f"### {title} ({len(cidrs)})", ""] for (label, host), group in sorted(buckets.items()): - lines.append(f"**{label}** — `{host}`") + lines.append(f"**{label}** — {host}") lines.extend(f"- `{cidr}`" for cidr in sorted(group, key=ip_sort)) lines.append("") return lines @@ -65,7 +75,7 @@ def render_full(doc: dict) -> list[str]: """List every entry in order — used for the first release, where nothing is a 'change'.""" lines = ["### Published IPs", ""] for entry in doc.get("entries", []): - lines.append(f"**{describe(entry)}** — `{entry.get('host', '')}`") + lines.append(f"**{describe(entry)}** — {host_label(entry)}") lines.extend(f"- `{cidr}`" for cidr in entry["nat_ips"]) lines.append("") return lines @@ -118,18 +128,27 @@ def main() -> int: lines += render_changed("Added", added, new_index) lines += render_changed("Removed", removed, old_index) + # Only link per-plane assets that this release actually built. + planes = sorted({e["control_plane"] for e in new_doc.get("entries", [])}) lines += [ "---", "", "### Full list", "", f"- Everything: {REPO}/releases/download/v{version}/nat-ips.json", - f"- US control plane: {REPO}/releases/download/v{version}/us.json", - f"- EU control plane: {REPO}/releases/download/v{version}/eu.json", + ] + lines += [ + f"- {plane.upper()} control plane: {REPO}/releases/download/v{version}/{plane}.json" + for plane in planes + ] + totals = f"**{new_doc['entry_count']} entries, {new_doc['ip_count']} unique IPs**" + if new_doc.get("provisioned_count"): + totals += f" ({new_doc['live_count']} live, {new_doc['provisioned_count']} provisioned)" + lines += [ "- Plain text for firewall scripts: swap `.json` for `.txt` in any URL above.", f"- Always-latest instead of pinned: {REPO}/releases/latest/download/nat-ips.json", "", - f"Totals in this release: **{new_doc['entry_count']} entries, {new_doc['ip_count']} unique IPs**.", + f"Totals in this release: {totals}.", "", ] diff --git a/scripts/validate.py b/scripts/validate.py index 1c4933d..32936b2 100644 --- a/scripts/validate.py +++ b/scripts/validate.py @@ -88,6 +88,12 @@ def validate_file(path: Path, validator: Draft202012Validator) -> list[dict]: if hostname and not HOSTNAME_RE.match(hostname): error(where, f"hostname {hostname!r} is not a valid lowercase FQDN") + # Catches a row pasted into the wrong cluster: the endpoint always leads with its + # own airport code, so a mismatch means the entry has been crossed with another. + airport = entry.get("airport_code") + if hostname and airport and not hostname.startswith(f"{airport}."): + error(where, f"hostname {hostname!r} does not start with airport code {airport!r}") + nets = [n for n in (check_cidr(where, ip) for ip in entry["nat_ips"]) if n] for a_index, a in enumerate(nets): for b in nets[a_index + 1 :]: From 8617ad4cd6e63584687878288afafc5a9528d9f5 Mon Sep 17 00:00:00 2001 From: Shubham Rai Date: Mon, 12 Oct 2026 18:31:00 +0530 Subject: [PATCH 2/7] fix: publish dist via release assets instead of committing to main The org ruleset on main requires pull requests, so the release workflow's commit-and-push step could never succeed. Rather than ask for a bot bypass, stop tracking dist/ at all: it is a build artifact, CI builds it and attaches it to the release, and gh creates the tag at the merge commit. The diff baseline for release notes now comes from the previous release's nat-ips.json asset rather than from git, so old releases must not be deleted. Consumer URLs are unaffected - they already pointed at release assets. --- .github/workflows/release.yml | 49 +-- .gitignore | 1 + README.md | 8 +- dist/eu.json | 613 ---------------------------------- dist/eu.txt | 88 ----- dist/nat-ips.json | 613 ---------------------------------- dist/nat-ips.txt | 88 ----- dist/us.json | 36 -- dist/us.txt | 6 - 9 files changed, 18 insertions(+), 1484 deletions(-) delete mode 100644 dist/eu.json delete mode 100644 dist/eu.txt delete mode 100644 dist/nat-ips.json delete mode 100644 dist/nat-ips.txt delete mode 100644 dist/us.json delete mode 100644 dist/us.txt diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c3a1f17..489a87e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,7 +4,6 @@ on: push: branches: [main] paths: - # dist/** is deliberately absent — this workflow commits there and must not retrigger itself. - "data/**" - "schema/**" - "scripts/**" @@ -60,11 +59,15 @@ jobs: echo "version=$VERSION" >> "$GITHUB_OUTPUT" echo "Releasing v$VERSION" - - name: Capture the previously released artifact + - name: Fetch the previously released artifact id: previous + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - PREV="$(git tag -l 'v*' --sort=-version:refname | head -n 1)" - if [ -n "$PREV" ] && git show "$PREV:dist/nat-ips.json" > "${RUNNER_TEMP}/old.json" 2>/dev/null; then + # dist/ is not tracked in git, so the diff baseline comes from the last release. + PREV="$(gh release list --limit 1 --json tagName --jq '.[0].tagName // empty')" + if [ -n "$PREV" ] && gh release download "$PREV" \ + --pattern nat-ips.json --output "${RUNNER_TEMP}/old.json" 2>/dev/null; then echo "tag=$PREV" >> "$GITHUB_OUTPUT" echo "Comparing against $PREV" else @@ -82,48 +85,22 @@ jobs: ARGS+=(--old "${RUNNER_TEMP}/old.json") fi uv run scripts/release_notes.py "${ARGS[@]}" - cat "${RUNNER_TEMP}/notes.md" - - - name: Update CHANGELOG - run: | - printf '%s\n\n' "$(cat "${RUNNER_TEMP}/notes.md")" > "${RUNNER_TEMP}/changelog.md" - if [ -f CHANGELOG.md ]; then - cat CHANGELOG.md >> "${RUNNER_TEMP}/changelog.md" - fi - mv "${RUNNER_TEMP}/changelog.md" CHANGELOG.md + cat "${RUNNER_TEMP}/notes.md" >> "$GITHUB_STEP_SUMMARY" - name: Stop here on a dry run if: inputs.dry_run - run: | - echo "::notice::Dry run — skipping commit, tag and release." - git --no-pager diff --stat + run: echo "::notice::Dry run — notes are in the job summary. Nothing tagged or published." - - name: Commit, tag and publish + - name: Tag and publish if: ${{ !inputs.dry_run }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} VERSION: ${{ steps.version.outputs.version }} run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - - git add dist/ CHANGELOG.md - if git diff --staged --quiet; then - echo "::notice::Nothing changed — no release published." - exit 0 - fi - - git commit -m "chore(release): v${VERSION}" - git tag -a "v${VERSION}" -m "NAT IPs v${VERSION}" - git push origin HEAD:main --follow-tags - + # gh creates the tag at this commit, so nothing is ever pushed to a protected branch. gh release create "v${VERSION}" \ + --target "${GITHUB_SHA}" \ --title "NAT IPs v${VERSION}" \ --notes-file "${RUNNER_TEMP}/notes.md" \ --latest \ - dist/nat-ips.json \ - dist/nat-ips.txt \ - dist/us.json \ - dist/us.txt \ - dist/eu.json \ - dist/eu.txt + dist/* diff --git a/.gitignore b/.gitignore index 1806e14..2ddfefc 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ RELEASE_NOTES.md __pycache__/ *.pyc .DS_Store +dist/ diff --git a/README.md b/README.md index 6fe7669..c760aaf 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ Every release note lists the exact IPs **added** and **removed** versus the prev ## For TrueFoundry engineers -Source of truth is `data//.json`. Everything in `dist/` is generated — never edit it by hand. +Source of truth is `data//.json`. `dist/` is a build artifact — it is **not tracked in git**, it is built in CI and attached to each release. Build it locally whenever you want to inspect it. ``` data/ @@ -137,7 +137,7 @@ To add or change IPs: 1. Edit the relevant file under `data/`. 2. Open a PR. CI validates the schema and comments the exact IP diff for review. -3. Merge to `main`. The release workflow builds `dist/`, tags `v`, and publishes a release with notes listing added and removed IPs. +3. Merge to `main`. The release workflow builds `dist/`, tags `v` at that commit, and publishes a release with notes listing added and removed IPs. Run the same checks locally: @@ -148,9 +148,9 @@ uv run scripts/build.py --version 0.0.0-local Validation rejects malformed CIDRs, host bits set on a prefix, private or reserved addresses, overlapping ranges within an entry, `live` entries missing a `hostname`, a `hostname` whose leading label disagrees with its `airport_code`, gateway entries missing `airport_code` or `status`, and a `control_plane` that disagrees with its directory. Duplicate IPs across entries are reported as warnings. -To preview a release without publishing: **Actions → Release → Run workflow → dry run**. +To preview a release without publishing: **Actions → Release → Run workflow → dry run**. The notes land in the job summary. -> The release workflow pushes a commit to `main`. If you enable branch protection, allow `github-actions[bot]` to bypass it, or the release step will fail. +> The release workflow never pushes to `main` — it tags the merge commit and uploads artifacts, so the branch ruleset requiring pull requests doesn't block it. Each release's diff baseline is the `nat-ips.json` asset from the previous release, so **don't delete old releases**. ### Provenance diff --git a/dist/eu.json b/dist/eu.json deleted file mode 100644 index e8e4056..0000000 --- a/dist/eu.json +++ /dev/null @@ -1,613 +0,0 @@ -{ - "version": "2026.10.12", - "generation_timestamp": "2026-10-12T12:52:47Z", - "entry_count": 42, - "ip_count": 84, - "live_count": 15, - "provisioned_count": 27, - "entries": [ - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "ap-southeast-6", - "continent": "oc", - "status": "provisioned", - "airport_code": "akl", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "3.102.184.172/32", - "3.103.30.44/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "eu-north-1", - "continent": "eu", - "status": "provisioned", - "airport_code": "arn", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "13.48.213.79/32", - "13.62.133.235/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "me-south-1", - "continent": "me", - "status": "provisioned", - "airport_code": "bah", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "157.175.127.180/32", - "157.241.96.125/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "ap-southeast-7", - "continent": "ap", - "status": "provisioned", - "airport_code": "bkk", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "43.209.246.149/32", - "43.210.99.107/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "ap-south-1", - "continent": "ap", - "status": "live", - "airport_code": "bom", - "gateway_host": "gateway.truefoundry.ai", - "host": "bom.gateway.truefoundry.ai", - "nat_ips": [ - "13.126.176.139/32", - "15.207.191.126/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "azure", - "region": "australiacentral", - "continent": "oc", - "status": "live", - "airport_code": "cbr", - "gateway_host": "gateway.truefoundry.ai", - "host": "cbr.gateway.truefoundry.ai", - "nat_ips": [ - "20.28.68.101/32", - "20.53.28.45/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "eu-west-3", - "continent": "eu", - "status": "provisioned", - "airport_code": "cdg", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "13.39.61.233/32", - "15.237.200.183/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "ap-southeast-3", - "continent": "ap", - "status": "provisioned", - "airport_code": "cgk", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "15.232.2.7/32", - "16.79.100.152/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "us-east-2", - "continent": "na", - "status": "provisioned", - "airport_code": "cle", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "3.130.37.65/32", - "18.119.53.236/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "af-south-1", - "continent": "af", - "status": "live", - "airport_code": "cpt", - "gateway_host": "gateway.truefoundry.ai", - "host": "cpt.gateway.truefoundry.ai", - "nat_ips": [ - "13.244.129.178/32", - "13.246.115.110/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "gcp", - "region": "us-south1", - "continent": "na", - "status": "live", - "airport_code": "dfw", - "gateway_host": "gateway.truefoundry.ai", - "host": "dfw.gateway.truefoundry.ai", - "nat_ips": [ - "34.174.18.136/32", - "34.174.240.150/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "gcp", - "region": "me-central1", - "continent": "me", - "status": "live", - "airport_code": "dia", - "gateway_host": "gateway.truefoundry.ai", - "host": "dia.gateway.truefoundry.ai", - "nat_ips": [ - "34.18.22.15/32", - "34.18.192.62/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "eu-west-1", - "continent": "eu", - "status": "provisioned", - "airport_code": "dub", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "54.72.115.226/32", - "54.246.5.151/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "me-central-1", - "continent": "me", - "status": "provisioned", - "airport_code": "dxb", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "3.29.12.147/32", - "40.172.236.59/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "eu-central-1", - "continent": "eu", - "status": "provisioned", - "airport_code": "fra", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "3.65.93.12/32", - "63.181.139.3/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "sa-east-1", - "continent": "sa", - "status": "provisioned", - "airport_code": "gru", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "56.126.18.155/32", - "56.126.81.140/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "gcp", - "region": "southamerica-east1", - "continent": "sa", - "status": "live", - "airport_code": "gru", - "gateway_host": "gateway.truefoundry.ai", - "host": "gru.gateway.truefoundry.ai", - "nat_ips": [ - "34.39.220.106/32", - "34.39.220.236/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "azure", - "region": "swedencentral", - "continent": "eu", - "status": "live", - "airport_code": "gvx", - "gateway_host": "gateway.truefoundry.ai", - "host": "gvx.gateway.truefoundry.ai", - "nat_ips": [ - "4.165.99.208/32", - "4.165.129.123/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "ap-east-1", - "continent": "ap", - "status": "provisioned", - "airport_code": "hkg", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "16.163.170.188/32", - "18.166.19.152/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "ap-northeast-1", - "continent": "ap", - "status": "provisioned", - "airport_code": "hnd", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "18.178.26.39/32", - "54.238.73.1/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "ap-south-2", - "continent": "ap", - "status": "provisioned", - "airport_code": "hyd", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "16.112.48.100/32", - "40.192.126.53/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "ap-northeast-2", - "continent": "ap", - "status": "provisioned", - "airport_code": "icn", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "43.200.33.159/32", - "43.200.144.76/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "ap-northeast-3", - "continent": "ap", - "status": "provisioned", - "airport_code": "kix", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "15.152.62.45/32", - "15.168.90.90/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "ap-southeast-5", - "continent": "ap", - "status": "provisioned", - "airport_code": "kul", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "56.68.6.121/32", - "56.68.75.208/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "eu-west-2", - "continent": "eu", - "status": "live", - "airport_code": "lhr", - "gateway_host": "gateway.truefoundry.ai", - "host": "lhr.gateway.truefoundry.ai", - "nat_ips": [ - "13.42.115.230/32", - "18.130.148.118/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "eu-south-2", - "continent": "eu", - "status": "provisioned", - "airport_code": "mad", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "15.216.64.85/32", - "51.48.180.27/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "gcp", - "region": "europe-southwest1", - "continent": "eu", - "status": "live", - "airport_code": "mad", - "gateway_host": "gateway.truefoundry.ai", - "host": "mad.gateway.truefoundry.ai", - "nat_ips": [ - "34.175.44.33/32", - "34.175.219.109/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "ap-southeast-4", - "continent": "oc", - "status": "live", - "airport_code": "mel", - "gateway_host": "gateway.truefoundry.ai", - "host": "mel.gateway.truefoundry.ai", - "nat_ips": [ - "16.51.12.109/32", - "16.51.101.207/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "mx-central-1", - "continent": "na", - "status": "provisioned", - "airport_code": "mex", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "78.13.15.252/32", - "78.13.183.82/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "eu-south-1", - "continent": "eu", - "status": "provisioned", - "airport_code": "mxp", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "18.102.33.104/32", - "51.118.102.128/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "us-east-1", - "continent": "na", - "status": "live", - "airport_code": "orf", - "gateway_host": "gateway.truefoundry.ai", - "host": "orf.gateway.truefoundry.ai", - "nat_ips": [ - "52.4.51.133/32", - "54.204.114.10/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "us-west-2", - "continent": "na", - "status": "provisioned", - "airport_code": "pdx", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "34.210.236.98/32", - "44.236.36.218/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "us-west-1", - "continent": "na", - "status": "provisioned", - "airport_code": "sfo", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "13.52.112.39/32", - "54.193.213.45/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "azure", - "region": "westus", - "continent": "na", - "status": "live", - "airport_code": "sfo", - "gateway_host": "gateway.truefoundry.ai", - "host": "sfo.gateway.truefoundry.ai", - "nat_ips": [ - "20.245.207.53/32", - "40.75.129.73/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "ap-southeast-1", - "continent": "ap", - "status": "live", - "airport_code": "sin", - "gateway_host": "gateway.truefoundry.ai", - "host": "sin.gateway.truefoundry.ai", - "nat_ips": [ - "13.251.96.74/32", - "18.143.12.175/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "ap-southeast-2", - "continent": "oc", - "status": "live", - "airport_code": "syd", - "gateway_host": "gateway.truefoundry.ai", - "host": "syd.gateway.truefoundry.ai", - "nat_ips": [ - "3.105.148.45/32", - "13.237.191.204/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "il-central-1", - "continent": "me", - "status": "provisioned", - "airport_code": "tlv", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "51.16.181.62/32", - "51.84.87.166/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "ap-east-2", - "continent": "ap", - "status": "provisioned", - "airport_code": "tpe", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "43.212.202.18/32", - "43.213.3.104/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "ca-west-1", - "continent": "na", - "status": "provisioned", - "airport_code": "yyc", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "40.176.147.21/32", - "40.177.86.240/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "ca-central-1", - "continent": "na", - "status": "provisioned", - "airport_code": "yyz", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "15.156.30.166/32", - "16.52.203.122/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "gcp", - "region": "northamerica-northeast2", - "continent": "na", - "status": "live", - "airport_code": "yyz", - "gateway_host": "gateway.truefoundry.ai", - "host": "yyz.gateway.truefoundry.ai", - "nat_ips": [ - "34.130.196.232/32", - "34.130.234.128/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "eu-central-2", - "continent": "eu", - "status": "provisioned", - "airport_code": "zrh", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "51.96.80.198/32", - "51.96.196.104/32" - ] - } - ] -} diff --git a/dist/eu.txt b/dist/eu.txt deleted file mode 100644 index bf44df2..0000000 --- a/dist/eu.txt +++ /dev/null @@ -1,88 +0,0 @@ -# TrueFoundry outbound NAT IPs -# version: 2026.10.12 -# generated: 2026-10-12T12:52:47Z -# https://github.com/truefoundry/nat-ips -3.29.12.147/32 -3.65.93.12/32 -3.102.184.172/32 -3.103.30.44/32 -3.105.148.45/32 -3.130.37.65/32 -4.165.99.208/32 -4.165.129.123/32 -13.39.61.233/32 -13.42.115.230/32 -13.48.213.79/32 -13.52.112.39/32 -13.62.133.235/32 -13.126.176.139/32 -13.237.191.204/32 -13.244.129.178/32 -13.246.115.110/32 -13.251.96.74/32 -15.152.62.45/32 -15.156.30.166/32 -15.168.90.90/32 -15.207.191.126/32 -15.216.64.85/32 -15.232.2.7/32 -15.237.200.183/32 -16.51.12.109/32 -16.51.101.207/32 -16.52.203.122/32 -16.79.100.152/32 -16.112.48.100/32 -16.163.170.188/32 -18.102.33.104/32 -18.119.53.236/32 -18.130.148.118/32 -18.143.12.175/32 -18.166.19.152/32 -18.178.26.39/32 -20.28.68.101/32 -20.53.28.45/32 -20.245.207.53/32 -34.18.22.15/32 -34.18.192.62/32 -34.39.220.106/32 -34.39.220.236/32 -34.130.196.232/32 -34.130.234.128/32 -34.174.18.136/32 -34.174.240.150/32 -34.175.44.33/32 -34.175.219.109/32 -34.210.236.98/32 -40.75.129.73/32 -40.172.236.59/32 -40.176.147.21/32 -40.177.86.240/32 -40.192.126.53/32 -43.200.33.159/32 -43.200.144.76/32 -43.209.246.149/32 -43.210.99.107/32 -43.212.202.18/32 -43.213.3.104/32 -44.236.36.218/32 -51.16.181.62/32 -51.48.180.27/32 -51.84.87.166/32 -51.96.80.198/32 -51.96.196.104/32 -51.118.102.128/32 -52.4.51.133/32 -54.72.115.226/32 -54.193.213.45/32 -54.204.114.10/32 -54.238.73.1/32 -54.246.5.151/32 -56.68.6.121/32 -56.68.75.208/32 -56.126.18.155/32 -56.126.81.140/32 -63.181.139.3/32 -78.13.15.252/32 -78.13.183.82/32 -157.175.127.180/32 -157.241.96.125/32 diff --git a/dist/nat-ips.json b/dist/nat-ips.json deleted file mode 100644 index e8e4056..0000000 --- a/dist/nat-ips.json +++ /dev/null @@ -1,613 +0,0 @@ -{ - "version": "2026.10.12", - "generation_timestamp": "2026-10-12T12:52:47Z", - "entry_count": 42, - "ip_count": 84, - "live_count": 15, - "provisioned_count": 27, - "entries": [ - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "ap-southeast-6", - "continent": "oc", - "status": "provisioned", - "airport_code": "akl", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "3.102.184.172/32", - "3.103.30.44/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "eu-north-1", - "continent": "eu", - "status": "provisioned", - "airport_code": "arn", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "13.48.213.79/32", - "13.62.133.235/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "me-south-1", - "continent": "me", - "status": "provisioned", - "airport_code": "bah", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "157.175.127.180/32", - "157.241.96.125/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "ap-southeast-7", - "continent": "ap", - "status": "provisioned", - "airport_code": "bkk", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "43.209.246.149/32", - "43.210.99.107/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "ap-south-1", - "continent": "ap", - "status": "live", - "airport_code": "bom", - "gateway_host": "gateway.truefoundry.ai", - "host": "bom.gateway.truefoundry.ai", - "nat_ips": [ - "13.126.176.139/32", - "15.207.191.126/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "azure", - "region": "australiacentral", - "continent": "oc", - "status": "live", - "airport_code": "cbr", - "gateway_host": "gateway.truefoundry.ai", - "host": "cbr.gateway.truefoundry.ai", - "nat_ips": [ - "20.28.68.101/32", - "20.53.28.45/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "eu-west-3", - "continent": "eu", - "status": "provisioned", - "airport_code": "cdg", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "13.39.61.233/32", - "15.237.200.183/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "ap-southeast-3", - "continent": "ap", - "status": "provisioned", - "airport_code": "cgk", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "15.232.2.7/32", - "16.79.100.152/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "us-east-2", - "continent": "na", - "status": "provisioned", - "airport_code": "cle", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "3.130.37.65/32", - "18.119.53.236/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "af-south-1", - "continent": "af", - "status": "live", - "airport_code": "cpt", - "gateway_host": "gateway.truefoundry.ai", - "host": "cpt.gateway.truefoundry.ai", - "nat_ips": [ - "13.244.129.178/32", - "13.246.115.110/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "gcp", - "region": "us-south1", - "continent": "na", - "status": "live", - "airport_code": "dfw", - "gateway_host": "gateway.truefoundry.ai", - "host": "dfw.gateway.truefoundry.ai", - "nat_ips": [ - "34.174.18.136/32", - "34.174.240.150/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "gcp", - "region": "me-central1", - "continent": "me", - "status": "live", - "airport_code": "dia", - "gateway_host": "gateway.truefoundry.ai", - "host": "dia.gateway.truefoundry.ai", - "nat_ips": [ - "34.18.22.15/32", - "34.18.192.62/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "eu-west-1", - "continent": "eu", - "status": "provisioned", - "airport_code": "dub", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "54.72.115.226/32", - "54.246.5.151/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "me-central-1", - "continent": "me", - "status": "provisioned", - "airport_code": "dxb", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "3.29.12.147/32", - "40.172.236.59/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "eu-central-1", - "continent": "eu", - "status": "provisioned", - "airport_code": "fra", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "3.65.93.12/32", - "63.181.139.3/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "sa-east-1", - "continent": "sa", - "status": "provisioned", - "airport_code": "gru", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "56.126.18.155/32", - "56.126.81.140/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "gcp", - "region": "southamerica-east1", - "continent": "sa", - "status": "live", - "airport_code": "gru", - "gateway_host": "gateway.truefoundry.ai", - "host": "gru.gateway.truefoundry.ai", - "nat_ips": [ - "34.39.220.106/32", - "34.39.220.236/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "azure", - "region": "swedencentral", - "continent": "eu", - "status": "live", - "airport_code": "gvx", - "gateway_host": "gateway.truefoundry.ai", - "host": "gvx.gateway.truefoundry.ai", - "nat_ips": [ - "4.165.99.208/32", - "4.165.129.123/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "ap-east-1", - "continent": "ap", - "status": "provisioned", - "airport_code": "hkg", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "16.163.170.188/32", - "18.166.19.152/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "ap-northeast-1", - "continent": "ap", - "status": "provisioned", - "airport_code": "hnd", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "18.178.26.39/32", - "54.238.73.1/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "ap-south-2", - "continent": "ap", - "status": "provisioned", - "airport_code": "hyd", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "16.112.48.100/32", - "40.192.126.53/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "ap-northeast-2", - "continent": "ap", - "status": "provisioned", - "airport_code": "icn", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "43.200.33.159/32", - "43.200.144.76/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "ap-northeast-3", - "continent": "ap", - "status": "provisioned", - "airport_code": "kix", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "15.152.62.45/32", - "15.168.90.90/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "ap-southeast-5", - "continent": "ap", - "status": "provisioned", - "airport_code": "kul", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "56.68.6.121/32", - "56.68.75.208/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "eu-west-2", - "continent": "eu", - "status": "live", - "airport_code": "lhr", - "gateway_host": "gateway.truefoundry.ai", - "host": "lhr.gateway.truefoundry.ai", - "nat_ips": [ - "13.42.115.230/32", - "18.130.148.118/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "eu-south-2", - "continent": "eu", - "status": "provisioned", - "airport_code": "mad", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "15.216.64.85/32", - "51.48.180.27/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "gcp", - "region": "europe-southwest1", - "continent": "eu", - "status": "live", - "airport_code": "mad", - "gateway_host": "gateway.truefoundry.ai", - "host": "mad.gateway.truefoundry.ai", - "nat_ips": [ - "34.175.44.33/32", - "34.175.219.109/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "ap-southeast-4", - "continent": "oc", - "status": "live", - "airport_code": "mel", - "gateway_host": "gateway.truefoundry.ai", - "host": "mel.gateway.truefoundry.ai", - "nat_ips": [ - "16.51.12.109/32", - "16.51.101.207/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "mx-central-1", - "continent": "na", - "status": "provisioned", - "airport_code": "mex", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "78.13.15.252/32", - "78.13.183.82/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "eu-south-1", - "continent": "eu", - "status": "provisioned", - "airport_code": "mxp", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "18.102.33.104/32", - "51.118.102.128/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "us-east-1", - "continent": "na", - "status": "live", - "airport_code": "orf", - "gateway_host": "gateway.truefoundry.ai", - "host": "orf.gateway.truefoundry.ai", - "nat_ips": [ - "52.4.51.133/32", - "54.204.114.10/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "us-west-2", - "continent": "na", - "status": "provisioned", - "airport_code": "pdx", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "34.210.236.98/32", - "44.236.36.218/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "us-west-1", - "continent": "na", - "status": "provisioned", - "airport_code": "sfo", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "13.52.112.39/32", - "54.193.213.45/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "azure", - "region": "westus", - "continent": "na", - "status": "live", - "airport_code": "sfo", - "gateway_host": "gateway.truefoundry.ai", - "host": "sfo.gateway.truefoundry.ai", - "nat_ips": [ - "20.245.207.53/32", - "40.75.129.73/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "ap-southeast-1", - "continent": "ap", - "status": "live", - "airport_code": "sin", - "gateway_host": "gateway.truefoundry.ai", - "host": "sin.gateway.truefoundry.ai", - "nat_ips": [ - "13.251.96.74/32", - "18.143.12.175/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "ap-southeast-2", - "continent": "oc", - "status": "live", - "airport_code": "syd", - "gateway_host": "gateway.truefoundry.ai", - "host": "syd.gateway.truefoundry.ai", - "nat_ips": [ - "3.105.148.45/32", - "13.237.191.204/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "il-central-1", - "continent": "me", - "status": "provisioned", - "airport_code": "tlv", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "51.16.181.62/32", - "51.84.87.166/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "ap-east-2", - "continent": "ap", - "status": "provisioned", - "airport_code": "tpe", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "43.212.202.18/32", - "43.213.3.104/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "ca-west-1", - "continent": "na", - "status": "provisioned", - "airport_code": "yyc", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "40.176.147.21/32", - "40.177.86.240/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "ca-central-1", - "continent": "na", - "status": "provisioned", - "airport_code": "yyz", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "15.156.30.166/32", - "16.52.203.122/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "gcp", - "region": "northamerica-northeast2", - "continent": "na", - "status": "live", - "airport_code": "yyz", - "gateway_host": "gateway.truefoundry.ai", - "host": "yyz.gateway.truefoundry.ai", - "nat_ips": [ - "34.130.196.232/32", - "34.130.234.128/32" - ] - }, - { - "control_plane": "eu", - "type": "gateway", - "cloud": "aws", - "region": "eu-central-2", - "continent": "eu", - "status": "provisioned", - "airport_code": "zrh", - "gateway_host": "gateway.truefoundry.ai", - "nat_ips": [ - "51.96.80.198/32", - "51.96.196.104/32" - ] - } - ] -} diff --git a/dist/nat-ips.txt b/dist/nat-ips.txt deleted file mode 100644 index bf44df2..0000000 --- a/dist/nat-ips.txt +++ /dev/null @@ -1,88 +0,0 @@ -# TrueFoundry outbound NAT IPs -# version: 2026.10.12 -# generated: 2026-10-12T12:52:47Z -# https://github.com/truefoundry/nat-ips -3.29.12.147/32 -3.65.93.12/32 -3.102.184.172/32 -3.103.30.44/32 -3.105.148.45/32 -3.130.37.65/32 -4.165.99.208/32 -4.165.129.123/32 -13.39.61.233/32 -13.42.115.230/32 -13.48.213.79/32 -13.52.112.39/32 -13.62.133.235/32 -13.126.176.139/32 -13.237.191.204/32 -13.244.129.178/32 -13.246.115.110/32 -13.251.96.74/32 -15.152.62.45/32 -15.156.30.166/32 -15.168.90.90/32 -15.207.191.126/32 -15.216.64.85/32 -15.232.2.7/32 -15.237.200.183/32 -16.51.12.109/32 -16.51.101.207/32 -16.52.203.122/32 -16.79.100.152/32 -16.112.48.100/32 -16.163.170.188/32 -18.102.33.104/32 -18.119.53.236/32 -18.130.148.118/32 -18.143.12.175/32 -18.166.19.152/32 -18.178.26.39/32 -20.28.68.101/32 -20.53.28.45/32 -20.245.207.53/32 -34.18.22.15/32 -34.18.192.62/32 -34.39.220.106/32 -34.39.220.236/32 -34.130.196.232/32 -34.130.234.128/32 -34.174.18.136/32 -34.174.240.150/32 -34.175.44.33/32 -34.175.219.109/32 -34.210.236.98/32 -40.75.129.73/32 -40.172.236.59/32 -40.176.147.21/32 -40.177.86.240/32 -40.192.126.53/32 -43.200.33.159/32 -43.200.144.76/32 -43.209.246.149/32 -43.210.99.107/32 -43.212.202.18/32 -43.213.3.104/32 -44.236.36.218/32 -51.16.181.62/32 -51.48.180.27/32 -51.84.87.166/32 -51.96.80.198/32 -51.96.196.104/32 -51.118.102.128/32 -52.4.51.133/32 -54.72.115.226/32 -54.193.213.45/32 -54.204.114.10/32 -54.238.73.1/32 -54.246.5.151/32 -56.68.6.121/32 -56.68.75.208/32 -56.126.18.155/32 -56.126.81.140/32 -63.181.139.3/32 -78.13.15.252/32 -78.13.183.82/32 -157.175.127.180/32 -157.241.96.125/32 diff --git a/dist/us.json b/dist/us.json deleted file mode 100644 index 2e125fb..0000000 --- a/dist/us.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "version": "0.0.0-unreleased", - "generation_timestamp": "2026-10-12T12:23:55Z", - "entry_count": 2, - "ip_count": 2, - "entries": [ - { - "control_plane": "us", - "type": "control_plane", - "host": "*.us.truefoundry.cloud", - "cloud": "aws", - "region": "us-east-1", - "continent": "na", - "notes": "PLACEHOLDER - not real data, replace before making this repo public", - "nat_ips": [ - "34.175.44.33/32", - "34.175.219.109/32" - ] - }, - { - "control_plane": "us", - "type": "gateway", - "host": "mad.us.truefoundry.cloud", - "cloud": "gcp", - "region": "europe-southwest1", - "continent": "eu", - "gateway_host": "us.truefoundry.cloud", - "airport_code": "mad", - "notes": "PLACEHOLDER - not real data, replace before making this repo public", - "nat_ips": [ - "34.175.44.33/32", - "34.175.219.109/32" - ] - } - ] -} diff --git a/dist/us.txt b/dist/us.txt deleted file mode 100644 index aeea4a1..0000000 --- a/dist/us.txt +++ /dev/null @@ -1,6 +0,0 @@ -# TrueFoundry outbound NAT IPs -# version: 0.0.0-unreleased -# generated: 2026-10-12T12:23:55Z -# https://github.com/truefoundry/nat-ips -34.175.44.33/32 -34.175.219.109/32 From 8a9e73a0f24cfc6175db5e0f1aebf0baf8329210 Mon Sep 17 00:00:00 2001 From: Shubham Rai Date: Mon, 12 Oct 2026 18:32:52 +0530 Subject: [PATCH 3/7] fix: make the PR diff job survive a schema change The diff job builds the base branch's data/ with the PR's build.py, so this PR's new required status field made the baseline build crash with KeyError. Read gateway-only keys defensively, and mark the job continue-on-error: it exists to render a reviewer-facing diff, and validate is the real gate. --- .github/workflows/validate.yml | 3 +++ scripts/build.py | 9 ++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 94a68d9..66f3536 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -36,8 +36,11 @@ jobs: diff: # Show reviewers exactly which IPs a PR adds or removes, so nobody has to read raw JSON. + # Informational only: `validate` is the gate, so a baseline that cannot be rebuilt + # (an old branch, a schema change) must never block the PR. if: github.event_name == 'pull_request' runs-on: ubuntu-latest + continue-on-error: true permissions: contents: read pull-requests: write diff --git a/scripts/build.py b/scripts/build.py index 15652dd..3582992 100644 --- a/scripts/build.py +++ b/scripts/build.py @@ -50,9 +50,12 @@ def load_entries() -> list[dict]: } if source["type"] == "gateway": - entry["status"] = source["status"] - entry["airport_code"] = source["airport_code"] - entry["gateway_host"] = doc["host"] + # Read defensively: the PR diff job builds the base branch's data with this + # script, so a schema change must not make the baseline unbuildable. + for key in ("status", "airport_code"): + if key in source: + entry[key] = source[key] + entry["gateway_host"] = doc.get("host") # A provisioned cluster has no published endpoint yet, so it gets no host. # Inventing one would tell customers to resolve a name that does not exist. if "hostname" in source: From 1aa68d335fd0d756586df3d11f72f17701a56f8c Mon Sep 17 00:00:00 2001 From: Shubham Rai Date: Mon, 12 Oct 2026 18:53:56 +0530 Subject: [PATCH 4/7] refactor: drop status field, remove CONTRIBUTING, move scripts to .github Per review: - status is gone. A cluster without a published endpoint simply has no hostname, which carries the same information without a second field to keep in sync. live_count/provisioned_count drop out of the envelope too. - CONTRIBUTING.md removed; the README already covers the edit flow and the wiki transcription hazard. - scripts/ moved to .github/scripts/, matching truefoundry/models. The move broke ROOT, which was parent.parent and is now parents[2]. validate.py failed loudly on the missing schema, but build.py just produced an empty build - in CI that would have published a release saying there is nothing to allowlist. build.py now refuses to run when data/ yields no files. --- {scripts => .github/scripts}/build.py | 21 ++++---- {scripts => .github/scripts}/release_notes.py | 13 ++--- {scripts => .github/scripts}/validate.py | 2 +- .github/workflows/release.yml | 8 +-- .github/workflows/validate.yml | 14 +++--- CONTRIBUTING.md | 49 ------------------- README.md | 27 +++------- data/eu/gateway.json | 42 ---------------- schema/nat-ips.schema.json | 26 +--------- 9 files changed, 35 insertions(+), 167 deletions(-) rename {scripts => .github/scripts}/build.py (85%) rename {scripts => .github/scripts}/release_notes.py (91%) rename {scripts => .github/scripts}/validate.py (98%) delete mode 100644 CONTRIBUTING.md diff --git a/scripts/build.py b/.github/scripts/build.py similarity index 85% rename from scripts/build.py rename to .github/scripts/build.py index 3582992..627faa3 100644 --- a/scripts/build.py +++ b/.github/scripts/build.py @@ -16,7 +16,7 @@ from datetime import datetime, timezone from pathlib import Path -ROOT = Path(__file__).resolve().parent.parent +ROOT = Path(__file__).resolve().parents[2] # .github/scripts/build.py -> repo root DATA_DIR = ROOT / "data" DIST_DIR = ROOT / "dist" @@ -36,7 +36,13 @@ def sort_key(entry: dict) -> tuple: def load_entries() -> list[dict]: entries: list[dict] = [] - for path in sorted(DATA_DIR.rglob("*.json")): + paths = sorted(DATA_DIR.rglob("*.json")) + if not paths: + # Never fall through to an empty build: that would publish a release telling + # customers there is nothing to allowlist. + raise SystemExit(f"no source files found under {DATA_DIR} — refusing to build") + + for path in paths: doc = json.loads(path.read_text()) control_plane = doc["control_plane"] @@ -52,12 +58,11 @@ def load_entries() -> list[dict]: if source["type"] == "gateway": # Read defensively: the PR diff job builds the base branch's data with this # script, so a schema change must not make the baseline unbuildable. - for key in ("status", "airport_code"): - if key in source: - entry[key] = source[key] + if "airport_code" in source: + entry["airport_code"] = source["airport_code"] entry["gateway_host"] = doc.get("host") - # A provisioned cluster has no published endpoint yet, so it gets no host. - # Inventing one would tell customers to resolve a name that does not exist. + # A cluster with no published endpoint gets no host. Inventing one would + # tell customers to resolve a name that does not exist. if "hostname" in source: entry["host"] = source["hostname"] else: @@ -111,8 +116,6 @@ def envelope(subset: list[dict]) -> dict: "generation_timestamp": timestamp, "entry_count": len(subset), "ip_count": len(flat_ip_list(subset)), - "live_count": sum(1 for e in subset if e.get("status") != "provisioned"), - "provisioned_count": sum(1 for e in subset if e.get("status") == "provisioned"), "entries": subset, } diff --git a/scripts/release_notes.py b/.github/scripts/release_notes.py similarity index 91% rename from scripts/release_notes.py rename to .github/scripts/release_notes.py index c6dd035..7c8c173 100644 --- a/scripts/release_notes.py +++ b/.github/scripts/release_notes.py @@ -40,13 +40,9 @@ def describe(entry: dict) -> str: def host_label(entry: dict) -> str: - """A provisioned cluster has no endpoint, so say that rather than print empty backticks.""" + """Not every cluster has a published endpoint; say so rather than print empty backticks.""" host = entry.get("host") - if host: - return f"`{host}`" - if entry.get("status") == "provisioned": - return "_provisioned, no endpoint yet_" - return "_no endpoint_" + return f"`{host}`" if host else "_no published endpoint_" def ip_sort(cidr: str) -> ipaddress.IPv4Network: @@ -141,14 +137,11 @@ def main() -> int: f"- {plane.upper()} control plane: {REPO}/releases/download/v{version}/{plane}.json" for plane in planes ] - totals = f"**{new_doc['entry_count']} entries, {new_doc['ip_count']} unique IPs**" - if new_doc.get("provisioned_count"): - totals += f" ({new_doc['live_count']} live, {new_doc['provisioned_count']} provisioned)" lines += [ "- Plain text for firewall scripts: swap `.json` for `.txt` in any URL above.", f"- Always-latest instead of pinned: {REPO}/releases/latest/download/nat-ips.json", "", - f"Totals in this release: {totals}.", + f"Totals in this release: **{new_doc['entry_count']} entries, {new_doc['ip_count']} unique IPs**.", "", ] diff --git a/scripts/validate.py b/.github/scripts/validate.py similarity index 98% rename from scripts/validate.py rename to .github/scripts/validate.py index 32936b2..5fba860 100644 --- a/scripts/validate.py +++ b/.github/scripts/validate.py @@ -19,7 +19,7 @@ from jsonschema import Draft202012Validator -ROOT = Path(__file__).resolve().parent.parent +ROOT = Path(__file__).resolve().parents[2] # .github/scripts/validate.py -> repo root DATA_DIR = ROOT / "data" SCHEMA_PATH = ROOT / "schema" / "nat-ips.schema.json" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 489a87e..b2e43c4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,7 +6,7 @@ on: paths: - "data/**" - "schema/**" - - "scripts/**" + - ".github/scripts/**" workflow_dispatch: inputs: dry_run: @@ -34,7 +34,7 @@ jobs: enable-cache: true - name: Validate source files - run: uv run scripts/validate.py + run: uv run .github/scripts/validate.py - name: Refuse to publish placeholder data run: | @@ -76,7 +76,7 @@ jobs: fi - name: Build artifacts - run: uv run scripts/build.py --version "${{ steps.version.outputs.version }}" + run: uv run .github/scripts/build.py --version "${{ steps.version.outputs.version }}" - name: Write release notes run: | @@ -84,7 +84,7 @@ jobs: if [ -n "${{ steps.previous.outputs.tag }}" ]; then ARGS+=(--old "${RUNNER_TEMP}/old.json") fi - uv run scripts/release_notes.py "${ARGS[@]}" + uv run .github/scripts/release_notes.py "${ARGS[@]}" cat "${RUNNER_TEMP}/notes.md" >> "$GITHUB_STEP_SUMMARY" - name: Stop here on a dry run diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 66f3536..1b820c2 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -5,14 +5,14 @@ on: paths: - "data/**" - "schema/**" - - "scripts/**" + - ".github/scripts/**" - ".github/workflows/validate.yml" push: branches: [main] paths: - "data/**" - "schema/**" - - "scripts/**" + - ".github/scripts/**" workflow_dispatch: permissions: @@ -29,10 +29,10 @@ jobs: enable-cache: true - name: Validate source files - run: uv run scripts/validate.py + run: uv run .github/scripts/validate.py - name: Build artifacts - run: uv run scripts/build.py --version 0.0.0-ci + run: uv run .github/scripts/build.py --version 0.0.0-ci diff: # Show reviewers exactly which IPs a PR adds or removes, so nobody has to read raw JSON. @@ -55,19 +55,19 @@ jobs: - name: Build this branch run: | - uv run scripts/build.py --version "pr-${{ github.event.pull_request.number }}" + uv run .github/scripts/build.py --version "pr-${{ github.event.pull_request.number }}" cp dist/nat-ips.json "${RUNNER_TEMP}/new.json" - name: Build the base branch run: | git checkout "origin/${{ github.base_ref }}" -- data/ - uv run scripts/build.py --version "${{ github.base_ref }}" + uv run .github/scripts/build.py --version "${{ github.base_ref }}" cp dist/nat-ips.json "${RUNNER_TEMP}/old.json" git checkout HEAD -- data/ - name: Render diff run: | - uv run scripts/release_notes.py \ + uv run .github/scripts/release_notes.py \ --new "${RUNNER_TEMP}/new.json" \ --old "${RUNNER_TEMP}/old.json" \ --out "${RUNNER_TEMP}/diff.md" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 7b42b96..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,49 +0,0 @@ -# Contributing - -This repo publishes TrueFoundry's outbound NAT IPs. Changes to `data/` are made by TrueFoundry engineers; anyone can open an issue. - -## Reporting a problem - -[Open an issue](https://github.com/truefoundry/nat-ips/issues) if an IP looks wrong, a region is missing, or traffic arrives from an address that isn't listed. Include the destination you saw it on and a timestamp. - -Anything security-sensitive goes to security@truefoundry.com, not a public issue. - -## Changing the data - -1. Edit the right file under `data//`. Nothing in `dist/` is hand-edited. -2. Write IPs as CIDR — a single address is `/32`. -3. Run the checks locally: - ```bash - uv run scripts/validate.py - uv run scripts/build.py --version 0.0.0-local - ``` -4. Open a PR. CI validates and comments the exact IP diff. -5. Merge to `main` — the release workflow tags and publishes automatically. - -### Adding a regional gateway PoP - -Gateway entries need an `airport_code` (lowercase IATA) and a `status`. A `live` entry also needs a `hostname`, whose first label must match the airport code: - -```json -{ - "type": "gateway", - "cloud": "gcp", - "region": "asia-south1", - "continent": "ap", - "airport_code": "bom", - "status": "live", - "cluster": "tfy-gcp-bom-gtw", - "hostname": "bom.gateway.truefoundry.ai", - "nat_ips": ["34.100.0.1/32"] -} -``` - -Use `"status": "provisioned"` and omit `hostname` while the NAT addresses are allocated but the endpoint isn't published. Flip to `live` and add the hostname when it goes into service — that flip is itself a release, which is what tells customers the region is active. - -### Transcribing from the wiki - -The *LLMGateway Clusters* page renders paired NAT addresses as one unseparated string (`18.119.53.2363.130.37.65`). Several of those split more than one way and **all candidates parse as valid IPv4**, so do not eyeball them. Resolve each against the provider's published ranges — AWS `ip-ranges.json`, GCP `cloud.json`, Azure service tags — and accept a split only when both addresses fall inside the region the row claims. Two entries currently in `data/` do not split the way they look; see the Provenance section in the README. - -### Timing - -Publish **before** the IPs go live. Customers need lead time to get a firewall change through their own approval process. Retiring an IP is lower risk, but still announce it ahead of the cutover. diff --git a/README.md b/README.md index c760aaf..0c71d6f 100644 --- a/README.md +++ b/README.md @@ -24,9 +24,9 @@ Pin to an exact version by replacing `latest/download` with `download/v2026.10.1 # Every IP, ready to paste into a firewall rule curl -sL https://github.com/truefoundry/nat-ips/releases/latest/download/nat-ips.txt | grep -v '^#' -# Only regions currently serving traffic +# Just the EU-region PoPs curl -sL https://github.com/truefoundry/nat-ips/releases/latest/download/nat-ips.json \ - | jq -r '.entries[] | select(.status == "live") | .nat_ips[]' + | jq -r '.entries[] | select(.continent == "eu") | .nat_ips[]' ``` ## Coverage @@ -39,17 +39,6 @@ curl -sL https://github.com/truefoundry/nat-ips/releases/latest/download/nat-ips If you call models through the AI Gateway, what's published today is what you need. Control-plane egress IPs are coming; watch releases to be notified. -## live vs provisioned - -Every gateway entry carries a `status`: - -- **`live`** — the regional endpoint is serving traffic today. Has a `hostname` such as `mad.gateway.truefoundry.ai`. -- **`provisioned`** — the NAT addresses are allocated and reserved for a region that isn't published yet. No `hostname`. - -**Allowlist both.** A provisioned IP sends you no traffic until that region activates, so permitting it early costs nothing — and it means a new region doesn't turn into an urgent firewall change on your side. - -Currently 15 live, 27 provisioned. - ## Data format ```json @@ -58,8 +47,6 @@ Currently 15 live, 27 provisioned. "generation_timestamp": "2026-10-12T12:52:47Z", "entry_count": 42, "ip_count": 84, - "live_count": 15, - "provisioned_count": 27, "entries": [ { "control_plane": "eu", @@ -67,7 +54,6 @@ Currently 15 live, 27 provisioned. "cloud": "gcp", "region": "europe-southwest1", "continent": "eu", - "status": "live", "airport_code": "mad", "gateway_host": "gateway.truefoundry.ai", "host": "mad.gateway.truefoundry.ai", @@ -83,12 +69,11 @@ Currently 15 live, 27 provisioned. | `generation_timestamp` | UTC build time, ISO 8601 | | `control_plane` | `us` or `eu` — which control plane manages this cluster | | `type` | `control_plane` or `gateway` | -| `status` | Gateway entries only — `live` or `provisioned` | | `cloud` / `region` | Where the egress NAT lives | | `continent` | `na`, `sa`, `eu`, `ap`, `me`, `af`, `oc` | | `airport_code` | Gateway entries only — IATA code for the PoP | | `gateway_host` | Gateway entries only — the global entry point | -| `host` | The endpoint this entry serves. Absent on `provisioned` entries, which have none yet | +| `host` | The endpoint this entry serves. Absent when the cluster's NAT addresses are allocated but its endpoint isn't published yet — **allowlist those IPs anyway**, so a region going live isn't an urgent firewall change on your side | | `nat_ips` | IPv4 CIDRs. Single addresses are written `/32` | > `airport_code` is **not unique**. Two clusters in the same metro on different clouds share a code — `sfo` is both AWS `us-west-1` and Azure `westus`. Key on `cluster`/`region`, not the airport code. @@ -142,11 +127,11 @@ To add or change IPs: Run the same checks locally: ```bash -uv run scripts/validate.py -uv run scripts/build.py --version 0.0.0-local +uv run .github/scripts/validate.py +uv run .github/scripts/build.py --version 0.0.0-local ``` -Validation rejects malformed CIDRs, host bits set on a prefix, private or reserved addresses, overlapping ranges within an entry, `live` entries missing a `hostname`, a `hostname` whose leading label disagrees with its `airport_code`, gateway entries missing `airport_code` or `status`, and a `control_plane` that disagrees with its directory. Duplicate IPs across entries are reported as warnings. +Validation rejects malformed CIDRs, host bits set on a prefix, private or reserved addresses, overlapping ranges within an entry, a `hostname` whose leading label disagrees with its `airport_code`, gateway entries missing an `airport_code`, and a `control_plane` that disagrees with its directory. Duplicate IPs across entries are reported as warnings. To preview a release without publishing: **Actions → Release → Run workflow → dry run**. The notes land in the job summary. diff --git a/data/eu/gateway.json b/data/eu/gateway.json index d7bc1f7..ad4357d 100644 --- a/data/eu/gateway.json +++ b/data/eu/gateway.json @@ -10,7 +10,6 @@ "region": "us-east-1", "continent": "na", "airport_code": "orf", - "status": "live", "cluster": "tfy-aws-orf-gtw", "hostname": "orf.gateway.truefoundry.ai", "nat_ips": [ @@ -24,7 +23,6 @@ "region": "us-east-2", "continent": "na", "airport_code": "cle", - "status": "provisioned", "cluster": "tfy-aws-cle-gte", "nat_ips": [ "18.119.53.236/32", @@ -37,7 +35,6 @@ "region": "us-west-1", "continent": "na", "airport_code": "sfo", - "status": "provisioned", "cluster": "tfy-aws-sfo-gtw", "nat_ips": [ "13.52.112.39/32", @@ -50,7 +47,6 @@ "region": "us-west-2", "continent": "na", "airport_code": "pdx", - "status": "provisioned", "cluster": "tfy-aws-pdx-gtw", "nat_ips": [ "44.236.36.218/32", @@ -63,7 +59,6 @@ "region": "mx-central-1", "continent": "na", "airport_code": "mex", - "status": "provisioned", "cluster": "tfy-aws-mex-gtw", "nat_ips": [ "78.13.15.252/32", @@ -76,7 +71,6 @@ "region": "southamerica-east1", "continent": "sa", "airport_code": "gru", - "status": "live", "cluster": "tfy-gcp-gru-gtw", "hostname": "gru.gateway.truefoundry.ai", "nat_ips": [ @@ -90,7 +84,6 @@ "region": "sa-east-1", "continent": "sa", "airport_code": "gru", - "status": "provisioned", "cluster": "tfy-aws-gru-gtw", "nat_ips": [ "56.126.81.140/32", @@ -103,7 +96,6 @@ "region": "northamerica-northeast2", "continent": "na", "airport_code": "yyz", - "status": "live", "cluster": "tfy-gcp-yyz-gtw", "hostname": "yyz.gateway.truefoundry.ai", "nat_ips": [ @@ -117,7 +109,6 @@ "region": "ca-central-1", "continent": "na", "airport_code": "yyz", - "status": "provisioned", "cluster": "tfy-aws-yyz-gtw", "nat_ips": [ "16.52.203.122/32", @@ -130,7 +121,6 @@ "region": "ca-west-1", "continent": "na", "airport_code": "yyc", - "status": "provisioned", "cluster": "tfy-aws-yyc-gtw", "nat_ips": [ "40.177.86.240/32", @@ -143,7 +133,6 @@ "region": "westus", "continent": "na", "airport_code": "sfo", - "status": "live", "cluster": "tfy-az-sfo-gtw", "hostname": "sfo.gateway.truefoundry.ai", "nat_ips": [ @@ -157,7 +146,6 @@ "region": "us-south1", "continent": "na", "airport_code": "dfw", - "status": "live", "cluster": "tfy-gcp-dfw-gtw", "hostname": "dfw.gateway.truefoundry.ai", "nat_ips": [ @@ -171,7 +159,6 @@ "region": "eu-west-2", "continent": "eu", "airport_code": "lhr", - "status": "live", "cluster": "tfy-aws-lhr-gtw", "hostname": "lhr.gateway.truefoundry.ai", "nat_ips": [ @@ -185,7 +172,6 @@ "region": "eu-west-3", "continent": "eu", "airport_code": "cdg", - "status": "provisioned", "cluster": "tfy-aws-cdg-gtw", "nat_ips": [ "13.39.61.233/32", @@ -198,7 +184,6 @@ "region": "eu-west-1", "continent": "eu", "airport_code": "dub", - "status": "provisioned", "cluster": "tfy-aws-dub-gtw", "nat_ips": [ "54.72.115.226/32", @@ -211,7 +196,6 @@ "region": "europe-southwest1", "continent": "eu", "airport_code": "mad", - "status": "live", "cluster": "tfy-gcp-mad-gtw", "hostname": "mad.gateway.truefoundry.ai", "nat_ips": [ @@ -225,7 +209,6 @@ "region": "eu-south-2", "continent": "eu", "airport_code": "mad", - "status": "provisioned", "cluster": "tfy-aws-mad-gtw", "nat_ips": [ "51.48.180.27/32", @@ -238,7 +221,6 @@ "region": "eu-south-1", "continent": "eu", "airport_code": "mxp", - "status": "provisioned", "cluster": "tfy-aws-mxp-gtw", "nat_ips": [ "51.118.102.128/32", @@ -251,7 +233,6 @@ "region": "eu-central-1", "continent": "eu", "airport_code": "fra", - "status": "provisioned", "cluster": "tfy-aws-fra-gtw", "nat_ips": [ "3.65.93.12/32", @@ -264,7 +245,6 @@ "region": "eu-central-2", "continent": "eu", "airport_code": "zrh", - "status": "provisioned", "cluster": "tfy-aws-zrh-gtw", "nat_ips": [ "51.96.196.104/32", @@ -277,7 +257,6 @@ "region": "swedencentral", "continent": "eu", "airport_code": "gvx", - "status": "live", "cluster": "tfy-az-gvx-gtw", "hostname": "gvx.gateway.truefoundry.ai", "nat_ips": [ @@ -291,7 +270,6 @@ "region": "eu-north-1", "continent": "eu", "airport_code": "arn", - "status": "provisioned", "cluster": "tfy-aws-arn-gtw", "nat_ips": [ "13.48.213.79/32", @@ -304,7 +282,6 @@ "region": "af-south-1", "continent": "af", "airport_code": "cpt", - "status": "live", "cluster": "tfy-aws-cpt-gtw", "hostname": "cpt.gateway.truefoundry.ai", "nat_ips": [ @@ -318,7 +295,6 @@ "region": "me-central1", "continent": "me", "airport_code": "dia", - "status": "live", "cluster": "tfy-gcp-dia-gtw", "hostname": "dia.gateway.truefoundry.ai", "nat_ips": [ @@ -332,7 +308,6 @@ "region": "me-south-1", "continent": "me", "airport_code": "bah", - "status": "provisioned", "cluster": "tfy-aws-bah-gtw", "nat_ips": [ "157.175.127.180/32", @@ -345,7 +320,6 @@ "region": "me-central-1", "continent": "me", "airport_code": "dxb", - "status": "provisioned", "cluster": "tfy-aws-dxb-gtw", "nat_ips": [ "40.172.236.59/32", @@ -358,7 +332,6 @@ "region": "il-central-1", "continent": "me", "airport_code": "tlv", - "status": "provisioned", "cluster": "tfy-aws-tlv-gtw", "nat_ips": [ "51.84.87.166/32", @@ -371,7 +344,6 @@ "region": "ap-south-1", "continent": "ap", "airport_code": "bom", - "status": "live", "cluster": "tfy-aws-bom-gtw", "hostname": "bom.gateway.truefoundry.ai", "nat_ips": [ @@ -385,7 +357,6 @@ "region": "ap-south-2", "continent": "ap", "airport_code": "hyd", - "status": "provisioned", "cluster": "tfy-aws-hyd-gtw", "nat_ips": [ "16.112.48.100/32", @@ -398,7 +369,6 @@ "region": "ap-east-1", "continent": "ap", "airport_code": "hkg", - "status": "provisioned", "cluster": "tfy-aws-hkg-gtw", "nat_ips": [ "16.163.170.188/32", @@ -411,7 +381,6 @@ "region": "ap-east-2", "continent": "ap", "airport_code": "tpe", - "status": "provisioned", "cluster": "tfy-aws-tpe-gtw", "nat_ips": [ "43.213.3.104/32", @@ -424,7 +393,6 @@ "region": "ap-southeast-7", "continent": "ap", "airport_code": "bkk", - "status": "provisioned", "cluster": "tfy-aws-bkk-gtw", "nat_ips": [ "43.210.99.107/32", @@ -437,7 +405,6 @@ "region": "ap-southeast-3", "continent": "ap", "airport_code": "cgk", - "status": "provisioned", "cluster": "tfy-aws-cgk-gtw", "nat_ips": [ "16.79.100.152/32", @@ -450,7 +417,6 @@ "region": "ap-southeast-1", "continent": "ap", "airport_code": "sin", - "status": "live", "cluster": "tfy-aws-sin-gtw", "hostname": "sin.gateway.truefoundry.ai", "nat_ips": [ @@ -464,7 +430,6 @@ "region": "ap-southeast-4", "continent": "oc", "airport_code": "mel", - "status": "live", "cluster": "tfy-aws-mel-gtw", "hostname": "mel.gateway.truefoundry.ai", "nat_ips": [ @@ -478,7 +443,6 @@ "region": "ap-southeast-6", "continent": "oc", "airport_code": "akl", - "status": "provisioned", "cluster": "tfy-aws-akl-gtw", "nat_ips": [ "3.103.30.44/32", @@ -491,7 +455,6 @@ "region": "ap-southeast-5", "continent": "ap", "airport_code": "kul", - "status": "provisioned", "cluster": "tfy-aws-kul-gtw", "nat_ips": [ "56.68.75.208/32", @@ -504,7 +467,6 @@ "region": "ap-southeast-2", "continent": "oc", "airport_code": "syd", - "status": "live", "cluster": "tfy-aws-syd-gtw", "hostname": "syd.gateway.truefoundry.ai", "nat_ips": [ @@ -518,7 +480,6 @@ "region": "ap-northeast-1", "continent": "ap", "airport_code": "hnd", - "status": "provisioned", "cluster": "tfy-aws-hnd-gtw", "nat_ips": [ "54.238.73.1/32", @@ -531,7 +492,6 @@ "region": "ap-northeast-2", "continent": "ap", "airport_code": "icn", - "status": "provisioned", "cluster": "tfy-aws-icn-gtw", "nat_ips": [ "43.200.144.76/32", @@ -544,7 +504,6 @@ "region": "ap-northeast-3", "continent": "ap", "airport_code": "kix", - "status": "provisioned", "cluster": "tfy-aws-kix-gtw", "nat_ips": [ "15.152.62.45/32", @@ -557,7 +516,6 @@ "region": "australiacentral", "continent": "oc", "airport_code": "cbr", - "status": "live", "cluster": "tfy-az-cbr-gtw", "hostname": "cbr.gateway.truefoundry.ai", "nat_ips": [ diff --git a/schema/nat-ips.schema.json b/schema/nat-ips.schema.json index 51420cb..20c3522 100644 --- a/schema/nat-ips.schema.json +++ b/schema/nat-ips.schema.json @@ -58,14 +58,10 @@ "pattern": "^[a-z]{3}$", "description": "IATA code for the PoP. Required for gateway entries. Not unique: two clusters in the same metro on different clouds share a code." }, - "status": { - "enum": ["live", "provisioned"], - "description": "Required for gateway entries. 'live' means the regional endpoint is serving; 'provisioned' means the NAT addresses are allocated and reserved but the endpoint is not published yet. Allowlist both — a provisioned IP costs nothing and avoids a rush when the region is activated." - }, "hostname": { "type": "string", "format": "hostname", - "description": "Regional gateway endpoint customers resolve, e.g. mad.gateway.truefoundry.ai. Required when status is 'live'." + "description": "Regional gateway endpoint customers resolve, e.g. mad.gateway.truefoundry.ai. Absent on clusters whose NAT addresses are allocated but whose endpoint is not published yet." }, "cluster": { "type": "string", @@ -92,25 +88,7 @@ "required": ["type"] }, "then": { - "required": ["airport_code", "status"] - } - }, - { - "if": { - "properties": { "status": { "const": "live" } }, - "required": ["status"] - }, - "then": { - "required": ["hostname"] - } - }, - { - "if": { - "properties": { "type": { "const": "control_plane" } }, - "required": ["type"] - }, - "then": { - "not": { "required": ["status"] } + "required": ["airport_code"] } } ] From ff302c714a57c6c64bd64e13805505a33393a6af Mon Sep 17 00:00:00 2001 From: Vedant Pareek Date: Mon, 12 Oct 2026 19:01:29 +0530 Subject: [PATCH 5/7] Fixing the cle gateway name --- data/eu/gateway.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/eu/gateway.json b/data/eu/gateway.json index ad4357d..9507a50 100644 --- a/data/eu/gateway.json +++ b/data/eu/gateway.json @@ -23,7 +23,7 @@ "region": "us-east-2", "continent": "na", "airport_code": "cle", - "cluster": "tfy-aws-cle-gte", + "cluster": "tfy-aws-cle-gtw", "nat_ips": [ "18.119.53.236/32", "3.130.37.65/32" From c6102a97099b922ad7941238236aa45545ca4ff5 Mon Sep 17 00:00:00 2001 From: Vedant Pareek Date: Mon, 12 Oct 2026 21:41:33 +0530 Subject: [PATCH 6/7] Added the new IPs --- .github/schema/ip-ranges.schema.json | 61 ++++ .github/scripts/build.py | 115 ++---- .github/scripts/release_notes.py | 216 ++++++----- .github/scripts/validate.py | 206 ++++++++--- .github/workflows/release.yml | 44 ++- .github/workflows/validate.yml | 28 +- README.md | 157 +++----- data/eu/gateway.json | 527 --------------------------- eu/ip-ranges.json | 227 ++++++++++++ schema/nat-ips.schema.json | 97 ----- us/ip-ranges.json | 4 + 11 files changed, 682 insertions(+), 1000 deletions(-) create mode 100644 .github/schema/ip-ranges.schema.json delete mode 100644 data/eu/gateway.json create mode 100644 eu/ip-ranges.json delete mode 100644 schema/nat-ips.schema.json create mode 100644 us/ip-ranges.json diff --git a/.github/schema/ip-ranges.schema.json b/.github/schema/ip-ranges.schema.json new file mode 100644 index 0000000..833fe55 --- /dev/null +++ b/.github/schema/ip-ranges.schema.json @@ -0,0 +1,61 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/truefoundry/nat-ips/main/.github/schema/ip-ranges.schema.json", + "title": "TrueFoundry IP ranges", + "description": "Outbound NAT addresses for one control plane. The directory (eu or us) is the control plane.", + "type": "object", + "required": ["endpoints"], + "additionalProperties": false, + "properties": { + "$schema": { + "type": "string" + }, + "endpoints": { + "type": "array", + "items": { "$ref": "#/$defs/endpoint" } + } + }, + "$defs": { + "endpoint": { + "type": "object", + "required": ["type", "hostname", "nat_ips"], + "additionalProperties": false, + "properties": { + "type": { + "enum": ["control_plane", "gateway"] + }, + "airport_code": { + "type": "string", + "pattern": "^[a-z]{3}$", + "description": "IATA code for one gateway airport. Absent on geolocated hostnames such as us.gateway.truefoundry.ai." + }, + "hostname": { + "type": "string", + "format": "hostname", + "description": "The hostname a customer connects to. nat_ips is the full allowlist for this hostname." + }, + "nat_ips": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "pattern": "^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])/(3[0-2]|[12]?[0-9])$", + "description": "IPv4 CIDR. Single addresses are written as /32." + } + } + }, + "allOf": [ + { + "if": { + "properties": { "type": { "const": "control_plane" } }, + "required": ["type"] + }, + "then": { + "not": { "required": ["airport_code"] } + } + } + ] + } + } +} diff --git a/.github/scripts/build.py b/.github/scripts/build.py index 627faa3..63bf2e3 100644 --- a/.github/scripts/build.py +++ b/.github/scripts/build.py @@ -3,9 +3,9 @@ # requires-python = ">=3.11" # dependencies = [] # /// -"""Flatten the per-plane source files under data/ into the published artifacts in dist/. +"""Copy eu/ and us/ into the release artifacts in dist/. -Run with: uv run scripts/build.py --version 2026.10.12 +Run with: uv run .github/scripts/build.py --version 2026.10.12 """ from __future__ import annotations @@ -17,85 +17,36 @@ from pathlib import Path ROOT = Path(__file__).resolve().parents[2] # .github/scripts/build.py -> repo root -DATA_DIR = ROOT / "data" DIST_DIR = ROOT / "dist" +PLANES = ("eu", "us") +REPO = "https://github.com/truefoundry/nat-ips" -PLANES = ("us", "eu") - - -def sort_key(entry: dict) -> tuple: - """Stable ordering so a rebuild with no data change produces a byte-identical file.""" - return ( - entry["control_plane"], - 0 if entry["type"] == "control_plane" else 1, - entry.get("airport_code", ""), - entry["region"], - ) - - -def load_entries() -> list[dict]: - entries: list[dict] = [] - - paths = sorted(DATA_DIR.rglob("*.json")) - if not paths: - # Never fall through to an empty build: that would publish a release telling - # customers there is nothing to allowlist. - raise SystemExit(f"no source files found under {DATA_DIR} — refusing to build") - - for path in paths: - doc = json.loads(path.read_text()) - control_plane = doc["control_plane"] - - for source in doc["entries"]: - entry = { - "control_plane": control_plane, - "type": source["type"], - "cloud": source["cloud"], - "region": source["region"], - "continent": source["continent"], - } - - if source["type"] == "gateway": - # Read defensively: the PR diff job builds the base branch's data with this - # script, so a schema change must not make the baseline unbuildable. - if "airport_code" in source: - entry["airport_code"] = source["airport_code"] - entry["gateway_host"] = doc.get("host") - # A cluster with no published endpoint gets no host. Inventing one would - # tell customers to resolve a name that does not exist. - if "hostname" in source: - entry["host"] = source["hostname"] - else: - entry["host"] = doc.get("host_pattern") or doc.get("host") - - if "notes" in source: - entry["notes"] = source["notes"] - - entry["nat_ips"] = sorted( - source["nat_ips"], key=lambda cidr: ipaddress.IPv4Network(cidr) - ) - entries.append(entry) - - entries.sort(key=sort_key) - return entries - - -def flat_ip_list(entries: list[dict]) -> list[str]: - unique = {cidr for entry in entries for cidr in entry["nat_ips"]} + +def flat_ips(endpoints: list[dict]) -> list[str]: + unique = {cidr for endpoint in endpoints for cidr in endpoint["nat_ips"]} return sorted(unique, key=lambda cidr: ipaddress.IPv4Network(cidr)) +def load_plane(plane: str) -> dict: + path = ROOT / plane / "ip-ranges.json" + if not path.exists(): + raise SystemExit(f"missing {path} — refusing to build") + doc = json.loads(path.read_text()) + # Release assets are the public document. $schema is only for editors in the repo. + return {"endpoints": doc["endpoints"]} + + def write_json(path: Path, payload: dict) -> None: path.write_text(json.dumps(payload, indent=2) + "\n") print(f"wrote {path.relative_to(ROOT)}") -def write_text(path: Path, version: str, timestamp: str, ips: list[str]) -> None: +def write_text(path: Path, plane: str, version: str, timestamp: str, ips: list[str]) -> None: header = [ - "# TrueFoundry outbound NAT IPs", + f"# TrueFoundry {plane.upper()} outbound NAT IPs", f"# version: {version}", f"# generated: {timestamp}", - "# https://github.com/truefoundry/nat-ips", + f"# {REPO}", ] path.write_text("\n".join(header + ips) + "\n") print(f"wrote {path.relative_to(ROOT)}") @@ -107,29 +58,19 @@ def main() -> int: args = parser.parse_args() timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") - entries = load_entries() DIST_DIR.mkdir(exist_ok=True) - def envelope(subset: list[dict]) -> dict: - return { - "version": args.version, - "generation_timestamp": timestamp, - "entry_count": len(subset), - "ip_count": len(flat_ip_list(subset)), - "entries": subset, - } - - write_json(DIST_DIR / "nat-ips.json", envelope(entries)) - write_text(DIST_DIR / "nat-ips.txt", args.version, timestamp, flat_ip_list(entries)) - for plane in PLANES: - subset = [e for e in entries if e["control_plane"] == plane] - if not subset: - continue - write_json(DIST_DIR / f"{plane}.json", envelope(subset)) - write_text(DIST_DIR / f"{plane}.txt", args.version, timestamp, flat_ip_list(subset)) + doc = load_plane(plane) + write_json(DIST_DIR / f"{plane}.json", doc) + write_text( + DIST_DIR / f"{plane}.txt", + plane, + args.version, + timestamp, + flat_ips(doc["endpoints"]), + ) - print(f"\n{len(entries)} entries, {len(flat_ip_list(entries))} unique IPs, version {args.version}") return 0 diff --git a/.github/scripts/release_notes.py b/.github/scripts/release_notes.py index 7c8c173..e446d9f 100644 --- a/.github/scripts/release_notes.py +++ b/.github/scripts/release_notes.py @@ -3,9 +3,10 @@ # requires-python = ">=3.11" # dependencies = [] # /// -"""Diff two built nat-ips.json files and write release notes a firewall admin can act on. +"""Diff published IP range files and write release notes. -Run with: uv run scripts/release_notes.py --new dist/nat-ips.json --old /tmp/prev.json +Prints publish=, added=, and removed= for the release workflow. +Run with: uv run .github/scripts/release_notes.py --version 2026.10.12 --eu dist/eu.json --us dist/us.json """ from __future__ import annotations @@ -13,140 +14,165 @@ import argparse import ipaddress import json -from collections import defaultdict +import sys +from datetime import datetime, timedelta, timezone from pathlib import Path REPO = "https://github.com/truefoundry/nat-ips" +PLANES = ("eu", "us") +ACTIVATION_DAYS = 15 -def ip_owners(doc: dict) -> dict[str, list[dict]]: - """Map every CIDR to all entries that use it. +def ip_sort(cidr: str) -> ipaddress.IPv4Network: + return ipaddress.IPv4Network(cidr) - One NAT gateway can back several entries, so this is deliberately many-to-many — - attributing an IP to a single entry would hide where it is actually in use. - """ - index: dict[str, list[dict]] = defaultdict(list) - for entry in doc.get("entries", []): - for cidr in entry["nat_ips"]: - index[cidr].append(entry) - return index +def load_doc(path: Path | None) -> dict: + if path is None: + return {"endpoints": []} + if not path.exists(): + raise SystemExit(f"missing {path}") + return json.loads(path.read_text()) -def describe(entry: dict) -> str: - bits = [entry["control_plane"].upper(), entry["type"].replace("_", " "), entry["region"]] - if entry.get("airport_code"): - bits.append(entry["airport_code"].upper()) - return " · ".join(bits) +def cidrs_of(doc: dict) -> set[str]: + return {cidr for endpoint in doc.get("endpoints", []) for cidr in endpoint["nat_ips"]} -def host_label(entry: dict) -> str: - """Not every cluster has a published endpoint; say so rather than print empty backticks.""" - host = entry.get("host") - return f"`{host}`" if host else "_no published endpoint_" +def by_hostname(doc: dict) -> dict[str, set[str]]: + return {endpoint["hostname"]: set(endpoint["nat_ips"]) for endpoint in doc.get("endpoints", [])} -def ip_sort(cidr: str) -> ipaddress.IPv4Network: - return ipaddress.IPv4Network(cidr) + +def noun(count: int, singular: str, plural: str) -> str: + return f"{count} {singular if count == 1 else plural}" -def render_changed(title: str, cidrs: list[str], index: dict[str, list[dict]]) -> list[str]: - """Group changed IPs under every entry that uses them.""" - if not cidrs: - return [] +def activation_date(version: str) -> str | None: + """CalVer date plus 15 days. Local and CI versions have no calendar date.""" + date_part = version.split("-", 1)[0] + try: + published = datetime.strptime(date_part, "%Y.%m.%d").replace(tzinfo=timezone.utc) + except ValueError: + return None + return (published + timedelta(days=ACTIVATION_DAYS)).strftime("%Y-%m-%d") - buckets: dict[tuple[str, str], set[str]] = defaultdict(set) - for cidr in cidrs: - for entry in index[cidr]: - buckets[(describe(entry), host_label(entry))].add(cidr) - lines = [f"### {title} ({len(cidrs)})", ""] - for (label, host), group in sorted(buckets.items()): - lines.append(f"**{label}** — {host}") - lines.extend(f"- `{cidr}`" for cidr in sorted(group, key=ip_sort)) +def render_full(plane: str, doc: dict) -> list[str]: + lines: list[str] = [] + for endpoint in doc.get("endpoints", []): + lines.append(f"**{plane.upper()}** · `{endpoint['hostname']}`") + lines.extend(f"- `{cidr}`" for cidr in endpoint["nat_ips"]) lines.append("") return lines -def render_full(doc: dict) -> list[str]: - """List every entry in order — used for the first release, where nothing is a 'change'.""" - lines = ["### Published IPs", ""] - for entry in doc.get("entries", []): - lines.append(f"**{describe(entry)}** — {host_label(entry)}") - lines.extend(f"- `{cidr}`" for cidr in entry["nat_ips"]) +def render_hostname_changes(plane: str, hostname: str, added: list[str], removed: list[str]) -> list[str]: + lines = [f"### {plane.upper()} · `{hostname}`", ""] + if added: + lines.append("Added:") + lines.extend(f"- `{cidr}`" for cidr in added) + lines.append("") + if removed: + lines.append("Removed:") + lines.extend(f"- `{cidr}`" for cidr in removed) lines.append("") return lines def main() -> int: parser = argparse.ArgumentParser() - parser.add_argument("--new", required=True, type=Path) - parser.add_argument("--old", type=Path, help="omit for the first release") + parser.add_argument("--version", required=True) + parser.add_argument("--eu", required=True, type=Path) + parser.add_argument("--us", required=True, type=Path) + parser.add_argument("--old-eu", type=Path) + parser.add_argument("--old-us", type=Path) parser.add_argument("--out", type=Path, default=Path("RELEASE_NOTES.md")) args = parser.parse_args() - new_doc = json.loads(args.new.read_text()) - new_index = ip_owners(new_doc) - version = new_doc["version"] - - old_doc: dict = {} - if args.old and args.old.exists(): - old_doc = json.loads(args.old.read_text()) - old_index = ip_owners(old_doc) - - added = sorted(set(new_index) - set(old_index), key=ip_sort) - removed = sorted(set(old_index) - set(new_index), key=ip_sort) - - lines = [f"## TrueFoundry outbound NAT IPs — {version}", ""] - - if not old_doc: + new_docs = {"eu": load_doc(args.eu), "us": load_doc(args.us)} + has_baseline = args.old_eu is not None or args.old_us is not None + old_docs = { + "eu": load_doc(args.old_eu) if has_baseline else {"endpoints": []}, + "us": load_doc(args.old_us) if has_baseline else {"endpoints": []}, + } + + added_ips: set[str] = set() + removed_ips: set[str] = set() + hostname_changes: list[tuple[str, str, list[str], list[str]]] = [] + + for plane in PLANES: + new_set = cidrs_of(new_docs[plane]) + old_set = cidrs_of(old_docs[plane]) if has_baseline else set() + if has_baseline: + added_ips |= new_set - old_set + removed_ips |= old_set - new_set + + new_hosts = by_hostname(new_docs[plane]) + old_hosts = by_hostname(old_docs[plane]) if has_baseline else {} + for hostname in sorted(set(new_hosts) | set(old_hosts)): + added = sorted(new_hosts.get(hostname, set()) - old_hosts.get(hostname, set()), key=ip_sort) + removed = sorted(old_hosts.get(hostname, set()) - new_hosts.get(hostname, set()), key=ip_sort) + if added or removed: + hostname_changes.append((plane, hostname, added, removed)) + + publish = not has_baseline or bool(added_ips or removed_ips or hostname_changes) + lines = [f"## TrueFoundry IP ranges — {args.version}", ""] + + if not has_baseline: lines += [ - "First published release. Allowlist the IPs below for traffic arriving from TrueFoundry.", + "First published release. These addresses are in use. Allowlist them for traffic arriving from TrueFoundry.", "", - ] - lines += render_full(new_doc) - elif not added and not removed: - lines += [ - f"No IP changes since `{old_doc['version']}`. Metadata or docs only — **no firewall action needed**.", + "Addresses added in a later release start carrying traffic 15 days after that release. " + "When an address is removed, a release is published so it can be dropped from allowlists.", "", ] - else: - action = [] - if added: - action.append(f"**{len(added)} IP(s) to add**") - if removed: - action.append(f"**{len(removed)} IP(s) safe to remove**") + for plane in PLANES: + lines += render_full(plane, new_docs[plane]) + elif not hostname_changes: lines += [ - f"Changes since `{old_doc['version']}`: " + " and ".join(action) + ".", - "", - "> Add new IPs **before** the rollout date. Removing retired IPs is optional and can wait.", + "No IP changes since the previous release. **No firewall action needed.**", "", ] - lines += render_changed("Added", added, new_index) - lines += render_changed("Removed", removed, old_index) - - # Only link per-plane assets that this release actually built. - planes = sorted({e["control_plane"] for e in new_doc.get("entries", [])}) - lines += [ - "---", - "", - "### Full list", - "", - f"- Everything: {REPO}/releases/download/v{version}/nat-ips.json", - ] - lines += [ - f"- {plane.upper()} control plane: {REPO}/releases/download/v{version}/{plane}.json" - for plane in planes - ] + else: + if added_ips: + live = activation_date(args.version) + when = f" on **{live}**" if live else "" + lines += [ + f"Allowlist **{noun(len(added_ips), 'new address', 'new addresses')}** " + f"before they start carrying traffic{when}, {ACTIVATION_DAYS} days after this release.", + "", + ] + if removed_ips: + lines += [ + f"**{noun(len(removed_ips), 'address removed', 'addresses removed')}.** " + "They are no longer used and can be dropped from allowlists.", + "", + ] + if hostname_changes and not added_ips and not removed_ips: + lines += [ + "Addresses moved between hostnames. Update the allowlist for the hostnames below.", + "", + ] + for plane, hostname, added, removed in hostname_changes: + lines += render_hostname_changes(plane, hostname, added, removed) + + lines += ["---", "", "### Full list", ""] + for plane in PLANES: + count = len(cidrs_of(new_docs[plane])) + lines.append( + f"- {plane.upper()} ({count} addresses): {REPO}/releases/download/v{args.version}/{plane}.json" + ) lines += [ - "- Plain text for firewall scripts: swap `.json` for `.txt` in any URL above.", - f"- Always-latest instead of pinned: {REPO}/releases/latest/download/nat-ips.json", - "", - f"Totals in this release: **{new_doc['entry_count']} entries, {new_doc['ip_count']} unique IPs**.", + "- Plain text, one CIDR per line: swap `.json` for `.txt`.", + f"- Always-latest: {REPO}/releases/latest/download/eu.json", "", ] args.out.write_text("\n".join(lines)) - print(f"wrote {args.out} ({len(added)} added, {len(removed)} removed)") + print(f"publish={'true' if publish else 'false'}") + print(f"added={len(added_ips)}") + print(f"removed={len(removed_ips)}") + print(f"wrote {args.out} ({len(added_ips)} added, {len(removed_ips)} removed)", file=sys.stderr) return 0 diff --git a/.github/scripts/validate.py b/.github/scripts/validate.py index 5fba860..c39e913 100644 --- a/.github/scripts/validate.py +++ b/.github/scripts/validate.py @@ -3,9 +3,9 @@ # requires-python = ">=3.11" # dependencies = ["jsonschema>=4.21"] # /// -"""Validate every source file under data/ against the schema and our own sanity rules. +"""Validate eu/ip-ranges.json and us/ip-ranges.json. -Run with: uv run scripts/validate.py +Run with: uv run .github/scripts/validate.py """ from __future__ import annotations @@ -14,17 +14,35 @@ import json import re import sys -from collections import defaultdict from pathlib import Path from jsonschema import Draft202012Validator ROOT = Path(__file__).resolve().parents[2] # .github/scripts/validate.py -> repo root -DATA_DIR = ROOT / "data" -SCHEMA_PATH = ROOT / "schema" / "nat-ips.schema.json" +SCHEMA_PATH = ROOT / ".github" / "schema" / "ip-ranges.schema.json" +PLANES = ("eu", "us") HOSTNAME_RE = re.compile(r"^(?!-)[a-z0-9-]{1,63}(? None: warnings.append(f"{where}: {msg}") +def ip_sort(cidr: str): + return ipaddress.IPv4Network(cidr) + + def check_cidr(where: str, raw: str) -> ipaddress.IPv4Network | None: """Parse a CIDR and reject anything that cannot be a public NAT egress address.""" try: net = ipaddress.IPv4Network(raw, strict=True) except ValueError as exc: - # strict=True rejects host bits set, e.g. 10.0.0.1/24 — call that out specifically. error(where, f"invalid CIDR {raw!r}: {exc}") return None @@ -51,72 +72,147 @@ def check_cidr(where: str, raw: str) -> ipaddress.IPv4Network | None: return net -def validate_file(path: Path, validator: Draft202012Validator) -> list[dict]: - rel = path.relative_to(ROOT) +def endpoint_sort_key(endpoint: dict) -> tuple: + if endpoint["type"] == "control_plane": + return (0, endpoint["hostname"]) + hostname = endpoint["hostname"] + if hostname in GEO_ORDER: + return (1, GEO_ORDER.index(hostname)) + return (2, endpoint.get("airport_code", ""), hostname) + + +def check_rollups(where: str, endpoints: list[dict]) -> None: + """Geolocated hostnames must list exactly the NAT addresses of their airports.""" + airports: dict[str, set[str]] = {} + geos: dict[str, set[str]] = {} + + for endpoint in endpoints: + if endpoint["type"] != "gateway": + continue + if "airport_code" in endpoint: + airports[endpoint["airport_code"]] = set(endpoint["nat_ips"]) + else: + geos[endpoint["hostname"]] = set(endpoint["nat_ips"]) + + if not airports: + return + + expected_global = set().union(*airports.values()) + if GEO_ORDER[0] not in geos: + error(where, f"missing {GEO_ORDER[0]}, the union of every airport") + else: + report_set_diff(where, GEO_ORDER[0], expected_global, geos[GEO_ORDER[0]]) + + for hostname, members in REGIONAL_GATEWAYS.items(): + present = members & airports.keys() + if not present: + if hostname in geos: + error(where, f"{hostname} lists addresses but none of its airports are in this file") + continue + expected = set().union(*(airports[code] for code in present)) + if hostname not in geos: + error(where, f"missing {hostname}") + continue + report_set_diff(where, hostname, expected, geos[hostname]) + + +def report_set_diff(where: str, hostname: str, expected: set[str], actual: set[str]) -> None: + missing = sorted(expected - actual, key=ip_sort) + extra = sorted(actual - expected, key=ip_sort) + if missing: + shown = ", ".join(missing[:6]) + suffix = "" if len(missing) <= 6 else f" (+{len(missing) - 6} more)" + error(where, f"{hostname} is missing {shown}{suffix}") + if extra: + shown = ", ".join(extra[:6]) + suffix = "" if len(extra) <= 6 else f" (+{len(extra) - 6} more)" + error(where, f"{hostname} includes {shown}{suffix}, which are not on its airports") + + +def validate_file(plane: str, validator: Draft202012Validator) -> list[dict]: + path = ROOT / plane / "ip-ranges.json" + rel = f"{plane}/ip-ranges.json" + extras = sorted(p.name for p in (ROOT / plane).glob("*.json") if p.name != "ip-ranges.json") + if extras: + error(plane, f"unexpected JSON files {extras}; keep addresses in ip-ranges.json") + + if not path.exists(): + error(plane, "missing ip-ranges.json") + return [] + try: doc = json.loads(path.read_text()) except json.JSONDecodeError as exc: - error(str(rel), f"not valid JSON: {exc}") + error(rel, f"not valid JSON: {exc}") return [] schema_errors = sorted(validator.iter_errors(doc), key=lambda e: list(e.path)) if schema_errors: for exc in schema_errors: - location = "/".join(str(p) for p in exc.path) or "" - error(str(rel), f"{location}: {exc.message}") + location = "/".join(str(part) for part in exc.path) or "" + error(rel, f"{location}: {exc.message}") return [] - # The directory is the source of truth for which control plane a file belongs to. - expected_plane = path.parent.name - if doc["control_plane"] != expected_plane: - error(str(rel), f"control_plane is {doc['control_plane']!r} but the file lives under data/{expected_plane}/") + endpoints = doc["endpoints"] + if plane == "eu" and not endpoints: + error(rel, "EU publishes gateway addresses; endpoints is empty") - expected_type = doc["plane"] - if expected_type == "control_plane" and "host_pattern" not in doc: - error(str(rel), "control-plane files need a host_pattern") - if expected_type == "gateway" and "host" not in doc: - error(str(rel), "gateway files need a host") + if endpoints != sorted(endpoints, key=endpoint_sort_key): + error(rel, "endpoints must be ordered control plane, then geolocated gateways, then airport code") built: list[dict] = [] - for index, entry in enumerate(doc["entries"]): - where = f"{rel} entries[{index}]" + seen_hosts: dict[str, int] = {} + for index, endpoint in enumerate(endpoints): + where = f"{rel} endpoints[{index}]" + hostname = endpoint["hostname"] + if not HOSTNAME_RE.match(hostname): + error(where, f"hostname {hostname!r} is not a valid lowercase FQDN") - if entry["type"] != expected_type: - error(where, f"type is {entry['type']!r} but this file declares plane {expected_type!r}") + if hostname in seen_hosts: + error(where, f"hostname {hostname!r} is already used by endpoints[{seen_hosts[hostname]}]") + seen_hosts[hostname] = index - hostname = entry.get("hostname") - if hostname and not HOSTNAME_RE.match(hostname): - error(where, f"hostname {hostname!r} is not a valid lowercase FQDN") + airport = endpoint.get("airport_code") + if endpoint["type"] == "gateway" and airport is None and hostname not in GEO_ORDER: + error(where, f"hostname {hostname!r} is not a known geolocated gateway") + if airport is not None: + expected = f"{airport}.gateway.truefoundry.ai" + if hostname != expected: + error(where, f"hostname {hostname!r} does not match airport code {airport!r} ({expected})") - # Catches a row pasted into the wrong cluster: the endpoint always leads with its - # own airport code, so a mismatch means the entry has been crossed with another. - airport = entry.get("airport_code") - if hostname and airport and not hostname.startswith(f"{airport}."): - error(where, f"hostname {hostname!r} does not start with airport code {airport!r}") + ordered = sorted(endpoint["nat_ips"], key=ip_sort) + if endpoint["nat_ips"] != ordered: + error(where, "nat_ips must be sorted numerically") - nets = [n for n in (check_cidr(where, ip) for ip in entry["nat_ips"]) if n] - for a_index, a in enumerate(nets): - for b in nets[a_index + 1 :]: - if a.overlaps(b): - error(where, f"{a} and {b} overlap; collapse them into one entry") + nets = [net for net in (check_cidr(where, cidr) for cidr in endpoint["nat_ips"]) if net] + for left_index, left in enumerate(nets): + for right in nets[left_index + 1 :]: + if left.overlaps(right): + error(where, f"{left} and {right} overlap; collapse them into one range") - built.append({"file": str(rel), "entry": entry, "nets": nets}) + built.append({"plane": plane, "endpoint": endpoint, "nets": nets}) + check_rollups(rel, endpoints) return built -def check_cross_file(records: list[dict]) -> None: - """A NAT IP appearing in two places is usually a copy-paste slip, not a real shared gateway.""" - seen: dict[str, list[str]] = defaultdict(list) +def check_duplicates(records: list[dict]) -> None: + """The same address on a geolocated hostname and its airports is expected. + + The same address on two airports, or on two control planes, is a copy-paste slip. + """ + seen: dict[str, list[str]] = {} for record in records: - entry = record["entry"] - label = f"{record['file']} ({entry['region']})" + endpoint = record["endpoint"] + if endpoint["type"] == "gateway" and "airport_code" not in endpoint: + continue + label = f"{record['plane']} {endpoint['hostname']}" for net in record["nets"]: - seen[str(net)].append(label) + seen.setdefault(str(net), []).append(label) for cidr, locations in sorted(seen.items()): if len(locations) > 1: - warn("duplicate IP", f"{cidr} appears in {len(locations)} entries: {', '.join(locations)}") + warn("duplicate IP", f"{cidr} appears in {len(locations)} endpoints: {', '.join(locations)}") def main() -> int: @@ -124,27 +220,27 @@ def main() -> int: print(f"missing schema at {SCHEMA_PATH}", file=sys.stderr) return 1 + for plane in PLANES: + if not (ROOT / plane).is_dir(): + print(f"missing {plane}/ directory", file=sys.stderr) + return 1 + schema = json.loads(SCHEMA_PATH.read_text()) Draft202012Validator.check_schema(schema) validator = Draft202012Validator(schema) - paths = sorted(DATA_DIR.rglob("*.json")) - if not paths: - print("no source files found under data/", file=sys.stderr) - return 1 - records: list[dict] = [] - for path in paths: - records.extend(validate_file(path, validator)) - check_cross_file(records) + for plane in PLANES: + records.extend(validate_file(plane, validator)) + check_duplicates(records) for message in warnings: print(f"warning {message}") for message in errors: print(f"error {message}", file=sys.stderr) - total_ips = sum(len(r["nets"]) for r in records) - print(f"\nchecked {len(paths)} files, {len(records)} entries, {total_ips} IPs") + total_ips = sum(len(record["nets"]) for record in records) + print(f"\nchecked {len(PLANES)} files, {len(records)} endpoints, {total_ips} IP references") if errors: print(f"FAILED with {len(errors)} error(s)", file=sys.stderr) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b2e43c4..2705065 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,8 +4,9 @@ on: push: branches: [main] paths: - - "data/**" - - "schema/**" + - "eu/**" + - "us/**" + - ".github/schema/**" - ".github/scripts/**" workflow_dispatch: inputs: @@ -38,9 +39,9 @@ jobs: - name: Refuse to publish placeholder data run: | - if grep -rqi 'PLACEHOLDER' data/; then - echo "::error::data/ still contains PLACEHOLDER entries. Replace them with real IPs before releasing." - grep -rin 'PLACEHOLDER' data/ + if grep -rqi 'PLACEHOLDER' eu/ us/; then + echo "::error::eu/ or us/ still contains PLACEHOLDER entries. Replace them with real IPs before releasing." + grep -rin 'PLACEHOLDER' eu/ us/ exit 1 fi @@ -59,15 +60,15 @@ jobs: echo "version=$VERSION" >> "$GITHUB_OUTPUT" echo "Releasing v$VERSION" - - name: Fetch the previously released artifact + - name: Fetch the previously released artifacts id: previous env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - # dist/ is not tracked in git, so the diff baseline comes from the last release. PREV="$(gh release list --limit 1 --json tagName --jq '.[0].tagName // empty')" - if [ -n "$PREV" ] && gh release download "$PREV" \ - --pattern nat-ips.json --output "${RUNNER_TEMP}/old.json" 2>/dev/null; then + mkdir -p "${RUNNER_TEMP}/old" + if [ -n "$PREV" ] && gh release download "$PREV" --pattern eu.json --dir "${RUNNER_TEMP}/old" 2>/dev/null; then + gh release download "$PREV" --pattern us.json --dir "${RUNNER_TEMP}/old" 2>/dev/null || true echo "tag=$PREV" >> "$GITHUB_OUTPUT" echo "Comparing against $PREV" else @@ -79,20 +80,33 @@ jobs: run: uv run .github/scripts/build.py --version "${{ steps.version.outputs.version }}" - name: Write release notes + id: notes run: | - ARGS=(--new dist/nat-ips.json --out "${RUNNER_TEMP}/notes.md") - if [ -n "${{ steps.previous.outputs.tag }}" ]; then - ARGS+=(--old "${RUNNER_TEMP}/old.json") + ARGS=( + --version "${{ steps.version.outputs.version }}" + --eu dist/eu.json + --us dist/us.json + --out "${RUNNER_TEMP}/notes.md" + ) + if [ -f "${RUNNER_TEMP}/old/eu.json" ]; then + ARGS+=(--old-eu "${RUNNER_TEMP}/old/eu.json") fi - uv run .github/scripts/release_notes.py "${ARGS[@]}" + if [ -f "${RUNNER_TEMP}/old/us.json" ]; then + ARGS+=(--old-us "${RUNNER_TEMP}/old/us.json") + fi + uv run .github/scripts/release_notes.py "${ARGS[@]}" >> "$GITHUB_OUTPUT" cat "${RUNNER_TEMP}/notes.md" >> "$GITHUB_STEP_SUMMARY" - name: Stop here on a dry run if: inputs.dry_run run: echo "::notice::Dry run — notes are in the job summary. Nothing tagged or published." + - name: Skip when the address list is unchanged + if: ${{ !inputs.dry_run && steps.notes.outputs.publish != 'true' }} + run: echo "::notice::No addresses were added or removed. No release published." + - name: Tag and publish - if: ${{ !inputs.dry_run }} + if: ${{ !inputs.dry_run && steps.notes.outputs.publish == 'true' }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} VERSION: ${{ steps.version.outputs.version }} @@ -100,7 +114,7 @@ jobs: # gh creates the tag at this commit, so nothing is ever pushed to a protected branch. gh release create "v${VERSION}" \ --target "${GITHUB_SHA}" \ - --title "NAT IPs v${VERSION}" \ + --title "IP ranges v${VERSION}" \ --notes-file "${RUNNER_TEMP}/notes.md" \ --latest \ dist/* diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 1b820c2..0f2742d 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -3,15 +3,17 @@ name: Validate on: pull_request: paths: - - "data/**" - - "schema/**" + - "eu/**" + - "us/**" + - ".github/schema/**" - ".github/scripts/**" - ".github/workflows/validate.yml" push: branches: [main] paths: - - "data/**" - - "schema/**" + - "eu/**" + - "us/**" + - ".github/schema/**" - ".github/scripts/**" workflow_dispatch: @@ -56,24 +58,28 @@ jobs: - name: Build this branch run: | uv run .github/scripts/build.py --version "pr-${{ github.event.pull_request.number }}" - cp dist/nat-ips.json "${RUNNER_TEMP}/new.json" + cp dist/eu.json dist/us.json "${RUNNER_TEMP}/" - name: Build the base branch run: | - git checkout "origin/${{ github.base_ref }}" -- data/ + git checkout "origin/${{ github.base_ref }}" -- eu us uv run .github/scripts/build.py --version "${{ github.base_ref }}" - cp dist/nat-ips.json "${RUNNER_TEMP}/old.json" - git checkout HEAD -- data/ + mkdir -p "${RUNNER_TEMP}/old" + cp dist/eu.json dist/us.json "${RUNNER_TEMP}/old/" + git checkout HEAD -- eu us - name: Render diff run: | uv run .github/scripts/release_notes.py \ - --new "${RUNNER_TEMP}/new.json" \ - --old "${RUNNER_TEMP}/old.json" \ + --version "pr-${{ github.event.pull_request.number }}" \ + --eu "${RUNNER_TEMP}/eu.json" \ + --us "${RUNNER_TEMP}/us.json" \ + --old-eu "${RUNNER_TEMP}/old/eu.json" \ + --old-us "${RUNNER_TEMP}/old/us.json" \ --out "${RUNNER_TEMP}/diff.md" { - echo "" + echo "" echo "## Proposed IP changes" echo tail -n +2 "${RUNNER_TEMP}/diff.md" diff --git a/README.md b/README.md index 0c71d6f..f8a5d6c 100644 --- a/README.md +++ b/README.md @@ -1,153 +1,84 @@ -# TrueFoundry NAT IPs +# TrueFoundry IP ranges -The outbound (egress) IP addresses TrueFoundry connects **from**. If you restrict inbound traffic to your model endpoints, webhooks, databases, or private APIs, allowlist these. +Outbound IP addresses TrueFoundry uses when it connects to your systems. Allowlist these if you restrict inbound access to your model endpoints, webhooks, databases, or private APIs. -Published as machine-readable JSON with a [GitHub Release](https://github.com/truefoundry/nat-ips/releases) for every change, so you can subscribe and automate. +The directory is the control plane: -> **IPs change.** Regions get added and cloud NAT gateways get renumbered. Subscribe to releases rather than copying the list once. - ---- - -## Get the list - -Always-latest URLs — these follow the newest release: - -| What | URL | +| Control plane | List | | --- | --- | -| Everything | `https://github.com/truefoundry/nat-ips/releases/latest/download/nat-ips.json` | -| EU control plane only | `https://github.com/truefoundry/nat-ips/releases/latest/download/eu.json` | -| Plain text, one CIDR per line | swap `.json` → `.txt` in any URL above | +| EU | [`eu/ip-ranges.json`](eu/ip-ranges.json) | +| US | [`us/ip-ranges.json`](us/ip-ranges.json) | -Pin to an exact version by replacing `latest/download` with `download/v2026.10.12`. +Each [GitHub Release](https://github.com/truefoundry/nat-ips/releases) attaches the same lists: -```bash -# Every IP, ready to paste into a firewall rule -curl -sL https://github.com/truefoundry/nat-ips/releases/latest/download/nat-ips.txt | grep -v '^#' +- https://github.com/truefoundry/nat-ips/releases/latest/download/eu.json +- https://github.com/truefoundry/nat-ips/releases/latest/download/us.json -# Just the EU-region PoPs -curl -sL https://github.com/truefoundry/nat-ips/releases/latest/download/nat-ips.json \ - | jq -r '.entries[] | select(.continent == "eu") | .nat_ips[]' -``` - -## Coverage - -| Plane | Status | -| --- | --- | -| AI Gateway — `*.gateway.truefoundry.ai` | **Published**, 42 regions | -| EU control plane — `*.eu.truefoundry.cloud` | Not yet published | -| US control plane — `*.us.truefoundry.cloud` | Not yet published | +Replace `.json` with `.txt` for one CIDR per line. Pin a release by replacing `latest/download` with `download/v2026.10.12`. -If you call models through the AI Gateway, what's published today is what you need. Control-plane egress IPs are coming; watch releases to be notified. +## What to allowlist -## Data format +Find the hostname you connect to. `nat_ips` is every address that hostname can connect from. ```json { - "version": "2026.10.12", - "generation_timestamp": "2026-10-12T12:52:47Z", - "entry_count": 42, - "ip_count": 84, - "entries": [ + "endpoints": [ + { + "type": "control_plane", + "hostname": "eu.truefoundry.cloud", + "nat_ips": ["203.0.113.10/32"] + }, + { + "type": "gateway", + "hostname": "us.gateway.truefoundry.ai", + "nat_ips": ["52.4.51.133/32", "54.204.114.10/32"] + }, { - "control_plane": "eu", "type": "gateway", - "cloud": "gcp", - "region": "europe-southwest1", - "continent": "eu", - "airport_code": "mad", - "gateway_host": "gateway.truefoundry.ai", - "host": "mad.gateway.truefoundry.ai", - "nat_ips": ["34.175.44.33/32", "34.175.219.109/32"] + "airport_code": "orf", + "hostname": "orf.gateway.truefoundry.ai", + "nat_ips": ["52.4.51.133/32", "54.204.114.10/32"] } ] } ``` -| Field | Meaning | +`type` is `control_plane` or `gateway`. Airport endpoints include `airport_code`. Continent hostnames combine the regional gateways that can serve them, including the documented fallback, and `nat_ips` is that combined list: + +| Hostname | Addresses | | --- | --- | -| `version` | CalVer release, matching the git tag `v` | -| `generation_timestamp` | UTC build time, ISO 8601 | -| `control_plane` | `us` or `eu` — which control plane manages this cluster | -| `type` | `control_plane` or `gateway` | -| `cloud` / `region` | Where the egress NAT lives | -| `continent` | `na`, `sa`, `eu`, `ap`, `me`, `af`, `oc` | -| `airport_code` | Gateway entries only — IATA code for the PoP | -| `gateway_host` | Gateway entries only — the global entry point | -| `host` | The endpoint this entry serves. Absent when the cluster's NAT addresses are allocated but its endpoint isn't published yet — **allowlist those IPs anyway**, so a region going live isn't an urgent firewall change on your side | -| `nat_ips` | IPv4 CIDRs. Single addresses are written `/32` | +| `gateway.truefoundry.ai` | Every regional gateway | +| `us.gateway.truefoundry.ai` | North Virginia, San Francisco, Dallas, and Toronto | +| `eu.gateway.truefoundry.ai` | London, Madrid, Gavle, and Doha | +| `sa.gateway.truefoundry.ai` | Sao Paulo | +| `au.gateway.truefoundry.ai` | Sydney, Melbourne, and Singapore | +| `as.gateway.truefoundry.ai` | Mumbai, Singapore, Doha, and Melbourne | -> `airport_code` is **not unique**. Two clusters in the same metro on different clouds share a code — `sfo` is both AWS `us-west-1` and Azure `westus`. Key on `cluster`/`region`, not the airport code. +`us/ip-ranges.json` is empty until the US control plane publishes egress addresses. -Entries are sorted deterministically and IPs are sorted numerically, so a `git diff` between versions is readable. +## When addresses change -JSON Schema for the source files: [`schema/nat-ips.schema.json`](schema/nat-ips.schema.json). +Address changes are published as a GitHub Release. -## Subscribe to changes +A new address is announced in a release and starts carrying traffic **15 days** after that release. Allowlist it during those 15 days. -**1. GitHub notifications** — **Watch → Custom → Releases** at the top of this repo. Each person who wants alerts does this individually. +When an address is removed, a new release is published. Drop it from your allowlist after that release. -**2. RSS / Atom** — point a feed reader, the Slack RSS app, or an internal bot at: +To be notified, choose **Watch → Custom → Releases** on this repository, or subscribe to: ``` https://github.com/truefoundry/nat-ips/releases.atom ``` -In Slack: `/feed subscribe https://github.com/truefoundry/nat-ips/releases.atom` - -**3. Poll from automation** — check `version` and act when it changes: - -```bash -VERSION=$(curl -sL https://github.com/truefoundry/nat-ips/releases/latest/download/nat-ips.json | jq -r .version) -``` - -Every release note lists the exact IPs **added** and **removed** versus the previous release, so you never need to diff on your side. +## Updating the list -> Add newly published IPs promptly. Removing retired IPs is optional and can wait for your next maintenance window — stale entries are harmless. - ---- - -## For TrueFoundry engineers - -Source of truth is `data//.json`. `dist/` is a build artifact — it is **not tracked in git**, it is built in CI and attached to each release. Build it locally whenever you want to inspect it. - -``` -data/ -└── eu/ - └── gateway.json # 42 clusters on *.gateway.truefoundry.ai -``` - -Still to add: `data/eu/control-plane.json`, `data/us/control-plane.json`, `data/us/gateway.json`. - -To add or change IPs: - -1. Edit the relevant file under `data/`. -2. Open a PR. CI validates the schema and comments the exact IP diff for review. -3. Merge to `main`. The release workflow builds `dist/`, tags `v` at that commit, and publishes a release with notes listing added and removed IPs. - -Run the same checks locally: +Edit `eu/ip-ranges.json` or `us/ip-ranges.json` and open a pull request. Merging to `main` publishes a release when an address is added or removed. ```bash uv run .github/scripts/validate.py uv run .github/scripts/build.py --version 0.0.0-local ``` -Validation rejects malformed CIDRs, host bits set on a prefix, private or reserved addresses, overlapping ranges within an entry, a `hostname` whose leading label disagrees with its `airport_code`, gateway entries missing an `airport_code`, and a `control_plane` that disagrees with its directory. Duplicate IPs across entries are reported as warnings. - -To preview a release without publishing: **Actions → Release → Run workflow → dry run**. The notes land in the job summary. - -> The release workflow never pushes to `main` — it tags the merge commit and uploads artifacts, so the branch ruleset requiring pull requests doesn't block it. Each release's diff baseline is the `nat-ips.json` asset from the previous release, so **don't delete old releases**. - -### Provenance - -The gateway data was transcribed from the internal *LLMGateway Clusters* wiki page, where paired NAT addresses render as one unseparated string (`18.119.53.2363.130.37.65`). Those splits are ambiguous — several parse more than one way — so each was resolved against the provider's published ranges (AWS `ip-ranges.json`, GCP `cloud.json`, Azure service tags) and accepted only when both addresses fall inside the claimed region. Two rows do not split the way they look: - -- `tfy-aws-fra-gtw` → `3.65.93.12` + `63.181.139.3` (not `3.65.93.126` + `3.181.139.3`) -- `tfy-aws-hnd-gtw` → `54.238.73.1` + `18.178.26.39` (not `54.238.73.11` + `8.178.26.39`) - -Two region codes on the wiki are also wrong and are corrected here: `tfy-aws-mad-gtw` is `eu-south-2` (wiki says `ap-south-2`) and `tfy-aws-zrh-gtw` is `eu-central-2` (wiki says `eu-central-1`). - -**Re-verify after any bulk edit** rather than trusting a copy-paste from the wiki. - ## Reporting a problem -Something wrong or missing? [Open an issue](https://github.com/truefoundry/nat-ips/issues) or contact TrueFoundry support. For anything security-sensitive, email security@truefoundry.com instead of filing a public issue. +Something wrong or missing? [Open an issue](https://github.com/truefoundry/nat-ips/issues) or contact TrueFoundry support. For anything security-sensitive, email security@truefoundry.com. diff --git a/data/eu/gateway.json b/data/eu/gateway.json deleted file mode 100644 index 9507a50..0000000 --- a/data/eu/gateway.json +++ /dev/null @@ -1,527 +0,0 @@ -{ - "$schema": "../../schema/nat-ips.schema.json", - "plane": "gateway", - "control_plane": "eu", - "host": "gateway.truefoundry.ai", - "entries": [ - { - "type": "gateway", - "cloud": "aws", - "region": "us-east-1", - "continent": "na", - "airport_code": "orf", - "cluster": "tfy-aws-orf-gtw", - "hostname": "orf.gateway.truefoundry.ai", - "nat_ips": [ - "52.4.51.133/32", - "54.204.114.10/32" - ] - }, - { - "type": "gateway", - "cloud": "aws", - "region": "us-east-2", - "continent": "na", - "airport_code": "cle", - "cluster": "tfy-aws-cle-gtw", - "nat_ips": [ - "18.119.53.236/32", - "3.130.37.65/32" - ] - }, - { - "type": "gateway", - "cloud": "aws", - "region": "us-west-1", - "continent": "na", - "airport_code": "sfo", - "cluster": "tfy-aws-sfo-gtw", - "nat_ips": [ - "13.52.112.39/32", - "54.193.213.45/32" - ] - }, - { - "type": "gateway", - "cloud": "aws", - "region": "us-west-2", - "continent": "na", - "airport_code": "pdx", - "cluster": "tfy-aws-pdx-gtw", - "nat_ips": [ - "44.236.36.218/32", - "34.210.236.98/32" - ] - }, - { - "type": "gateway", - "cloud": "aws", - "region": "mx-central-1", - "continent": "na", - "airport_code": "mex", - "cluster": "tfy-aws-mex-gtw", - "nat_ips": [ - "78.13.15.252/32", - "78.13.183.82/32" - ] - }, - { - "type": "gateway", - "cloud": "gcp", - "region": "southamerica-east1", - "continent": "sa", - "airport_code": "gru", - "cluster": "tfy-gcp-gru-gtw", - "hostname": "gru.gateway.truefoundry.ai", - "nat_ips": [ - "34.39.220.236/32", - "34.39.220.106/32" - ] - }, - { - "type": "gateway", - "cloud": "aws", - "region": "sa-east-1", - "continent": "sa", - "airport_code": "gru", - "cluster": "tfy-aws-gru-gtw", - "nat_ips": [ - "56.126.81.140/32", - "56.126.18.155/32" - ] - }, - { - "type": "gateway", - "cloud": "gcp", - "region": "northamerica-northeast2", - "continent": "na", - "airport_code": "yyz", - "cluster": "tfy-gcp-yyz-gtw", - "hostname": "yyz.gateway.truefoundry.ai", - "nat_ips": [ - "34.130.196.232/32", - "34.130.234.128/32" - ] - }, - { - "type": "gateway", - "cloud": "aws", - "region": "ca-central-1", - "continent": "na", - "airport_code": "yyz", - "cluster": "tfy-aws-yyz-gtw", - "nat_ips": [ - "16.52.203.122/32", - "15.156.30.166/32" - ] - }, - { - "type": "gateway", - "cloud": "aws", - "region": "ca-west-1", - "continent": "na", - "airport_code": "yyc", - "cluster": "tfy-aws-yyc-gtw", - "nat_ips": [ - "40.177.86.240/32", - "40.176.147.21/32" - ] - }, - { - "type": "gateway", - "cloud": "azure", - "region": "westus", - "continent": "na", - "airport_code": "sfo", - "cluster": "tfy-az-sfo-gtw", - "hostname": "sfo.gateway.truefoundry.ai", - "nat_ips": [ - "40.75.129.73/32", - "20.245.207.53/32" - ] - }, - { - "type": "gateway", - "cloud": "gcp", - "region": "us-south1", - "continent": "na", - "airport_code": "dfw", - "cluster": "tfy-gcp-dfw-gtw", - "hostname": "dfw.gateway.truefoundry.ai", - "nat_ips": [ - "34.174.18.136/32", - "34.174.240.150/32" - ] - }, - { - "type": "gateway", - "cloud": "aws", - "region": "eu-west-2", - "continent": "eu", - "airport_code": "lhr", - "cluster": "tfy-aws-lhr-gtw", - "hostname": "lhr.gateway.truefoundry.ai", - "nat_ips": [ - "18.130.148.118/32", - "13.42.115.230/32" - ] - }, - { - "type": "gateway", - "cloud": "aws", - "region": "eu-west-3", - "continent": "eu", - "airport_code": "cdg", - "cluster": "tfy-aws-cdg-gtw", - "nat_ips": [ - "13.39.61.233/32", - "15.237.200.183/32" - ] - }, - { - "type": "gateway", - "cloud": "aws", - "region": "eu-west-1", - "continent": "eu", - "airport_code": "dub", - "cluster": "tfy-aws-dub-gtw", - "nat_ips": [ - "54.72.115.226/32", - "54.246.5.151/32" - ] - }, - { - "type": "gateway", - "cloud": "gcp", - "region": "europe-southwest1", - "continent": "eu", - "airport_code": "mad", - "cluster": "tfy-gcp-mad-gtw", - "hostname": "mad.gateway.truefoundry.ai", - "nat_ips": [ - "34.175.219.109/32", - "34.175.44.33/32" - ] - }, - { - "type": "gateway", - "cloud": "aws", - "region": "eu-south-2", - "continent": "eu", - "airport_code": "mad", - "cluster": "tfy-aws-mad-gtw", - "nat_ips": [ - "51.48.180.27/32", - "15.216.64.85/32" - ] - }, - { - "type": "gateway", - "cloud": "aws", - "region": "eu-south-1", - "continent": "eu", - "airport_code": "mxp", - "cluster": "tfy-aws-mxp-gtw", - "nat_ips": [ - "51.118.102.128/32", - "18.102.33.104/32" - ] - }, - { - "type": "gateway", - "cloud": "aws", - "region": "eu-central-1", - "continent": "eu", - "airport_code": "fra", - "cluster": "tfy-aws-fra-gtw", - "nat_ips": [ - "3.65.93.12/32", - "63.181.139.3/32" - ] - }, - { - "type": "gateway", - "cloud": "aws", - "region": "eu-central-2", - "continent": "eu", - "airport_code": "zrh", - "cluster": "tfy-aws-zrh-gtw", - "nat_ips": [ - "51.96.196.104/32", - "51.96.80.198/32" - ] - }, - { - "type": "gateway", - "cloud": "azure", - "region": "swedencentral", - "continent": "eu", - "airport_code": "gvx", - "cluster": "tfy-az-gvx-gtw", - "hostname": "gvx.gateway.truefoundry.ai", - "nat_ips": [ - "4.165.129.123/32", - "4.165.99.208/32" - ] - }, - { - "type": "gateway", - "cloud": "aws", - "region": "eu-north-1", - "continent": "eu", - "airport_code": "arn", - "cluster": "tfy-aws-arn-gtw", - "nat_ips": [ - "13.48.213.79/32", - "13.62.133.235/32" - ] - }, - { - "type": "gateway", - "cloud": "aws", - "region": "af-south-1", - "continent": "af", - "airport_code": "cpt", - "cluster": "tfy-aws-cpt-gtw", - "hostname": "cpt.gateway.truefoundry.ai", - "nat_ips": [ - "13.244.129.178/32", - "13.246.115.110/32" - ] - }, - { - "type": "gateway", - "cloud": "gcp", - "region": "me-central1", - "continent": "me", - "airport_code": "dia", - "cluster": "tfy-gcp-dia-gtw", - "hostname": "dia.gateway.truefoundry.ai", - "nat_ips": [ - "34.18.22.15/32", - "34.18.192.62/32" - ] - }, - { - "type": "gateway", - "cloud": "aws", - "region": "me-south-1", - "continent": "me", - "airport_code": "bah", - "cluster": "tfy-aws-bah-gtw", - "nat_ips": [ - "157.175.127.180/32", - "157.241.96.125/32" - ] - }, - { - "type": "gateway", - "cloud": "aws", - "region": "me-central-1", - "continent": "me", - "airport_code": "dxb", - "cluster": "tfy-aws-dxb-gtw", - "nat_ips": [ - "40.172.236.59/32", - "3.29.12.147/32" - ] - }, - { - "type": "gateway", - "cloud": "aws", - "region": "il-central-1", - "continent": "me", - "airport_code": "tlv", - "cluster": "tfy-aws-tlv-gtw", - "nat_ips": [ - "51.84.87.166/32", - "51.16.181.62/32" - ] - }, - { - "type": "gateway", - "cloud": "aws", - "region": "ap-south-1", - "continent": "ap", - "airport_code": "bom", - "cluster": "tfy-aws-bom-gtw", - "hostname": "bom.gateway.truefoundry.ai", - "nat_ips": [ - "13.126.176.139/32", - "15.207.191.126/32" - ] - }, - { - "type": "gateway", - "cloud": "aws", - "region": "ap-south-2", - "continent": "ap", - "airport_code": "hyd", - "cluster": "tfy-aws-hyd-gtw", - "nat_ips": [ - "16.112.48.100/32", - "40.192.126.53/32" - ] - }, - { - "type": "gateway", - "cloud": "aws", - "region": "ap-east-1", - "continent": "ap", - "airport_code": "hkg", - "cluster": "tfy-aws-hkg-gtw", - "nat_ips": [ - "16.163.170.188/32", - "18.166.19.152/32" - ] - }, - { - "type": "gateway", - "cloud": "aws", - "region": "ap-east-2", - "continent": "ap", - "airport_code": "tpe", - "cluster": "tfy-aws-tpe-gtw", - "nat_ips": [ - "43.213.3.104/32", - "43.212.202.18/32" - ] - }, - { - "type": "gateway", - "cloud": "aws", - "region": "ap-southeast-7", - "continent": "ap", - "airport_code": "bkk", - "cluster": "tfy-aws-bkk-gtw", - "nat_ips": [ - "43.210.99.107/32", - "43.209.246.149/32" - ] - }, - { - "type": "gateway", - "cloud": "aws", - "region": "ap-southeast-3", - "continent": "ap", - "airport_code": "cgk", - "cluster": "tfy-aws-cgk-gtw", - "nat_ips": [ - "16.79.100.152/32", - "15.232.2.7/32" - ] - }, - { - "type": "gateway", - "cloud": "aws", - "region": "ap-southeast-1", - "continent": "ap", - "airport_code": "sin", - "cluster": "tfy-aws-sin-gtw", - "hostname": "sin.gateway.truefoundry.ai", - "nat_ips": [ - "13.251.96.74/32", - "18.143.12.175/32" - ] - }, - { - "type": "gateway", - "cloud": "aws", - "region": "ap-southeast-4", - "continent": "oc", - "airport_code": "mel", - "cluster": "tfy-aws-mel-gtw", - "hostname": "mel.gateway.truefoundry.ai", - "nat_ips": [ - "16.51.101.207/32", - "16.51.12.109/32" - ] - }, - { - "type": "gateway", - "cloud": "aws", - "region": "ap-southeast-6", - "continent": "oc", - "airport_code": "akl", - "cluster": "tfy-aws-akl-gtw", - "nat_ips": [ - "3.103.30.44/32", - "3.102.184.172/32" - ] - }, - { - "type": "gateway", - "cloud": "aws", - "region": "ap-southeast-5", - "continent": "ap", - "airport_code": "kul", - "cluster": "tfy-aws-kul-gtw", - "nat_ips": [ - "56.68.75.208/32", - "56.68.6.121/32" - ] - }, - { - "type": "gateway", - "cloud": "aws", - "region": "ap-southeast-2", - "continent": "oc", - "airport_code": "syd", - "cluster": "tfy-aws-syd-gtw", - "hostname": "syd.gateway.truefoundry.ai", - "nat_ips": [ - "13.237.191.204/32", - "3.105.148.45/32" - ] - }, - { - "type": "gateway", - "cloud": "aws", - "region": "ap-northeast-1", - "continent": "ap", - "airport_code": "hnd", - "cluster": "tfy-aws-hnd-gtw", - "nat_ips": [ - "54.238.73.1/32", - "18.178.26.39/32" - ] - }, - { - "type": "gateway", - "cloud": "aws", - "region": "ap-northeast-2", - "continent": "ap", - "airport_code": "icn", - "cluster": "tfy-aws-icn-gtw", - "nat_ips": [ - "43.200.144.76/32", - "43.200.33.159/32" - ] - }, - { - "type": "gateway", - "cloud": "aws", - "region": "ap-northeast-3", - "continent": "ap", - "airport_code": "kix", - "cluster": "tfy-aws-kix-gtw", - "nat_ips": [ - "15.152.62.45/32", - "15.168.90.90/32" - ] - }, - { - "type": "gateway", - "cloud": "azure", - "region": "australiacentral", - "continent": "oc", - "airport_code": "cbr", - "cluster": "tfy-az-cbr-gtw", - "hostname": "cbr.gateway.truefoundry.ai", - "nat_ips": [ - "20.28.68.101/32", - "20.53.28.45/32" - ] - } - ] -} diff --git a/eu/ip-ranges.json b/eu/ip-ranges.json new file mode 100644 index 0000000..b21d022 --- /dev/null +++ b/eu/ip-ranges.json @@ -0,0 +1,227 @@ +{ + "$schema": "../.github/schema/ip-ranges.schema.json", + "endpoints": [ + { + "type": "gateway", + "hostname": "gateway.truefoundry.ai", + "nat_ips": [ + "3.105.148.45/32", + "4.165.99.208/32", + "4.165.129.123/32", + "13.42.115.230/32", + "13.126.176.139/32", + "13.237.191.204/32", + "13.244.129.178/32", + "13.246.115.110/32", + "13.251.96.74/32", + "15.207.191.126/32", + "16.51.12.109/32", + "16.51.101.207/32", + "18.130.148.118/32", + "18.143.12.175/32", + "20.245.207.53/32", + "34.18.22.15/32", + "34.18.192.62/32", + "34.39.220.106/32", + "34.39.220.236/32", + "34.130.196.232/32", + "34.130.234.128/32", + "34.174.18.136/32", + "34.174.240.150/32", + "34.175.44.33/32", + "34.175.219.109/32", + "40.75.129.73/32", + "52.4.51.133/32", + "54.204.114.10/32" + ] + }, + { + "type": "gateway", + "hostname": "us.gateway.truefoundry.ai", + "nat_ips": [ + "20.245.207.53/32", + "34.130.196.232/32", + "34.130.234.128/32", + "34.174.18.136/32", + "34.174.240.150/32", + "40.75.129.73/32", + "52.4.51.133/32", + "54.204.114.10/32" + ] + }, + { + "type": "gateway", + "hostname": "eu.gateway.truefoundry.ai", + "nat_ips": [ + "4.165.99.208/32", + "4.165.129.123/32", + "13.42.115.230/32", + "18.130.148.118/32", + "34.18.22.15/32", + "34.18.192.62/32", + "34.175.44.33/32", + "34.175.219.109/32" + ] + }, + { + "type": "gateway", + "hostname": "sa.gateway.truefoundry.ai", + "nat_ips": [ + "34.39.220.106/32", + "34.39.220.236/32" + ] + }, + { + "type": "gateway", + "hostname": "au.gateway.truefoundry.ai", + "nat_ips": [ + "3.105.148.45/32", + "13.237.191.204/32", + "13.251.96.74/32", + "16.51.12.109/32", + "16.51.101.207/32", + "18.143.12.175/32" + ] + }, + { + "type": "gateway", + "hostname": "as.gateway.truefoundry.ai", + "nat_ips": [ + "13.126.176.139/32", + "13.251.96.74/32", + "15.207.191.126/32", + "16.51.12.109/32", + "16.51.101.207/32", + "18.143.12.175/32", + "34.18.22.15/32", + "34.18.192.62/32" + ] + }, + { + "type": "gateway", + "airport_code": "bom", + "hostname": "bom.gateway.truefoundry.ai", + "nat_ips": [ + "13.126.176.139/32", + "15.207.191.126/32" + ] + }, + { + "type": "gateway", + "airport_code": "cpt", + "hostname": "cpt.gateway.truefoundry.ai", + "nat_ips": [ + "13.244.129.178/32", + "13.246.115.110/32" + ] + }, + { + "type": "gateway", + "airport_code": "dfw", + "hostname": "dfw.gateway.truefoundry.ai", + "nat_ips": [ + "34.174.18.136/32", + "34.174.240.150/32" + ] + }, + { + "type": "gateway", + "airport_code": "dia", + "hostname": "dia.gateway.truefoundry.ai", + "nat_ips": [ + "34.18.22.15/32", + "34.18.192.62/32" + ] + }, + { + "type": "gateway", + "airport_code": "gru", + "hostname": "gru.gateway.truefoundry.ai", + "nat_ips": [ + "34.39.220.106/32", + "34.39.220.236/32" + ] + }, + { + "type": "gateway", + "airport_code": "gvx", + "hostname": "gvx.gateway.truefoundry.ai", + "nat_ips": [ + "4.165.99.208/32", + "4.165.129.123/32" + ] + }, + { + "type": "gateway", + "airport_code": "lhr", + "hostname": "lhr.gateway.truefoundry.ai", + "nat_ips": [ + "13.42.115.230/32", + "18.130.148.118/32" + ] + }, + { + "type": "gateway", + "airport_code": "mad", + "hostname": "mad.gateway.truefoundry.ai", + "nat_ips": [ + "34.175.44.33/32", + "34.175.219.109/32" + ] + }, + { + "type": "gateway", + "airport_code": "mel", + "hostname": "mel.gateway.truefoundry.ai", + "nat_ips": [ + "16.51.12.109/32", + "16.51.101.207/32" + ] + }, + { + "type": "gateway", + "airport_code": "orf", + "hostname": "orf.gateway.truefoundry.ai", + "nat_ips": [ + "52.4.51.133/32", + "54.204.114.10/32" + ] + }, + { + "type": "gateway", + "airport_code": "sfo", + "hostname": "sfo.gateway.truefoundry.ai", + "nat_ips": [ + "20.245.207.53/32", + "40.75.129.73/32" + ] + }, + { + "type": "gateway", + "airport_code": "sin", + "hostname": "sin.gateway.truefoundry.ai", + "nat_ips": [ + "13.251.96.74/32", + "18.143.12.175/32" + ] + }, + { + "type": "gateway", + "airport_code": "syd", + "hostname": "syd.gateway.truefoundry.ai", + "nat_ips": [ + "3.105.148.45/32", + "13.237.191.204/32" + ] + }, + { + "type": "gateway", + "airport_code": "yyz", + "hostname": "yyz.gateway.truefoundry.ai", + "nat_ips": [ + "34.130.196.232/32", + "34.130.234.128/32" + ] + } + ] +} diff --git a/schema/nat-ips.schema.json b/schema/nat-ips.schema.json deleted file mode 100644 index 20c3522..0000000 --- a/schema/nat-ips.schema.json +++ /dev/null @@ -1,97 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/truefoundry/nat-ips/main/schema/nat-ips.schema.json", - "title": "TrueFoundry NAT IP source file", - "description": "Schema for the hand-edited source files under data/. Built artifacts in dist/ use the same entry shape inside a flat envelope.", - "type": "object", - "required": ["plane", "control_plane", "entries"], - "additionalProperties": false, - "properties": { - "$schema": { - "type": "string", - "description": "Relative path to this schema, so editors give completion and inline validation." - }, - "plane": { - "enum": ["control_plane", "gateway"], - "description": "Which plane this file describes." - }, - "control_plane": { - "enum": ["us", "eu"], - "description": "Which TrueFoundry control plane owns these IPs." - }, - "host": { - "type": "string", - "description": "Exact hostname customers call, e.g. us.truefoundry.cloud" - }, - "host_pattern": { - "type": "string", - "description": "Wildcard hostname pattern, e.g. *.us.truefoundry.cloud" - }, - "entries": { - "type": "array", - "minItems": 1, - "items": { "$ref": "#/$defs/entry" } - } - }, - "$defs": { - "entry": { - "type": "object", - "required": ["type", "cloud", "region", "continent", "nat_ips"], - "additionalProperties": false, - "properties": { - "type": { - "enum": ["control_plane", "gateway"] - }, - "cloud": { - "enum": ["aws", "gcp", "azure"] - }, - "region": { - "type": "string", - "minLength": 1, - "description": "Cloud provider region id, e.g. us-east-1 or europe-southwest1." - }, - "continent": { - "enum": ["na", "sa", "eu", "ap", "me", "af", "oc"] - }, - "airport_code": { - "type": "string", - "pattern": "^[a-z]{3}$", - "description": "IATA code for the PoP. Required for gateway entries. Not unique: two clusters in the same metro on different clouds share a code." - }, - "hostname": { - "type": "string", - "format": "hostname", - "description": "Regional gateway endpoint customers resolve, e.g. mad.gateway.truefoundry.ai. Absent on clusters whose NAT addresses are allocated but whose endpoint is not published yet." - }, - "cluster": { - "type": "string", - "description": "Internal cluster name, so this file can be reconciled against terraform state." - }, - "notes": { - "type": "string" - }, - "nat_ips": { - "type": "array", - "minItems": 1, - "uniqueItems": true, - "items": { - "type": "string", - "pattern": "^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])/(3[0-2]|[12]?[0-9])$", - "description": "IPv4 CIDR. Single addresses are written as /32." - } - } - }, - "allOf": [ - { - "if": { - "properties": { "type": { "const": "gateway" } }, - "required": ["type"] - }, - "then": { - "required": ["airport_code"] - } - } - ] - } - } -} diff --git a/us/ip-ranges.json b/us/ip-ranges.json new file mode 100644 index 0000000..5e00f9b --- /dev/null +++ b/us/ip-ranges.json @@ -0,0 +1,4 @@ +{ + "$schema": "../.github/schema/ip-ranges.schema.json", + "endpoints": [] +} From 7b13c09f2d7ead3cda5ea7086211a0c998613962 Mon Sep 17 00:00:00 2001 From: Vedant Pareek Date: Mon, 12 Oct 2026 21:46:53 +0530 Subject: [PATCH 7/7] fixing the workflow --- .github/workflows/validate.yml | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 0f2742d..64f99c7 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -62,11 +62,21 @@ jobs: - name: Build the base branch run: | - git checkout "origin/${{ github.base_ref }}" -- eu us - uv run .github/scripts/build.py --version "${{ github.base_ref }}" + # main predates eu/ and us/. An empty baseline still produces a useful + # "everything added" comment instead of failing the checkout. mkdir -p "${RUNNER_TEMP}/old" - cp dist/eu.json dist/us.json "${RUNNER_TEMP}/old/" - git checkout HEAD -- eu us + base="origin/${{ github.base_ref }}" + if git cat-file -e "${base}:eu/ip-ranges.json" \ + && git cat-file -e "${base}:us/ip-ranges.json"; then + git checkout "$base" -- eu us + uv run .github/scripts/build.py --version "${{ github.base_ref }}" + cp dist/eu.json dist/us.json "${RUNNER_TEMP}/old/" + git checkout HEAD -- eu us + else + echo "Base branch has no eu/ or us/ ip-ranges yet; diffing against an empty list." + printf '%s\n' '{"endpoints":[]}' > "${RUNNER_TEMP}/old/eu.json" + printf '%s\n' '{"endpoints":[]}' > "${RUNNER_TEMP}/old/us.json" + fi - name: Render diff run: |