jmoney/is-ip-in-range
is-ip-in-range
A high-performance, memory-efficient Java library for checking if IP addresses are within a set of CIDR ranges. Supports both IPv4 and IPv6 addresses.
Features
- Fast lookups: O(k) time complexity where k is the address length (32 bits for IPv4, 128 bits for IPv6)
- Memory efficient: Uses radix tree (Patricia trie) data structure with prefix sharing
- No iteration: Direct tree traversal, no looping over CIDR lists
- Dual stack support: Works with both IPv4 and IPv6 addresses
- Clean API: Simple, fluent interface with builder pattern support
- Zero dependencies: No external runtime dependencies
Performance Characteristics
The library uses a binary radix tree (Patricia trie) implementation:
- Lookup Time: O(k) where k is constant (32 for IPv4, 128 for IPv6)
- Insert Time: O(k) where k is constant
- Space Complexity: O(n*k) where n is number of ranges, but with significant savings from prefix sharing
- No rebalancing: Unlike interval trees, no costly rebalancing operations
This makes it ideal for scenarios with:
- Large numbers of CIDR ranges
- Frequent IP lookups
- Memory-constrained environments
Installation
This library is published to GitHub Packages. Follow these steps to use it in your Maven project:
1. Generate a GitHub Personal Access Token (PAT)
- Go to GitHub Settings → Developer settings → Personal access tokens → Tokens (classic)
- Click "Generate new token (classic)"
- Give it a descriptive name (e.g., "Maven GitHub Packages")
- Select the
read:packagesscope - Click "Generate token" and copy the token (you won't see it again!)
2. Configure Maven Authentication
Add your GitHub credentials to ~/.m2/settings.xml:
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
http://maven.apache.org/xsd/settings-1.0.0.xsd">
<servers>
<server>
<id>github</id>
<username>YOUR_GITHUB_USERNAME</username>
<password>YOUR_PERSONAL_ACCESS_TOKEN</password>
</server>
</servers>
</settings>Security Note: Never commit your settings.xml with credentials to version control!
3. Add Repository to Your Project
Add the GitHub Packages repository to your project's pom.xml:
<repositories>
<repository>
<id>github</id>
<url>https://maven.pkg.github.com/jmoney/is-ip-in-range</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>4. Add Dependency
Add the dependency to your pom.xml:
For stable releases (recommended for production):
<dependency>
<groupId>com.github.jmoney</groupId>
<artifactId>is-ip-in-range</artifactId>
<version>1.0.0</version>
</dependency>Check releases for the latest stable version.
For latest development snapshot:
<dependency>
<groupId>com.github.jmoney</groupId>
<artifactId>is-ip-in-range</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>Snapshots are automatically published on every commit to main. To get the latest snapshot updates:
mvn clean install -U # The -U flag forces Maven to check for updatesAlternative: Local Installation
For development, you can install to your local Maven repository:
git clone https://github.com/jmoney/is-ip-in-range.git
cd is-ip-in-range
mvn clean installThen use the dependency without needing GitHub authentication.
Published Artifacts
When publishing to GitHub Packages, the build automatically generates and uploads three JARs:
-
Main JAR (
is-ip-in-range-1.0.0-SNAPSHOT.jar)- Compiled bytecode
- What gets added to your classpath
-
Sources JAR (
is-ip-in-range-1.0.0-SNAPSHOT-sources.jar)- Original
.javasource files - IDEs (IntelliJ, Eclipse, VS Code) automatically download this
- Allows you to navigate into library code and see actual source
- Essential for debugging and understanding the code
- Original
-
Javadoc JAR (
is-ip-in-range-1.0.0-SNAPSHOT-javadoc.jar)- Generated HTML documentation
- IDEs show javadoc in code completion tooltips
- Can be viewed in browser
No additional configuration needed - Maven and your IDE handle downloading sources/javadoc automatically when you use the dependency!
Usage
Basic Usage
import com.github.jmoney.iprange.IpRanges;
// Create IP ranges and add CIDR ranges
IpRanges ranges = new IpRanges();
ranges.addRange("192.168.0.0/16");
ranges.addRange("10.0.0.0/8");
ranges.addRange("2001:db8::/32");
// Check if IPs are in any of the ranges
boolean result1 = ranges.contains("192.168.1.100"); // true
boolean result2 = ranges.contains("8.8.8.8"); // false
boolean result3 = ranges.contains("2001:db8::1"); // trueBuilder Pattern
IpRanges ranges = IpRanges.builder()
.addRange("192.168.0.0/16")
.addRange("10.0.0.0/8")
.addRange("172.16.0.0/12")
.addRange("2001:db8::/32")
.build();
if (ranges.contains("192.168.1.1")) {
System.out.println("IP is in allowed range");
}Constructor with Collection
List<String> privateRanges = Arrays.asList(
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16"
);
IpRanges ranges = new IpRanges(privateRanges);Copy Constructor
Create a copy of an existing IpRanges instance. The copy is independent and can be modified without affecting the original:
// Create original ranges
IpRanges baseRanges = IpRanges.builder()
.addRange("192.168.0.0/16")
.addRange("10.0.0.0/8")
.build();
// Create a copy
IpRanges extendedRanges = new IpRanges(baseRanges);
// Add additional ranges to the copy
extendedRanges.addRange("172.16.0.0/12");
// Original is unmodified
baseRanges.contains("172.16.1.1"); // false
extendedRanges.contains("172.16.1.1"); // trueThis is useful for creating variations of a base configuration:
// Base configuration for all internal networks
IpRanges internalBase = IpRanges.builder()
.addRange("10.0.0.0/8")
.addRange("172.16.0.0/12")
.addRange("192.168.0.0/16")
.build();
// Development environment: internal + dev cloud
IpRanges devRanges = new IpRanges(internalBase);
devRanges.addRange("52.94.0.0/16"); // AWS dev
// Production environment: internal + prod cloud
IpRanges prodRanges = new IpRanges(internalBase);
prodRanges.addRange("52.95.0.0/16"); // AWS prodThread Safety
Note: IpRanges instances are not thread-safe by default. However, you can achieve thread-safe, lock-free reads using a copy-on-write pattern with the copy constructor:
Copy-on-Write Pattern for Concurrent Access
Ideal for read-heavy workloads with rare updates:
public class IpAllowList {
// AtomicReference provides thread-safe reference swap
private final AtomicReference<IpRanges> ranges =
new AtomicReference<>(new IpRanges());
// READERS: Multiple threads, wait-free, no locks
public boolean isAllowed(String clientIp) {
IpRanges current = ranges.get(); // Atomic read
return current.contains(clientIp); // Read from immutable snapshot
}
// WRITER: Single thread performs copy-on-write
public void addAllowedRange(String cidr) {
IpRanges oldRanges = ranges.get();
IpRanges newRanges = new IpRanges(oldRanges); // Copy all ranges
newRanges.addRange(cidr); // Modify copy
ranges.set(newRanges); // Atomic swap
}
}How it works:
- Each
IpRangesinstance is effectively immutable once published - Readers access their snapshot without any locking
- Writer creates new instance, modifies it, then atomically publishes
AtomicReferenceensures visibility: readers always see fully constructed state
Trade-offs:
- ✅ Pros: Wait-free reads, maximum read throughput, no reader contention
- ✅ Perfect for: Many readers, infrequent writes (< 1/second)
- ❌ Cons: Write cost is O(n) where n = number of existing ranges
- ❌ Not ideal for: Frequent updates or very large range sets
Alternative for frequent updates:
If you need frequent writes, wrap with ReadWriteLock instead:
public class IpAllowList {
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final IpRanges ranges = new IpRanges();
public boolean isAllowed(String clientIp) {
lock.readLock().lock();
try {
return ranges.contains(clientIp);
} finally {
lock.readLock().unlock();
}
}
public void addAllowedRange(String cidr) {
lock.writeLock().lock();
try {
ranges.addRange(cidr);
} finally {
lock.writeLock().unlock();
}
}
}This approach has faster writes but readers may briefly block during updates.
Performance-Optimized Usage
For maximum performance, you can use the lower-level APIs with pre-converted IP addresses:
IpRanges ranges = new IpRanges();
ranges.addRange("192.168.0.0/16");
// Convert IP once, check multiple times
long ip = IPv4RadixTree.ipToLong("192.168.1.100");
boolean result = ranges.containsIPv4(ip);
// For IPv6
byte[] ipv6 = IPv6RadixTree.ipToBytes("2001:db8::1");
boolean result2 = ranges.containsIPv6(ipv6);Use Cases
Firewall Rules
IpRanges allowedIps = IpRanges.builder()
.addRange("10.0.0.0/8") // Internal network
.addRange("192.168.0.0/16") // Internal network
.addRange("52.94.0.0/16") // AWS services
.addRange("2001:db8::/32") // IPv6 internal
.build();
public boolean isAllowed(String clientIp) {
return allowedIps.contains(clientIp);
}Rate Limiting by IP Range
IpRanges trustedNetworks = new IpRanges();
trustedNetworks.addRange("10.0.0.0/8");
trustedNetworks.addRange("172.16.0.0/12");
public int getRateLimit(String ip) {
if (trustedNetworks.contains(ip)) {
return 10000; // Higher limit for trusted networks
}
return 100; // Standard limit
}Geographic IP Filtering
// Example: Block known cloud provider ranges
IpRanges blockedRanges = IpRanges.builder()
.addRanges(getAWSRanges())
.addRanges(getGCPRanges())
.addRanges(getAzureRanges())
.build();
public boolean shouldBlock(String ip) {
return blockedRanges.contains(ip);
}API Reference
IpRanges
Main API class for checking if IP addresses are within CIDR ranges.
Constructors
IpRanges()- Create empty IP rangesIpRanges(Collection<String> cidrRanges)- Create with initial CIDR rangesIpRanges(IpRanges other)- Copy constructor, creates independent copy with same ranges
Static Methods
builder()- Create a new Builder instance for fluent construction
Instance Methods
addRange(String cidr)- Add a CIDR range (auto-detects IPv4 vs IPv6), returns this for chainingaddRanges(Collection<String> cidrs)- Add multiple CIDR ranges, returns this for chainingcontains(String ip)- Check if IP is in any range (auto-detects IPv4 vs IPv6)containsIPv4(long ip)- Check IPv4 address as longcontainsIPv6(byte[] ip)- Check IPv6 address as byte arraygetRanges()- Get unmodifiable list of all CIDR ranges
IPv4RadixTree
Low-level radix tree for IPv4 addresses.
Methods
addRange(String cidr)- Add IPv4 CIDR rangeaddRange(long ip, int prefixLength)- Add range with numeric valuescontains(String ip)- Check if IPv4 address is in any rangecontains(long ip)- Check with numeric IPipToLong(String ip)- Convert IPv4 string to long
IPv6RadixTree
Low-level radix tree for IPv6 addresses.
Methods
addRange(String cidr)- Add IPv6 CIDR rangeaddRange(byte[] ip, int prefixLength)- Add range with byte arraycontains(String ip)- Check if IPv6 address is in any rangecontains(byte[] ip)- Check with byte arrayipToBytes(String ip)- Convert IPv6 string to byte array
Requirements
- Java 21 or higher
- Maven 3.6+ (for building)
Building
# Compile and run tests
mvn clean test
# Package as JAR
mvn clean package
# Install to local repository
mvn clean installTesting
The library includes comprehensive tests covering:
- IPv4 and IPv6 range matching
- Edge cases (0.0.0.0/0, /32, /128)
- Overlapping ranges
- Various CIDR prefix lengths
- Invalid input handling
Run tests:
mvn testDesign Rationale
Why Radix Tree over Interval Tree?
While interval trees are a common choice for range queries, radix trees (Patricia tries) are superior for CIDR range matching because:
- Natural fit: CIDR notation represents prefixes, which map directly to trie structure
- Constant time: O(32) for IPv4, O(128) for IPv6 regardless of range count
- Memory efficiency: Shared prefixes use the same nodes
- Simpler implementation: No rebalancing required
- Cache friendly: Sequential memory access patterns
Data Structure Details
- Binary trie: Each node has left (0 bit) and right (1 bit) children
- Path compression: Only stores bits up to the CIDR prefix length
- Prefix matching: Traversal stops when a CIDR endpoint is reached
- Space optimization: Empty branches are not allocated
Performance Benchmarks
Expected performance characteristics:
- Small dataset (< 100 ranges): ~100-200 ns per lookup
- Medium dataset (< 10K ranges): ~100-200 ns per lookup
- Large dataset (> 100K ranges): ~100-200 ns per lookup
Note: Lookup time remains constant regardless of dataset size!
Publishing (Maintainers)
The project uses three automated GitHub Actions workflows:
1. Pull Request Validation (pr.yaml)
Trigger: Pull requests to main
Actions:
- Runs all tests
- No deployment
Purpose: Validates changes before merging
2. Snapshot Publishing (main.yaml)
Trigger: Every push to main branch
Actions:
- Builds the project
- Runs all tests
- Publishes snapshot to GitHub Packages (e.g.,
1.0.0-SNAPSHOT)
How it works:
- Merge a PR to
main - Workflow automatically publishes new snapshot
- Developers can use latest snapshot with
mvn -U clean install
No manual action required - snapshots are published automatically!
3. Release Publishing (release.yaml)
Trigger: Manual workflow dispatch from GitHub UI
Actions:
Uses Maven Release Plugin to:
- Remove
-SNAPSHOTfrom version (e.g.,1.0.0) - Commit the release version
- Create and push git tag (e.g.,
v1.0.0) - Build and deploy release to GitHub Packages
- Bump to next development version (e.g.,
1.1.0-SNAPSHOT) - Commit and push to
main
How to create a release:
- Go to Actions tab on GitHub
- Select "Release" workflow
- Click "Run workflow"
- (Optional) Specify versions or let Maven auto-increment:
- Release version:
1.0.0 - Development version:
1.1.0-SNAPSHOT
- Release version:
- Click "Run workflow"
That's it! Maven Release Plugin handles:
- ✅ Version updates
- ✅ Git commits and tags
- ✅ Building and publishing release
- ✅ Version bumping for next development cycle
Note on snapshots: Due to GitHub Actions security (workflows triggered by GITHUB_TOKEN don't trigger other workflows), the new snapshot version won't be automatically published. The snapshot will be published on the next push to main, or you can manually trigger main.yaml if you need the snapshot immediately.
Workflow Summary
PR → main (via merge)
↓
main.yaml runs → Publishes 1.0.0-SNAPSHOT
Release workflow (manual trigger)
↓
release.yaml runs → mvn release:prepare release:perform
↓
├─ Creates tag v1.0.0
├─ Publishes 1.0.0 release
└─ Pushes 1.1.0-SNAPSHOT to main
↓
(Note: main.yaml won't auto-trigger due to GitHub Actions security)
Next push to main
↓
main.yaml runs → Publishes 1.1.0-SNAPSHOT
Local Development
For local testing, install to your local Maven repository:
mvn clean installVersion Types
Snapshots (1.0.0-SNAPSHOT):
- Latest development version
- Updated automatically on every main branch push
- May be unstable
- Use for testing and development
- Maven checks for updates with
-Uflag
Releases (1.0.0):
- Stable, tested versions
- Created via Release workflow
- Immutable once published
- Use in production
- Semantic versioning recommended
License
[Add your license here]
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Author
Jon Monette