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}