diff --git a/.github/pages/index.html b/.github/pages/index.html new file mode 100644 index 0000000..1c762b6 --- /dev/null +++ b/.github/pages/index.html @@ -0,0 +1,188 @@ + + + + + + TrueFoundry IP ranges + + + + +
+
+

TrueFoundry

+

IP ranges

+

Outbound 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.

+
+
+
+
+

Download

+
+ +
+

US control plane

+

Loading…

+ +
+
+ +

Which hostname to allowlist

+

Find the gateway you connect to in the globally distributed SaaS gateway docs. nat_ips on that hostname is the full allowlist, including the documented fallback region.

+ + + + + + + + + + + + +
HostnameAddresses from
gateway.truefoundry.aiEvery regional gateway
us.gateway.truefoundry.aiNorth Virginia, San Francisco, Dallas, and Toronto
eu.gateway.truefoundry.aiLondon, Madrid, Gavle, and Doha
sa.gateway.truefoundry.aiSao Paulo
au.gateway.truefoundry.aiSydney, Melbourne, and Singapore
as.gateway.truefoundry.aiMumbai, Singapore, Doha, and Melbourne
+ +

When addresses change

+

A new address is announced in a GitHub Release and starts carrying traffic 15 days later. Allowlist it during those 15 days. When an address is removed, drop it after that release. Pin a version from the release assets instead of these always-latest files.

