Skip to content
IP Intelligence

Detect iCloud Private Relay and Cloudflare WARP, Then Let Them Through

20 min readGeoIPHub Team

We downloaded Apple's list at 09:40 UTC on 12 October 2026. It held 285,091 prefixes, 41,734 IPv4 and 243,357 IPv6, across 235 country codes. Inside Cloudflare's 104.28.0.0/16, 19,723 of the 65,536 addresses are on it. The other 45,813 are not, so a rule written against that block, or against Cloudflare's whole network, cannot tell a Private Relay user from anything else routed there.

Below we profile Apple's file and give a tested Python matcher for IPv4 and IPv6. We explain what can and cannot be known about WARP, and show real lookups of relay addresses, including two places where our own geolocation disagrees with Apple's. The post ends with a policy table. For the wider set of anonymizer signals, start with how VPN and proxy detection works.

How to detect iCloud Private Relay: match Apple's own list

Apple publishes every Private Relay egress prefix in one CSV, so an exact match tells you an IP is a relay egress. Apple's guide for network and server operators points servers to that list, or to a provider flag:

"Your server can recognize traffic from Private Relay by using the above list of Private Relay IP addresses or by checking how it's categorized by your geo IP database provider."

The same page names the fields to look for: "Other relevant fields may include: 'is_relay', 'is_hosting', 'privacy_service', or 'privacy_proxy'." In other words, Apple expects you to use either its list or a field like the one our API returns.

The list is also clean in a way most IP lists are not. Apple's December 2021 Private Relay overview says relay IP addresses "are not used or shared for any purpose other than to provide the Private Relay service." So an exact match has no collateral. Nobody else's traffic leaves from those addresses.

What is in Apple's egress CSV

The file lives at https://mask-api.icloud.com/egress-ip-ranges.csv. We downloaded it and parsed every row with Python's ipaddress module.

MeasureValue (fetched 2026-10-12, 09:40 UTC)
Rows (prefixes)285,091
IPv4 prefixes41,734
IPv6 prefixes243,357
IPv4 addresses covered105,647
Distinct country codes235
Columns5: prefix, country, region, city, and an empty trailing field
File size12,047,231 bytes
Overlapping prefixesNone

Each row is a geofeed line. Here are two real rows from the file:

GeoIPHub API
172.224.226.0/27,GB,GB-EN,London, 104.28.46.99/32,ZM,,Lusaka,

The second row has an empty region, so your parser has to accept blanks. One counting trap: wc -l prints 285,090, one short, because the file has no trailing newline. Count parsed rows instead.

Polling the file is cheap. The server returned an ETag, "6acc40fb-b7d37f", and answered our conditional request with 304 Not Modified. Apple does not say how often it updates the list, so we won't guess. Pick a refresh schedule that suits you.

Why an ASN or /16 rule is not enough

Relay egress shares networks with other traffic. In 104.28.0.0/16, 19,723 of 65,536 addresses are on Apple's list, and the rest are not. Apple's overview says the second relay "is operated by third-party partners who are some of the largest content delivery networks (CDNs) in the world." The three relays we sampled sit on two of them, Cloudflare (AS13335) and Akamai (AS36183).

Blocking AS13335 hits far more than relay users. Allowlisting it lets far more than relays through. The prefix match is the precise tool, and you can check any IP for VPN or relay to see how a single address is classified.

Why a relay IP is not a VPN IP

Private Relay hides the user's IP from the site but keeps them in their real region, covers only some traffic, and is limited to validated Apple devices on paid accounts. That is why it should not be scored like a VPN.

The design splits knowledge across two relays run by different companies. Apple runs the first, which sees the user's address but not the site. A partner runs the second, which sees the site but not the user. Apple's operator page says that, this way, no single party, Apple included, can see the details of a user's browsing.

