Detect whether an IP address belongs to a major cloud provider, with no API call per lookup.
This library turns the IP range lists published by each cloud provider into a compact binary lookup table, so your application can answer "is this a cloud server IP?" with a local binary search.
The range data does not live in this repository and does not ship in the jar. It is built weekly by CI from the providers' own feeds and published as release assets. Your server downloads it once into a local cache and uses that copy from then on — so the data on your machine is yours, and it stays current without a library upgrade.
Both IPv4 and IPv6 are supported.
- Local lookups — after the one-time download, every lookup is a binary search against a cached table
- Data you own — tables live in a cache directory on your host, not inside the artifact
- IPv4 + IPv6 — full CIDR support at any prefix length
- Fast — binary search over ~41,000 blocks, held as primitive arrays rather than objects
- Provider / region filtering — restrict matching to one provider or one region
- Detailed match results —
findMatchtells you which provider, region, and CIDR block matched - Safe input handling — never throws for bad input, never performs a DNS lookup
- Verified downloads — every table is checked against the SHA-256 published in
version.json
| Provider | Source | IPv4 | IPv6 |
|---|---|---|---|
| Amazon (AWS) | ip-ranges.amazonaws.com | ✅ | ✅ |
| Microsoft (Azure) | ServiceTags (AzureCloud) | ✅ | ✅ |
| Google (GCP) | cloud.json | ✅ | ✅ |
| CloudFlare | ips-v4 / ips-v6 | ✅ | ✅ |
| DigitalOcean | geo feed | ✅ | ✅ |
| Oracle (OCI) | public_ip_ranges.json | ✅ | — |
| Tencent | Chat service IP list API | ✅ | — |
Oracle and Tencent publish IPv4-only feeds, and that is unlikely to change: Oracle hands out IPv6 as a per-VCN /56 rather than enumerating regional prefixes, so there is no authoritative list to consume. Third-party IPv6 sets for these providers are inferred from BGP announcements, which is fine for threat intelligence but not for answering "is this a cloud IP".
- JDK 11+
- Kotlin 1.9+ (for Kotlin consumers; works from Java as well)
Published via JitPack. Add the repository and dependency:
// settings.gradle.kts
dependencyResolutionManagement {
repositories {
mavenCentral()
maven("https://jitpack.io")
}
}// build.gradle.kts
dependencies {
implementation("com.github.developerlee79:server-ip-ranges:v2.0.1")
}Groovy DSL
// settings.gradle
dependencyResolutionManagement {
repositories {
mavenCentral()
maven { url 'https://jitpack.io' }
}
}
// build.gradle
dependencies {
implementation 'com.github.developerlee79:server-ip-ranges:v2.0.1'
}No range data ships in the jar, so pick a source before the first lookup. Calling this on every start is fine: when the cache is current only the few-hundred-byte version.json is fetched.
import com.devlee.ipranges.util.IPRangeData
fun main() {
// Downloads the published tables into ~/.cache/server-ip-ranges on first run.
IPRangeData.useRelease()
startApplication()
}A lookup before this runs throws IllegalStateException rather than answering "not a cloud IP" — a missing dataset must not look like a negative result.
Hosts that must not reach the network can stage the release assets themselves:
IPRangeData.usePackedDirectory(File("/opt/server-ip-ranges"))import com.devlee.ipranges.util.IPRangeUtil
class Test {
fun validateIP(ip: String?): Boolean {
return IPRangeUtil.isServerIP(ip)
}
}import com.devlee.ipranges.util.IPRangeUtil
import com.devlee.ipranges.core.provider.Provider
class Test {
fun validateIP(ip: String?): Boolean {
return IPRangeUtil.isServerIP(ip, Provider.Amazon)
}
fun validateIPWithRegion(ip: String?, region: String): Boolean {
return IPRangeUtil.isServerIP(ip, Provider.Amazon, region)
}
}import com.devlee.ipranges.util.IPRangeUtil
class Test {
fun describeIP(ip: String?) {
val match = IPRangeUtil.findMatch(ip) ?: return
println("provider=${match.provider}, region=${match.region}, range=${match.matchedRange}")
}
}null, blank, and non-IP-literal input (including hostnames) returnfalse/null— no exception is thrown for bad input.- Hostnames are rejected before any
InetAddresscall, so no DNS lookup is ever performed. - Addresses are normalized before matching: compressed IPv6 (
2001:db8::1), uppercase hex, and IPv4-mapped IPv6 (::ffff:1.2.3.4) all work. - IPv4 octets with leading zeros (
192.0.2.01) are rejected, since readers disagree on whether010means decimal 10 or octal 8. Send192.0.2.1. - A
falseresult covers both "not a cloud IP" and "unparseable input"; usefindMatchplus your own validation when you need to distinguish them.
- Weekly, a GitHub Actions job fetches each provider's published range document and parses it into region-grouped CIDR blocks. Nothing is committed — the parsed JSON is a build artifact. Azure is the one provider with no stable feed URL, so its link is scraped from the download page; the scrape only accepts a
download.microsoft.comURL and rejects a document older than 45 days, since a page stuck on an old link would otherwise republish stale ranges without any count changing enough to notice. - The same job packs each provider into
<provider>.bin: a sorted table of network addresses plus prefix lengths. End addresses are implied by the prefix length, and IPv4 blocks are stored in four bytes. A provider whose block count collapses fails the job instead of being published, since that is what a truncated upstream feed looks like. - The tables and a
version.jsonnaming their SHA-256 digests are attached to a dated release. IPRangeData.useRelease()downloads them into a cache directory, verifying each digest, and writes them into place atomically. Later starts re-fetch onlyversion.json.isServerIP/findMatchparse the input into an unsigned 32-bit or 128-bit integer and binary-search the cached table.
Providers publish overlapping and nested blocks, so a match walks back from the binary-search position while an enclosing block is still possible. Where blocks overlap, the most specific one is reported.
Data lives in three places, none of them this repository:
| Where | What | Who writes it |
|---|---|---|
| Release assets | <provider>.bin, version.json |
CI, weekly |
~/.cache/server-ip-ranges/<version>/ |
verified copy of the tables | IPRangeData.useRelease() |
<dataDir>/range/<provider>/ip-range.json |
parsed feed output | ./gradlew updateRangeFiles |
$XDG_CACHE_HOME/server-ip-ranges, falling back to ~/.cache/server-ip-ranges. Pass a different directory to useRelease(cacheDirectory = ...), or point usePackedDirectory at tables you staged yourself. A download that fails its digest check is discarded rather than cached.
./gradlew updateRangeFiles # fetch every provider feed into ./range
./gradlew packReleaseData # pack build/release-data: <provider>.bin + version.jsonupdateRangeFiles reports per-provider failures without aborting the whole run, and sets -Dipranges.dataDir to the project directory so the fetched JSON is what lookups read. Set that property yourself to point a running application at a directory of JSON instead. The default ./gradlew test task is hermetic — it builds its own fixtures and never touches the network.
Pass the previous manifest to arm the shrink guard when packing:
./gradlew packReleaseData -PpreviousVersionJson=previous-version.jsonIssues and pull requests welcome — provider additions, data corrections, and feature ideas alike.