+ +
+
+ + + diff --git a/.github/schema/ip-ranges.schema.json b/.github/schema/ip-ranges.schema.json index 6a54469..3ce86b5 100644 --- a/.github/schema/ip-ranges.schema.json +++ b/.github/schema/ip-ranges.schema.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://raw.githubusercontent.com/truefoundry/ip-ranges/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.", + "description": "Outbound NAT addresses for one control plane. The directory (global or us) is the control plane.", "type": "object", "required": ["endpoints"], "additionalProperties": false, @@ -31,8 +31,8 @@ }, "hostname": { "type": "string", - "format": "hostname", - "description": "The hostname a customer connects to. nat_ips is the full allowlist for this hostname." + "pattern": "^(\\*\\.)?(?!-)[a-z0-9-]{1,63}(?=3.11" # dependencies = [] # /// -"""Copy eu/ and us/ into the release artifacts in dist/. +"""Copy global/ and us/ into the release artifacts in dist/. Run with: uv run .github/scripts/build.py --version 2026.10.12 """ @@ -18,7 +18,7 @@ ROOT = Path(__file__).resolve().parents[2] # .github/scripts/build.py -> repo root DIST_DIR = ROOT / "dist" -PLANES = ("eu", "us") +PLANES = ("global", "us") REPO = "https://github.com/truefoundry/ip-ranges" diff --git a/.github/scripts/release_notes.py b/.github/scripts/release_notes.py index 008bca9..41ba5fb 100644 --- a/.github/scripts/release_notes.py +++ b/.github/scripts/release_notes.py @@ -6,7 +6,7 @@ """Diff published IP range files and write release notes. 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 +Run with: uv run .github/scripts/release_notes.py --version 2026.10.12 --global dist/global.json --us dist/us.json """ from __future__ import annotations @@ -20,7 +20,7 @@ REPO = "https://github.com/truefoundry/ip-ranges" PAGES = "https://truefoundry.github.io/ip-ranges" -PLANES = ("eu", "us") +PLANES = ("global", "us") ACTIVATION_DAYS = 15 @@ -83,17 +83,17 @@ def render_hostname_changes(plane: str, hostname: str, added: list[str], removed def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--version", required=True) - parser.add_argument("--eu", required=True, type=Path) + parser.add_argument("--global", required=True, type=Path, dest="global_doc") parser.add_argument("--us", required=True, type=Path) - parser.add_argument("--old-eu", type=Path) + parser.add_argument("--old-global", 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_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 + new_docs = {"global": load_doc(args.global_doc), "us": load_doc(args.us)} + has_baseline = args.old_global is not None or args.old_us is not None old_docs = { - "eu": load_doc(args.old_eu) if has_baseline else {"endpoints": []}, + "global": load_doc(args.old_global) if has_baseline else {"endpoints": []}, "us": load_doc(args.old_us) if has_baseline else {"endpoints": []}, } @@ -165,7 +165,7 @@ def main() -> int: ) lines += [ "- Plain text, one CIDR per line: swap `.json` for `.txt`.", - f"- Always-latest: {PAGES}/eu.json", + f"- Always-latest: {PAGES}/global.json", "", ] diff --git a/.github/scripts/validate.py b/.github/scripts/validate.py index c39e913..efdc685 100644 --- a/.github/scripts/validate.py +++ b/.github/scripts/validate.py @@ -3,7 +3,7 @@ # requires-python = ">=3.11" # dependencies = ["jsonschema>=4.21"] # /// -"""Validate eu/ip-ranges.json and us/ip-ranges.json. +"""Validate global/ip-ranges.json and us/ip-ranges.json. Run with: uv run .github/scripts/validate.py """ @@ -20,9 +20,9 @@ ROOT = Path(__file__).resolve().parents[2] # .github/scripts/validate.py -> repo root SCHEMA_PATH = ROOT / ".github" / "schema" / "ip-ranges.schema.json" -PLANES = ("eu", "us") +PLANES = ("global", "us") -HOSTNAME_RE = re.compile(r"^(?!-)[a-z0-9-]{1,63}(? list[dict]: return [] endpoints = doc["endpoints"] - if plane == "eu" and not endpoints: - error(rel, "EU publishes gateway addresses; endpoints is empty") + if plane == "global" and not endpoints: + error(rel, "global publishes gateway addresses; endpoints is empty") if endpoints != sorted(endpoints, key=endpoint_sort_key): error(rel, "endpoints must be ordered control plane, then geolocated gateways, then airport code") @@ -167,6 +167,8 @@ def validate_file(plane: str, validator: Draft202012Validator) -> list[dict]: hostname = endpoint["hostname"] if not HOSTNAME_RE.match(hostname): error(where, f"hostname {hostname!r} is not a valid lowercase FQDN") + if hostname.startswith("*.") and endpoint["type"] != "control_plane": + error(where, "only a control plane hostname may start with *.") if hostname in seen_hosts: error(where, f"hostname {hostname!r} is already used by endpoints[{seen_hosts[hostname]}]") diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ea144ac..65c0903 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,10 +4,12 @@ on: push: branches: [main] paths: - - "eu/**" + - "global/**" - "us/**" - ".github/schema/**" - ".github/scripts/**" + - ".github/pages/**" + - ".github/workflows/release.yml" workflow_dispatch: inputs: dry_run: @@ -42,9 +44,9 @@ jobs: - name: Refuse to publish placeholder data run: | - 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/ + if grep -rqi 'PLACEHOLDER' global/ us/; then + echo "::error::global/ or us/ still contains PLACEHOLDER entries. Replace them with real IPs before releasing." + grep -rin 'PLACEHOLDER' global/ us/ exit 1 fi @@ -70,14 +72,20 @@ jobs: run: | PREV="$(gh release list --limit 1 --json tagName --jq '.[0].tagName // empty')" 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 + if [ -n "$PREV" ] && gh release download "$PREV" --pattern global.json --dir "${RUNNER_TEMP}/old" 2>/dev/null; then echo "tag=$PREV" >> "$GITHUB_OUTPUT" echo "Comparing against $PREV" + elif [ -n "$PREV" ] && gh release download "$PREV" --pattern eu.json --dir "${RUNNER_TEMP}/old" 2>/dev/null; then + mv "${RUNNER_TEMP}/old/eu.json" "${RUNNER_TEMP}/old/global.json" + echo "tag=$PREV" >> "$GITHUB_OUTPUT" + echo "Comparing against $PREV (published as eu.json)" else echo "tag=" >> "$GITHUB_OUTPUT" echo "No previous release to compare against" fi + if [ -n "$PREV" ]; then + gh release download "$PREV" --pattern us.json --dir "${RUNNER_TEMP}/old" 2>/dev/null || true + fi - name: Build artifacts run: uv run .github/scripts/build.py --version "${{ steps.version.outputs.version }}" @@ -87,12 +95,12 @@ jobs: run: | ARGS=( --version "${{ steps.version.outputs.version }}" - --eu dist/eu.json + --global dist/global.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") + if [ -f "${RUNNER_TEMP}/old/global.json" ]; then + ARGS+=(--old-global "${RUNNER_TEMP}/old/global.json") fi if [ -f "${RUNNER_TEMP}/old/us.json" ]; then ARGS+=(--old-us "${RUNNER_TEMP}/old/us.json") @@ -126,23 +134,8 @@ jobs: if: ${{ !inputs.dry_run }} run: | mkdir -p site - cp dist/eu.json dist/us.json dist/eu.txt dist/us.txt site/ - cat > site/index.html << 'EOF' - - - - - TrueFoundry IP ranges - - -

TrueFoundry IP ranges