HopSees the user's IPSees which site
User's iPhone, iPad or MacYesYes
First relay (Apple)YesNo
Second relay (a partner network, e.g. Cloudflare or Akamai)NoYes
Your serverNo, only the relay IPIt is the site

Four lines from Apple's operator page change how you should treat these addresses:

  • Scope. "Private Relay protects users' web browsing in Safari, DNS resolution queries, and insecure http app traffic." A relay IP does not mean a Safari session. An app making plain http requests arrives the same way.
  • Region. "Private Relay preserves the region the user is in, so your server can trust the region assigned to the IP address it sees." The 2021 overview adds that it "does not allow users to represent themselves as connecting from a different country or region."
  • Validation. "All connections that use Private Relay validate that the client is an iPhone, iPad, or Mac and that the customer has a valid iCloud+ subscription."
  • Stability. "The relay IP address will remain stable during a browsing session while a user is interacting with your website."

A VPN lets the user pick any country and covers the whole device. Cloudflare is candid about its own product. "Technically, WARP is a VPN," it wrote when it launched WARP. We come back to WARP below.

One practical trap: some relay egress lives in data centers, so hosting flags can fire. Our lookup of 172.224.226.1, a relay on Apple's list, returns is_hosting: true. That is why relays show up as datacenter IPs, and why a blanket hosting block catches Apple customers.

Match an IP against Apple's list in Python

Load the CSV into sorted integer ranges, one list per IP version, and binary-search them. Refresh with a conditional request, and keep the last good copy if a download fails or looks wrong.

This is the exact script we ran on 2026-10-12, on Python 3.9.6. It uses only the standard library.

GeoIPHub API
"""Match an IP against Apple's iCloud Private Relay egress list. Standard library only.""" import bisect, csv, ipaddress, os, tempfile, time, urllib.error, urllib.request URL = "https://mask-api.icloud.com/egress-ip-ranges.csv" CSV_PATH, ETAG_PATH, MIN_ROWS = "egress.csv", "egress.etag", 100_000 def load(path): """Parse the CSV into one sorted range list per IP version, then validate it.""" ranges = {4: [], 6: []} with open(path, newline="") as f: for row in csv.reader(f): if len(row) != 5: raise ValueError(f"bad row: {row}") net = ipaddress.ip_network(row[0], strict=False) ranges[net.version].append((int(net.network_address), int(net.broadcast_address), *row[:4])) index = {} for version, rs in ranges.items(): rs.sort() if any(b[0] <= a[1] for a, b in zip(rs, rs[1:])): raise ValueError(f"overlapping IPv{version} prefixes") index[version] = ([r[0] for r in rs], rs) return index, len(ranges[4]) + len(ranges[6]) def refresh(index=None, rows=0): """Fetch a new copy only if it changed. On any failure, keep serving the last good one.""" headers = {} if index and os.path.exists(ETAG_PATH): headers["If-None-Match"] = open(ETAG_PATH).read().strip() fd, tmp = tempfile.mkstemp(dir=".", suffix=".part") os.close(fd) try: with urllib.request.urlopen(urllib.request.Request(URL, headers=headers), timeout=60) as resp: with open(tmp, "wb") as out: out.write(resp.read()) etag = resp.headers.get("ETag", "") new_index, new_rows = load(tmp) if new_rows < max(MIN_ROWS, 0.9 * rows): raise ValueError(f"only {new_rows} rows, keeping the old file") os.replace(tmp, CSV_PATH) # atomic swap with open(ETAG_PATH, "w") as f: f.write(etag) print(f"downloaded new list: {new_rows:,} prefixes") return new_index, new_rows except urllib.error.HTTPError as e: print("not modified, keeping current list" if e.code == 304 else f"refresh failed: {e}") except Exception as e: print(f"refresh failed: {e}") finally: if os.path.exists(tmp): os.remove(tmp) return index, rows def match(index, ip): """Return Apple's row for a relay egress IP, or None.""" try: addr = ipaddress.ip_address(ip.strip()) except (ValueError, AttributeError): return None if addr.version == 6 and addr.ipv4_mapped: addr = addr.ipv4_mapped # ::ffff:104.28.46.99 becomes 104.28.46.99 starts, rs = index[addr.version] n = int(addr) i = bisect.bisect_right(starts, n) - 1 if i >= 0 and n <= rs[i][1]: return dict(zip(("prefix", "country", "region", "city"), rs[i][2:])) return None if __name__ == "__main__": t0 = time.time() index, rows = load(CSV_PATH) if os.path.exists(CSV_PATH) else (None, 0) index, rows = refresh(index, rows) print(f"{rows:,} prefixes ready in {time.time() - t0:.1f}s") for ip in ["104.28.46.99", "172.224.226.1", "2a02:26f7:c880::1", "::ffff:104.28.28.41", "162.159.192.1", "102.129.235.17", "not-an-ip"]: print(f"{ip:22} {match(index, ip)}")

