ip_alloc_lookup/lib.rs
1//! # Offline RIPE-based IP Allocation Lookup
2//!
3//! This crate provides fast, offline lookups for IPv4 and IPv6 addresses based on
4//! RIPE NCC delegated allocation data. It maps IP address ranges to ISO-3166
5//! country codes and includes a built-in classification for European Union (EU)
6//! membership.
7//!
8//! ## What this crate does
9//!
10//! - Performs **purely offline** IP lookups using pre-generated range tables.
11//! - Supports **IPv4 and IPv6** with logarithmic-time lookups.
12//! - Uses **RIPE delegated statistics**, not active geolocation or probing.
13//! - Associates each IP range with a **country code** and an **EU membership flag**.
14//!
15//! ## What this crate does NOT do
16//!
17//! - It does **not** determine the physical location of hosts or users.
18//! - It does **not** track BGP routing, anycast behavior, or traffic paths.
19//! - It does **not** provide legal or regulatory compliance guarantees.
20//!
21//! The country and EU information reflect the **RIR allocation or assignment
22//! metadata** published by RIPE NCC. In real-world networks, traffic may be served
23//! from different locations due to CDNs, anycast, tunneling, or routing policies.
24//!
25//! ## Design goals
26//!
27//! - Predictable performance (no syscalls or I/O on lookup).
28//! - Deterministic results (static data, no runtime mutation).
29//! - Minimal memory overhead and no mandatory external dependencies.
30//!
31//! ## Typical use cases
32//!
33//! - High-throughput IP classification in hot paths (firewalls, proxies, logging).
34//! - Allocation-based policy checks (e.g. EU vs non-EU).
35//! - Offline or restricted environments where external services are unavailable.
36//!
37//! This crate should be understood as an **IP allocation lookup**, not a
38//! geolocation service.
39mod database;
40
41// Re-export public API
42pub use database::{GeoIpDb, GeoInfo, DbStats};
43
44// We keep the parser public for users who want to work with raw RIPE data
45use std::net::{Ipv4Addr, Ipv6Addr};
46
47#[derive(Debug, Clone, PartialEq)]
48pub struct IpRange {
49 pub start_v4: Option<Ipv4Addr>,
50 pub start_v6: Option<Ipv6Addr>,
51 pub count: u128,
52 pub country: String,
53}
54
55/// Parses RIPE delegated stats format for both IPv4 and IPv6
56/// This is exposed for advanced users who want to process RIPE data themselves
57pub fn parse_ripe_delegated(content: &str) -> Vec<IpRange> {
58 content
59 .lines()
60 .filter(|line| {
61 !line.starts_with('#')
62 && !line.starts_with('2')
63 && (line.contains("ipv4") || line.contains("ipv6"))
64 })
65 .filter_map(|line| {
66 let parts: Vec<&str> = line.split('|').collect();
67
68 if parts.len() < 7 {
69 return None;
70 }
71
72 let ip_type = parts[2];
73 let country = parts[1].to_string();
74
75 if ip_type == "ipv4" {
76 Some(IpRange {
77 start_v4: parts[3].parse().ok(),
78 start_v6: None,
79 count: parts[4].parse::<u32>().ok()? as u128,
80 country,
81 })
82 } else if ip_type == "ipv6" {
83 // For IPv6, the count field is actually the prefix length
84 let prefix_len: u32 = parts[4].parse().ok()?;
85 let host_bits = 128 - prefix_len;
86 let count = if host_bits >= 128 {
87 u128::MAX
88 } else {
89 1u128 << host_bits
90 };
91
92 Some(IpRange {
93 start_v4: None,
94 start_v6: parts[3].parse().ok(),
95 count,
96 country,
97 })
98 } else {
99 None
100 }
101 })
102 .collect()
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108 use std::net::IpAddr;
109
110 #[test]
111 fn test_basic_usage() {
112 // This is what users will actually do
113 let db = GeoIpDb::new();
114
115 println!("\n Testing basic library usage:");
116
117 // Test a German IPv4
118 let ipv4: IpAddr = "46.4.0.1".parse().unwrap();
119 if let Some(info) = db.lookup(ipv4) {
120 println!(" 46.4.0.1 -> {} (EU: {})", info.country_code_str(), info.is_eu);
121 assert!(info.is_eu);
122 }
123
124 // Test convenience method
125 let is_eu = db.is_eu("46.4.0.1".parse().unwrap());
126 println!(" is_eu(46.4.0.1) = {}", is_eu);
127
128 // Show stats
129 let stats = db.stats();
130 println!(" Database: {} IPv4 ranges ({} EU, {} non-EU)",
131 stats.total_v4_ranges, stats.eu_v4_ranges, stats.non_eu_v4_ranges);
132 println!(" {} IPv6 ranges ({} EU, {} non-EU)",
133 stats.total_v6_ranges, stats.eu_v6_ranges, stats.non_eu_v6_ranges);
134 }
135
136 #[test]
137 fn test_ipv6_lookup() {
138 let db = GeoIpDb::new();
139
140 // Try to look up a common European IPv6 address
141 let ipv6: IpAddr = "2a01:4f8::1".parse().unwrap();
142
143 if let Some(info) = db.lookup(ipv6) {
144 println!(" 2a01:4f8::1 -> {} (EU: {})", info.country_code_str(), info.is_eu);
145 } else {
146 println!(" 2a01:4f8::1 not found in database");
147 }
148 }
149}