- - - - EOF + cp dist/global.json dist/us.json dist/global.txt dist/us.txt site/ + cp .github/pages/index.html site/index.html - name: Upload the GitHub Pages artifact if: ${{ !inputs.dry_run }} diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 64f99c7..c43e6c4 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -3,7 +3,7 @@ name: Validate on: pull_request: paths: - - "eu/**" + - "global/**" - "us/**" - ".github/schema/**" - ".github/scripts/**" @@ -11,7 +11,7 @@ on: push: branches: [main] paths: - - "eu/**" + - "global/**" - "us/**" - ".github/schema/**" - ".github/scripts/**" @@ -58,23 +58,32 @@ jobs: - name: Build this branch run: | uv run .github/scripts/build.py --version "pr-${{ github.event.pull_request.number }}" - cp dist/eu.json dist/us.json "${RUNNER_TEMP}/" + cp dist/global.json dist/us.json "${RUNNER_TEMP}/" - name: Build the base branch run: | - # main predates eu/ and us/. An empty baseline still produces a useful - # "everything added" comment instead of failing the checkout. + # Older bases used eu/ instead of global/. An empty baseline still produces a + # useful "everything added" comment instead of failing the checkout. mkdir -p "${RUNNER_TEMP}/old" base="origin/${{ github.base_ref }}" - if git cat-file -e "${base}:eu/ip-ranges.json" \ + if git cat-file -e "${base}:global/ip-ranges.json" \ + && git cat-file -e "${base}:us/ip-ranges.json"; then + git checkout "$base" -- global us + uv run .github/scripts/build.py --version "${{ github.base_ref }}" + cp dist/global.json dist/us.json "${RUNNER_TEMP}/old/" + git checkout HEAD -- global us + elif git cat-file -e "${base}:eu/ip-ranges.json" \ && git cat-file -e "${base}:us/ip-ranges.json"; then git checkout "$base" -- eu us + mkdir -p global + cp eu/ip-ranges.json global/ip-ranges.json 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 + cp dist/global.json dist/us.json "${RUNNER_TEMP}/old/" + git checkout HEAD -- global us + rm -rf eu 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" + echo "Base branch has no global/ or us/ ip-ranges yet; diffing against an empty list." + printf '%s\n' '{"endpoints":[]}' > "${RUNNER_TEMP}/old/global.json" printf '%s\n' '{"endpoints":[]}' > "${RUNNER_TEMP}/old/us.json" fi @@ -82,9 +91,9 @@ jobs: run: | uv run .github/scripts/release_notes.py \ --version "pr-${{ github.event.pull_request.number }}" \ - --eu "${RUNNER_TEMP}/eu.json" \ + --global "${RUNNER_TEMP}/global.json" \ --us "${RUNNER_TEMP}/us.json" \ - --old-eu "${RUNNER_TEMP}/old/eu.json" \ + --old-global "${RUNNER_TEMP}/old/global.json" \ --old-us "${RUNNER_TEMP}/old/us.json" \ --out "${RUNNER_TEMP}/diff.md" diff --git a/README.md b/README.md index 8a1b998..19d3f4e 100644 --- a/README.md +++ b/README.md @@ -6,17 +6,17 @@ The directory is the control plane: | Control plane | List | | --- | --- | -| EU | [`eu/ip-ranges.json`](eu/ip-ranges.json) | +| Global | [`global/ip-ranges.json`](global/ip-ranges.json) | | US | [`us/ip-ranges.json`](us/ip-ranges.json) | -Always-latest URLs are on GitHub Pages: +The EU control plane is the global control plane. Always-latest URLs are on GitHub Pages: -- https://truefoundry.github.io/ip-ranges/eu.json +- https://truefoundry.github.io/ip-ranges/global.json - https://truefoundry.github.io/ip-ranges/us.json Replace `.json` with `.txt` for one CIDR per line. -Each [GitHub Release](https://github.com/truefoundry/ip-ranges/releases) attaches the same lists, so a version can be pinned: `https://github.com/truefoundry/ip-ranges/releases/download/v2026.10.12/eu.json`. +Each [GitHub Release](https://github.com/truefoundry/ip-ranges/releases) attaches the same lists, so a version can be pinned: `https://github.com/truefoundry/ip-ranges/releases/download/v2026.10.12/global.json`. ## What to allowlist @@ -27,7 +27,7 @@ Find the hostname you connect to. `nat_ips` is every address that hostname can c "endpoints": [ { "type": "control_plane", - "hostname": "eu.truefoundry.cloud", + "hostname": "*.truefoundry.cloud", "nat_ips": ["203.0.113.10/32"] }, { @@ -56,8 +56,6 @@ Find the hostname you connect to. `nat_ips` is every address that hostname can c | `au.gateway.truefoundry.ai` | Sydney, Melbourne, and Singapore | | `as.gateway.truefoundry.ai` | Mumbai, Singapore, Doha, and Melbourne | -`us/ip-ranges.json` is empty until the US control plane publishes egress addresses. - ## When addresses change Address changes are published as a GitHub Release. diff --git a/eu/ip-ranges.json b/global/ip-ranges.json similarity index 96% rename from eu/ip-ranges.json rename to global/ip-ranges.json index b21d022..df6f7d6 100644 --- a/eu/ip-ranges.json +++ b/global/ip-ranges.json @@ -1,6 +1,14 @@ { "$schema": "../.github/schema/ip-ranges.schema.json", "endpoints": [ + { + "type": "control_plane", + "hostname": "*.truefoundry.cloud", + "nat_ips": [ + "52.50.208.12/32", + "52.57.120.24/32" + ] + }, { "type": "gateway", "hostname": "gateway.truefoundry.ai", diff --git a/us/ip-ranges.json b/us/ip-ranges.json index 5e00f9b..c89e865 100644 --- a/us/ip-ranges.json +++ b/us/ip-ranges.json @@ -1,4 +1,12 @@ { "$schema": "../.github/schema/ip-ranges.schema.json", - "endpoints": [] + "endpoints": [ + { + "type": "control_plane", + "hostname": "*.us.truefoundry.cloud", + "nat_ips": [ + "32.188.54.155/32" + ] + } + ] }