The first run, at 09:41 UTC, downloaded the file and printed:

GeoIPHub API
downloaded new list: 285,091 prefixes 285,091 prefixes ready in 12.8s 104.28.46.99 {'prefix': '104.28.46.99/32', 'country': 'ZM', 'region': '', 'city': 'Lusaka'} 172.224.226.1 {'prefix': '172.224.226.0/27', 'country': 'GB', 'region': 'GB-EN', 'city': 'London'} 2a02:26f7:c880::1 {'prefix': '2a02:26f7:c880::/54', 'country': 'GB', 'region': 'GB-EN', 'city': 'London'} ::ffff:104.28.28.41 {'prefix': '104.28.28.41/32', 'country': 'AU', 'region': 'AU-SA', 'city': 'Adelaide'} 162.159.192.1 None 102.129.235.17 None not-an-ip None

162.159.192.1 is a WARP connection address and 102.129.235.17 is a commercial VPN server, so neither should match, and neither did. We also tested the failure paths:

  • Unchanged file. The second run sent the saved ETag, got a 304 and printed "not modified, keeping current list". Loading and indexing the local file took 4.3 seconds, so build the index once at startup, not per request.
  • Bad download. We pointed URL at a path that does not exist (Apple's server answered 400). Separately, we raised the minimum row count above the real one. Both times the script logged the failure, deleted its temp file and kept answering from the old copy.
  • Mapped addresses. ::ffff:104.28.28.41 matched its IPv4 row. Dual-stack servers often log IPv4 clients in that form.

Because the file has no overlapping prefixes, one binary search per lookup is enough. The loader refuses any future file that has overlaps, and the refresh then falls back to the last good copy.

How to detect Cloudflare WARP, and why there is no list

You cannot match WARP against a public list, because Cloudflare does not publish one. WARP can only be identified by a provider that labels ranges it knows, or on the user's own device.

Cloudflare's egress policy documentation is plain about it: "Cloudflare does not publish Cloudflare One Client egress IP ranges." The "WARP IP ranges" that circulate in forum threads are something else. Cloudflare's firewall guide says of 162.159.192.0/24: "This IP is used for the consumer WARP client (1.1.1.1 with WARP) and is not required for Zero Trust services." That is where the WARP app connects. Your web server never sees those addresses as client IPs.

Does the site get the user's real address some other way? Not in normal browsing. Cloudflare's WARP FAQ says: "1.1.1.1 + WARP replaces your original IP address with a Cloudflare IP that consistently and accurately represents your approximate location. This happens regardless of whether the site is on the Cloudflare network or not."

The same FAQ documents two leaks. "WARP does not proxy WebRTC traffic." And: "If you grant a website access to your microphone or camera, traffic will bypass WARP and your original IP address will be visible."

On the device itself, https://www.cloudflare.com/cdn-cgi/trace prints a warp= field. On our machine, which does not run WARP, it read warp=off. Only the person who opens that page sees it, so it helps a support team debugging a customer's connection. It does nothing for a server scoring a visitor.

Our API returns relay_provider: "Cloudflare WARP" for Cloudflare's published WARP connection ranges. A lookup of 162.159.192.1 returned is_relay: true, relay_provider: "Cloudflare WARP", is_hosting: true and is_anycast: true, with a score of 24 and allow. That is the address WARP apps connect to, not one your server will see as a visitor.

What our API returns for Cloudflare addresses not on Apple's list

Cloudflare privacy-network egress addresses that are not on Apple's list come back labelled "Cloudflare", while Cloudflare's published WARP connection ranges come back "Cloudflare WARP". Here is 104.28.210.10. It sits in the same /16 as two of our Apple relays but is not on Apple's list. This is a trimmed copy of our lookup at 09:40 UTC on 2026-10-12, values unedited:

GeoIPHub API
{ "ip": "104.28.210.10", "asn": { "asn": 13335, "org": "CLOUDFLARENET - Cloudflare, Inc.", "user_type": "consumer_privacy_network" }, "geo": { "country_code": "CA", "city": "Toronto" }, "detection": { "is_vpn": false, "is_hosting": false, "is_relay": true, "is_anonymous": false, "relay_provider": "Cloudflare" }, "scoring": { "fraud_score": 9, "recommended_action": "allow", "detection_methods": ["relay_ip"] } }
IPOn Apple's list?relay_providerOther fieldsScore, action, methods
104.28.210.10NoCloudflareAS13335, consumer_privacy_network, is_vpn false, is_hosting false9, allow, relay_ip
104.28.28.41Yes (AU, Adelaide)iCloud Private RelayAS13335, consumer_privacy_network9, allow, relay_ip
162.159.192.1No (WARP connection address)Cloudflare WARPis_hosting true, is_anycast true24, allow, datacenter_ip + no_ptr_datacenter + relay_ip
102.129.235.17 (VPN, for contrast)Nononeis_vpn true, is_anonymous true92, block, known_vpn_server + openvpn_open + ipsec_open

Read "Cloudflare" as "a Cloudflare consumer-privacy egress address not attributed to Apple". It does not mean a confirmed WARP user, and we do not claim it does. Our API reference lists only two relay_provider values today, "iCloud Private Relay" and "Cloudflare WARP", so this third value is currently undocumented. We have asked the backend team to document it.

The useful point sits in the first two rows. Inside one Cloudflare /16, the exact CSV match separates Apple's relay (104.28.28.41) from other Cloudflare privacy egress (104.28.210.10). Our label agrees with Apple's list on both. Both score 9 and allow, so your policy is the same either way.

Private Relay and WARP side by side

QuestioniCloud Private RelayCloudflare WARP
Who runs itApple, with partner networks for the second relayCloudflare
Who can use itiCloud+ subscribers on validated iPhone, iPad or MacAnyone who installs the free 1.1.1.1 app
What it coversSafari, DNS queries, insecure http app trafficThe device's traffic, except WebRTC and sites given camera or microphone access
Public egress listYes, Apple's CSVNo
LocationRegion kept; coarse city by default, broader if the user choosesA Cloudflare IP that represents the user's approximate location
How a server identifies itExact match against Apple's CSV, or a provider's relay flagA provider's label only

Both can sit on AS13335, so the network alone separates neither.

What our lookups return for relay IPs

In our lookups, Private Relay addresses come back as relays, not as anonymous or VPN, and land in allow unless other evidence exists. We traced 104.28.46.99 end to end. Apple's row says Zambia, Lusaka. This is the request, as you would send it with a key:

GeoIPHub API
curl https://api.geoiphub.com/v1/lookup/104.28.46.99 \ -H "x-api-key: YOUR_API_KEY"

We ran it without a key at 09:40 UTC on 2026-10-12, which the API allows for light use. Here is a trimmed copy of the response, values unedited:

GeoIPHub API
{ "ip": "104.28.46.99", "asn": { "asn": 13335, "org": "CLOUDFLARENET - Cloudflare, Inc.", "asn_type": "unknown", "user_type": "consumer_privacy_network" }, "geo": { "country_code": "AO", "region": "Luanda", "city": "Luanda", "geo_confidence": 0.8 }, "detection": { "is_proxy": false, "is_vpn": false, "is_tor": false, "is_hosting": false, "is_relay": true, "is_anonymous": false, "anonymity_level": "medium", "relay_provider": "iCloud Private Relay" }, "scoring": { "fraud_score": 9, "recommended_action": "allow", "detection_methods": ["relay_ip"] } }

The relay is named, is_vpn and is_anonymous are false, and scoring.detection_methods shows the one signal that fired: relay_ip. The score is 9, so the action is allow. The geolocation says Angola, which Apple's row contradicts; we deal with that in the next section. The decision for this address is to allow it, rate-limit it as a shared address, and take its location from Apple's row.

Here are the other relays next to a real VPN:

IPApple's rowOur result on 2026-10-12
172.224.226.1GB, GB-EN, LondonAS36183 Akamai, asn_type hosting; is_relay true, iCloud Private Relay, is_hosting true; score 24, allow; datacenter_ip, no_ptr_datacenter, relay_ip; geo GB, London
104.28.28.41AU, AU-SA, AdelaideAS13335; is_relay true, iCloud Private Relay; score 9, allow; relay_ip; geo AU, Sydney, 0.8
102.129.235.17 (VPN)Not on the listAS834 IPXO; is_vpn true, is_anonymous true; score 92, block; known_vpn_server, openvpn_open, ipsec_open

The relay flag does not lower the score. It adds to it. On 104.28.46.99, relay_ip is the only entry in detection_methods, so it accounts for the whole score of 9. 172.224.226.1 scores 24 because two datacenter signals fired as well, which leaves it one point inside the allow limit of 25. So relays land in allow unless other evidence exists. If something else counts against the address, such as an abuse listing, the score rises and the action can move to review. The how the score is built page lists the signals.

The API labels the relay. Apple's CSV is your independent check, and the matcher above runs it without us. Scoring a relay as if it were a VPN is the classic case in VPN detection false positives.

Why Private Relay users show the wrong city, including in our data

A relay IP only promises the user's region, and a coarse city by default. The only authoritative location for a relay IP is Apple's own row, and our database does not always agree with it.

Apple's operator page says the relay IP "accurately represents the client's coarse city-level location by default". Users can widen that. Apple's current iPhone User Guide lists two IP Address Location options, "Maintain General Location" and "Use Country and Time Zone". The 2021 overview describes them. The first picks addresses that "map to a roughly city-level area consistent with where the user is actually connecting from". The second picks addresses "across a broader, more regional area", and "All Relay IP addresses will still map to the user's original country and time zone."

We compared our geolocation with Apple's rows for the three relays:

IPApple's rowOur geoAgree?
104.28.46.99ZM, LusakaAO, Luanda (geo_confidence 0.8)No, different country
104.28.28.41AU, AU-SA, AdelaideAU, Sydney (geo_confidence 0.8)Country yes, city no
172.224.226.1GB, GB-EN, LondonGB, LondonYes

Two of the three disagree. Zambia against Angola is a country-level miss, the kind that trips a licensing or sanctions rule. Adelaide against Sydney gets the country right and the city wrong. Three IPs are not a benchmark, but they are enough to set a rule: for relay IPs, take location from Apple's row, not from any geo database, ours included. Decide on country or region, and treat the city as a hint. We have logged both mismatches with our backend team.

Should you block Private Relay or WARP? Let them through and score the session

No. Apple validates every relay client and says IP-based fraud rules "might need to be adjusted", so allow Private Relay traffic, rate-limit it like a shared carrier address, and step up only when other evidence points to risk.

Apple's operator page is direct about both points:

"Traditional fraud detection that relies on IP addresses might need to be adjusted to ensure legitimate users aren't impacted."

"Consider treating these addresses like larger carrier-grade NAT or enterprise IP addresses to better account for this type of traffic, since many Private Relay users may be assigned to a single relay IP address."

Apple also offers a replacement for the challenge page. It notes that "Common mitigations include CAPTCHAs or blocking traffic altogether," and says "Every Apple device running iOS 16, iPadOS 16, or macOS Monterey and later supports Private Access Tokens," which let a server check for a legitimate device without identifying the user.

Can fraudsters use relays? They can, but Private Relay needs an Apple device and a paid iCloud+ account. Apple says the validation "is designed to ensure that only valid Apple devices and accounts in good standing are allowed to use Private Relay." That makes it a costly way to farm accounts. WARP is a free app, so give WARP sessions a little more weight on your other signals, not a block. Harder IP-hiding problems sit elsewhere, such as mobile proxies.

One detail for your rate limiter: Apple's carrier-grade NAT advice is about how you treat the address, not about a flag. Our threat.is_cgnat was false on all three relay lookups. Key the shared-address handling on is_relay or the CSV match instead. To check an address from your own logs, paste it into the free IP lookup tool.

What you seeActionWhy
IP in Apple's CSV or is_relay true, no other risk evidenceAllow; rate-limit per account or session, not per IPApple validates device and account, and many users share one relay IP
Relay or WARP IP plus a risky account signal (new account, payment country outside the IP's region, signup velocity)Step up: a Private Access Token where supported, otherwise 2FA or an email check, not a CAPTCHA wallThe IP adds no evidence; the account signal does
Relay IP that also lands in review, for example because it is on an abuse listReviewThat evidence is about the address, not the relay
WARP-labelled IPAllow and score the sessionIt is free and common; it hides the user but does not prove intent
Location rules (licensing, tax) on relay trafficDecide on country or region from Apple's row, never cityApple keeps the region; the city is coarse or broader by setting

Three checks to run on your own logs this week:

  • Count the relay users you turned away. Join the last 30 days of signups, logins and declines against Apple's CSV with the matcher above. How many relay users did your rules decline or challenge?
  • Find ASN and hosting blocks. Search your rules for AS13335, AS36183 or is_hosting. 172.224.226.1 is a relay with is_hosting: true, so a hosting block hits relay users.
  • Check per-IP limits and city geofences. Apple says to treat relay IPs like carrier-grade NAT, so a per-IP limit sized for one household will throttle many users, and a city-level fence will misfire on a coarse location.

Frequently Asked Questions

How do I know if iCloud Private Relay is on?

On an iPhone, open Settings, tap your name, then iCloud, then Private Relay. As a site operator you cannot see the setting, only the IP: if it matches Apple's egress list, that request came through Private Relay.

Is iCloud Private Relay a VPN?

No. It covers Safari, DNS and insecure http app traffic only, keeps the user in their own country and region, and is limited to validated Apple devices on iCloud+.

Does Cloudflare WARP hide your IP address?

Yes. Cloudflare says WARP replaces the original IP with a Cloudflare IP whether or not the site is on Cloudflare, except for WebRTC traffic and sites given camera or microphone access.

Where is the list of iCloud Private Relay IP addresses?

Apple publishes it at mask-api.icloud.com/egress-ip-ranges.csv. On 2026-10-12 it held 285,091 IPv4 and IPv6 prefixes across 235 country codes.

Can a website see the real IP behind Private Relay?

Not by design. The site sees only the relay IP, and only Apple's first relay sees the user's own address.

Should I block iCloud Private Relay?

No. Apple says IP-based fraud rules may need adjusting for relay users. Allow it, rate-limit it like carrier-grade NAT, and step up only on other risk signals.