session_guard/intel/maxmind.rs
1//! [`IpIntel`] backed by `MaxMind` `GeoLite2` / `GeoIP2` databases.
2
3use std::fmt;
4use std::net::IpAddr;
5use std::path::{Path, PathBuf};
6use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
7use std::time::SystemTime;
8
9use arc_swap::{ArcSwap, ArcSwapOption};
10use maxminddb::{PathElement, Reader};
11use serde::Deserialize;
12
13use super::{Anonymity, IpInfo, IpIntel};
14
15/// Which database an error is about.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17#[non_exhaustive]
18pub enum MaxMindDatabase {
19 /// The City or Country database, used for the ISO country code and, with a City edition,
20 /// the city.
21 Country,
22 /// The ASN database, used for the autonomous system number and organization name.
23 Asn,
24 /// The Anonymous IP, Anonymous Plus or IP Risk database, used for the [`Anonymity`].
25 Anonymizer,
26}
27
28impl fmt::Display for MaxMindDatabase {
29 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30 f.write_str(match self {
31 Self::Country => "country",
32 Self::Asn => "ASN",
33 Self::Anonymizer => "anonymizer",
34 })
35 }
36}
37
38/// Errors loading `MaxMind` databases.
39#[derive(Debug, thiserror::Error)]
40#[non_exhaustive]
41pub enum MaxMindError {
42 /// The database file could not be read.
43 #[error("cannot read MaxMind {database} database {}: {source}", path.display())]
44 Io {
45 /// Which database.
46 database: MaxMindDatabase,
47 /// The path that failed.
48 path: PathBuf,
49 /// The underlying I/O error.
50 #[source]
51 source: std::io::Error,
52 },
53 /// The bytes are not a valid MMDB file.
54 #[error("invalid MaxMind {database} database: {source}")]
55 Invalid {
56 /// Which database.
57 database: MaxMindDatabase,
58 /// The parser's error.
59 #[source]
60 source: Box<dyn std::error::Error + Send + Sync + 'static>,
61 },
62 /// A valid database of the wrong kind, e.g. an ASN database passed as the country database.
63 #[error("expected a MaxMind {database} database, found {found:?}")]
64 WrongDatabase {
65 /// Which slot the database was passed for.
66 database: MaxMindDatabase,
67 /// The database type from the file's metadata.
68 found: String,
69 },
70}
71
72/// Country, city, ASN, network name and anonymizer lookups from `MaxMind` databases
73/// (`GeoLite2-City` / `GeoLite2-Country` / `GeoIP2-*` for the country, and the city with a City
74/// edition; `GeoLite2-ASN` for the autonomous system number and organization name; and
75/// optionally `GeoIP2-Anonymous-IP`, `GeoIP-Anonymous-Plus` or `GeoIP2-IP-Risk` for VPNs,
76/// proxies and Tor, added with [`with_anonymizer`](Self::with_anonymizer)).
77///
78/// Databases are read into memory once, so lookups on the request path never touch the disk
79/// and are lock-free. Unknown addresses and lookup errors yield empty fields.
80///
81/// # Anonymizers
82///
83/// The anonymizer databases are paid `GeoIP2` products; there is no `GeoLite2` edition. With
84/// one loaded, every lookup sets [`IpInfo::anonymity`], which feeds the
85/// [`TorOrProxyEntered`](crate::Check::TorOrProxyEntered) and
86/// [`AnonymizerEntered`](crate::Check::AnonymizerEntered) checks. The database's flags map to
87/// the most severe [`Anonymity`] that applies:
88///
89/// | Flag | Anonymity |
90/// |---|---|
91/// | `is_tor_exit_node` | [`TorExitNode`](Anonymity::TorExitNode) |
92/// | `is_public_proxy` | [`PublicProxy`](Anonymity::PublicProxy) |
93/// | `is_residential_proxy` | [`ResidentialProxy`](Anonymity::ResidentialProxy) |
94/// | `is_anonymous_vpn`, unless `anonymizer_confidence` is below 50 | [`Vpn`](Anonymity::Vpn) |
95/// | `is_hosting_provider` | [`HostingProvider`](Anonymity::HostingProvider) |
96/// | none of these, or an address the database does not list | [`Direct`](Anonymity::Direct) |
97///
98/// A failed lookup (e.g. an IPv6 address in an IPv4-only database) leaves the anonymity
99/// unknown, so it never fires a check. The IP Risk database's `ip_risk` score is not used: the
100/// guard scores what changed during a session rather than how risky an address is on its own.
101///
102/// ```no_run
103/// use session_guard::MaxMindIntel;
104///
105/// let intel = MaxMindIntel::open("/var/lib/GeoIP/GeoLite2-Country.mmdb", None::<&str>)?
106/// .with_anonymizer("/var/lib/GeoIP/GeoIP2-Anonymous-IP.mmdb")?;
107/// # let _ = intel;
108/// # Ok::<(), session_guard::MaxMindError>(())
109/// ```
110///
111/// ```no_run
112/// use session_guard::{Guard, GuardConfig, MaxMindIntel, MemoryStore};
113///
114/// let intel = MaxMindIntel::open(
115/// "/var/lib/GeoIP/GeoLite2-Country.mmdb",
116/// Some("/var/lib/GeoIP/GeoLite2-ASN.mmdb"),
117/// )?;
118/// let guard = Guard::builder(MemoryStore::new(), GuardConfig::default()).ip_intel(intel).build();
119/// # let _ = guard;
120/// # Ok::<(), session_guard::MaxMindError>(())
121/// ```
122///
123/// # Keeping the databases up to date
124///
125/// `MaxMind` publishes new databases weekly or twice a week, and `geoipupdate` (as a cron job,
126/// a sidecar or a shared volume) replaces the files on disk. Those new files are only used once
127/// they are reloaded:
128///
129/// - [`spawn_reload`](Self::spawn_reload) (feature `tokio`) checks the files on an interval
130/// and reloads them when they change. This is the usual choice.
131/// - [`reload_if_changed`](Self::reload_if_changed) does a single check, to drive from your own
132/// scheduler or runtime.
133/// - [`reload`](Self::reload) and [`reload_from_bytes`](Self::reload_from_bytes) swap the
134/// databases unconditionally.
135///
136/// A change is detected from each file's modification time and size, so it works on every kind
137/// of mount, including network file systems and Kubernetes volumes that deliver no file system
138/// events. A reload swaps the country and ASN databases at once; in-flight lookups finish on the
139/// old ones. The anonymizer database is checked and reloaded on its own, so a failed update of
140/// one never holds back the other. [`reload`](Self::reload) and
141/// [`reload_from_bytes`](Self::reload_from_bytes) leave the anonymizer in place; swap it with
142/// [`reload_anonymizer`](Self::reload_anonymizer) or
143/// [`reload_anonymizer_from_bytes`](Self::reload_anonymizer_from_bytes).
144///
145/// To reload a source the guard uses, wrap it in an `Arc`, pass a clone to
146/// [`GuardBuilder::ip_intel`](crate::GuardBuilder::ip_intel) and keep the other:
147///
148/// ```no_run
149/// # #[cfg(feature = "tokio")]
150/// # async fn run() -> Result<(), session_guard::MaxMindError> {
151/// use std::sync::Arc;
152/// use std::time::Duration;
153///
154/// use session_guard::{Guard, GuardConfig, MaxMindIntel, MemoryStore};
155///
156/// let intel = Arc::new(MaxMindIntel::open(
157/// "/var/lib/GeoIP/GeoLite2-Country.mmdb",
158/// Some("/var/lib/GeoIP/GeoLite2-ASN.mmdb"),
159/// )?);
160/// intel.spawn_reload(Duration::from_secs(60 * 60));
161/// let guard = Guard::builder(MemoryStore::new(), GuardConfig::default())
162/// .ip_intel(Arc::clone(&intel))
163/// .build();
164/// # let _ = guard;
165/// # Ok(())
166/// # }
167/// ```
168pub struct MaxMindIntel {
169 databases: ArcSwap<Databases>,
170 anonymizer: ArcSwapOption<Reader<Vec<u8>>>,
171 /// The files the current databases were read from. The lock also serializes reloads, so an
172 /// older read never replaces a newer one.
173 files: Mutex<Watched>,
174}
175
176/// The files [`MaxMindIntel::reload_if_changed`] follows; `None` for databases from bytes.
177#[derive(Default)]
178struct Watched {
179 geo: Option<Files>,
180 anonymizer: Option<AnonymizerFile>,
181}
182
183/// The files behind the databases and their state just before they were read.
184struct Files {
185 country: PathBuf,
186 asn: Option<PathBuf>,
187 stamp: Stamp,
188}
189
190/// The state of the country and ASN files that [`MaxMindIntel::reload_if_changed`] compares.
191type Stamp = (FileStamp, Option<FileStamp>);
192
193#[derive(Debug, Clone, Copy, PartialEq, Eq)]
194struct FileStamp {
195 modified: Option<SystemTime>,
196 len: u64,
197}
198
199impl Files {
200 /// Reads the databases at `country` and `asn`. The files' state is taken before reading, so
201 /// a change during the read is caught by the next check instead of being missed.
202 fn read(country: PathBuf, asn: Option<PathBuf>) -> Result<(Databases, Self), MaxMindError> {
203 let stamp = stamp(&country, asn.as_deref())?;
204 let databases = Databases::read(&country, asn.as_deref())?;
205 Ok((databases, Self { country, asn, stamp }))
206 }
207}
208
209/// The anonymizer database's file and its state just before it was read.
210struct AnonymizerFile {
211 path: PathBuf,
212 stamp: FileStamp,
213}
214
215impl AnonymizerFile {
216 /// Like [`Files::read`], for the anonymizer database.
217 fn read(path: PathBuf) -> Result<(Reader<Vec<u8>>, Self), MaxMindError> {
218 let database = MaxMindDatabase::Anonymizer;
219 let stamp = file_stamp(database, &path)?;
220 let bytes = std::fs::read(&path).map_err(io_error(database, &path))?;
221 Ok((reader(database, bytes)?, Self { path, stamp }))
222 }
223}
224
225fn stamp(country: &Path, asn: Option<&Path>) -> Result<Stamp, MaxMindError> {
226 Ok((
227 file_stamp(MaxMindDatabase::Country, country)?,
228 asn.map(|path| file_stamp(MaxMindDatabase::Asn, path)).transpose()?,
229 ))
230}
231
232fn file_stamp(database: MaxMindDatabase, path: &Path) -> Result<FileStamp, MaxMindError> {
233 let metadata = std::fs::metadata(path).map_err(io_error(database, path))?;
234 Ok(FileStamp {
235 modified: metadata.modified().ok(),
236 len: metadata.len(),
237 })
238}
239
240fn io_error(database: MaxMindDatabase, path: &Path) -> impl FnOnce(std::io::Error) -> MaxMindError + '_ {
241 move |source| MaxMindError::Io {
242 database,
243 path: path.to_owned(),
244 source,
245 }
246}
247
248struct Databases {
249 country: Reader<Vec<u8>>,
250 asn: Option<Reader<Vec<u8>>>,
251}
252
253impl Databases {
254 fn read(country: &Path, asn: Option<impl AsRef<Path>>) -> Result<Self, MaxMindError> {
255 let read = |database, path: &Path| std::fs::read(path).map_err(io_error(database, path));
256 let country = read(MaxMindDatabase::Country, country)?;
257 let asn = asn.map(|path| read(MaxMindDatabase::Asn, path.as_ref())).transpose()?;
258 Self::parse(country, asn)
259 }
260
261 fn parse(country: Vec<u8>, asn: Option<Vec<u8>>) -> Result<Self, MaxMindError> {
262 Ok(Self {
263 country: reader(MaxMindDatabase::Country, country)?,
264 asn: asn.map(|bytes| reader(MaxMindDatabase::Asn, bytes)).transpose()?,
265 })
266 }
267}
268
269fn reader(database: MaxMindDatabase, bytes: Vec<u8>) -> Result<Reader<Vec<u8>>, MaxMindError> {
270 let reader = Reader::from_source(bytes).map_err(|source| MaxMindError::Invalid {
271 database,
272 source: Box::new(source),
273 })?;
274 let kind = reader.metadata().database_type.to_ascii_lowercase();
275 // A database of a kind we do not recognize gets the benefit of the doubt in every slot.
276 let kinds = [
277 (
278 MaxMindDatabase::Country,
279 kind.contains("country") || kind.contains("city"),
280 ),
281 (MaxMindDatabase::Asn, kind.contains("asn")),
282 (
283 MaxMindDatabase::Anonymizer,
284 kind.contains("anonymous") || kind.contains("ip-risk"),
285 ),
286 ];
287 let fits = kinds.iter().any(|&(slot, is)| is && slot == database) || kinds.iter().all(|&(_, is)| !is);
288 if !fits {
289 return Err(MaxMindError::WrongDatabase {
290 database,
291 found: reader.metadata().database_type.clone(),
292 });
293 }
294 Ok(reader)
295}
296
297impl MaxMindIntel {
298 /// Reads the country database (City or Country edition) and, optionally, the ASN database.
299 ///
300 /// # Errors
301 ///
302 /// Fails when a file cannot be read, is not a valid MMDB file, or is the wrong kind of
303 /// database.
304 pub fn open(country_db: impl AsRef<Path>, asn_db: Option<impl AsRef<Path>>) -> Result<Self, MaxMindError> {
305 let (databases, files) = Files::read(
306 country_db.as_ref().to_owned(),
307 asn_db.map(|path| path.as_ref().to_owned()),
308 )?;
309 Ok(Self {
310 databases: ArcSwap::from_pointee(databases),
311 anonymizer: ArcSwapOption::empty(),
312 files: Mutex::new(Watched {
313 geo: Some(files),
314 anonymizer: None,
315 }),
316 })
317 }
318
319 /// Like [`open`](Self::open), from database contents already in memory.
320 ///
321 /// # Errors
322 ///
323 /// Fails when the bytes are not a valid MMDB file or the wrong kind of database.
324 pub fn from_bytes(country_db: Vec<u8>, asn_db: Option<Vec<u8>>) -> Result<Self, MaxMindError> {
325 Ok(Self {
326 databases: ArcSwap::from_pointee(Databases::parse(country_db, asn_db)?),
327 anonymizer: ArcSwapOption::empty(),
328 files: Mutex::new(Watched::default()),
329 })
330 }
331
332 /// Adds an Anonymous IP, Anonymous Plus or IP Risk database, so lookups also report the
333 /// [`Anonymity`] of the network; see [Anonymizers](Self#anonymizers).
334 /// [`reload_if_changed`](Self::reload_if_changed) follows its file too.
335 ///
336 /// # Errors
337 ///
338 /// Fails when the file cannot be read, is not a valid MMDB file, or is the wrong kind of
339 /// database.
340 pub fn with_anonymizer(self, anonymizer_db: impl AsRef<Path>) -> Result<Self, MaxMindError> {
341 self.reload_anonymizer(anonymizer_db)?;
342 Ok(self)
343 }
344
345 /// Like [`with_anonymizer`](Self::with_anonymizer), from database contents already in
346 /// memory.
347 ///
348 /// # Errors
349 ///
350 /// Fails when the bytes are not a valid MMDB file or the wrong kind of database.
351 pub fn with_anonymizer_bytes(self, anonymizer_db: Vec<u8>) -> Result<Self, MaxMindError> {
352 self.reload_anonymizer_from_bytes(anonymizer_db)?;
353 Ok(self)
354 }
355
356 /// Replaces the country and ASN databases with freshly read files, which later
357 /// [`reload_if_changed`](Self::reload_if_changed) checks follow. In-flight lookups finish on
358 /// the old databases. On error the current databases and files stay in place. The
359 /// anonymizer database is left as it is.
360 ///
361 /// # Errors
362 ///
363 /// As for [`open`](Self::open).
364 pub fn reload(&self, country_db: impl AsRef<Path>, asn_db: Option<impl AsRef<Path>>) -> Result<(), MaxMindError> {
365 let mut files = self.lock_files();
366 let (databases, read) = Files::read(
367 country_db.as_ref().to_owned(),
368 asn_db.map(|path| path.as_ref().to_owned()),
369 )?;
370 self.databases.store(Arc::new(databases));
371 files.geo = Some(read);
372 Ok(())
373 }
374
375 /// Like [`reload`](Self::reload), from database contents already in memory. The databases
376 /// no longer come from files, so [`reload_if_changed`](Self::reload_if_changed) stops
377 /// checking them (but still checks the anonymizer database's file, if it has one).
378 ///
379 /// # Errors
380 ///
381 /// As for [`from_bytes`](Self::from_bytes).
382 pub fn reload_from_bytes(&self, country_db: Vec<u8>, asn_db: Option<Vec<u8>>) -> Result<(), MaxMindError> {
383 let mut files = self.lock_files();
384 self.databases.store(Arc::new(Databases::parse(country_db, asn_db)?));
385 files.geo = None;
386 Ok(())
387 }
388
389 /// Replaces (or adds) the anonymizer database with a freshly read file, which later
390 /// [`reload_if_changed`](Self::reload_if_changed) checks follow. On error the current
391 /// anonymizer database and file stay in place.
392 ///
393 /// # Errors
394 ///
395 /// As for [`with_anonymizer`](Self::with_anonymizer).
396 pub fn reload_anonymizer(&self, anonymizer_db: impl AsRef<Path>) -> Result<(), MaxMindError> {
397 let mut files = self.lock_files();
398 let (reader, read) = AnonymizerFile::read(anonymizer_db.as_ref().to_owned())?;
399 self.anonymizer.store(Some(Arc::new(reader)));
400 files.anonymizer = Some(read);
401 Ok(())
402 }
403
404 /// Like [`reload_anonymizer`](Self::reload_anonymizer), from database contents already in
405 /// memory. [`reload_if_changed`](Self::reload_if_changed) then stops checking the
406 /// anonymizer database.
407 ///
408 /// # Errors
409 ///
410 /// As for [`with_anonymizer_bytes`](Self::with_anonymizer_bytes).
411 pub fn reload_anonymizer_from_bytes(&self, anonymizer_db: Vec<u8>) -> Result<(), MaxMindError> {
412 let mut files = self.lock_files();
413 let reader = reader(MaxMindDatabase::Anonymizer, anonymizer_db)?;
414 self.anonymizer.store(Some(Arc::new(reader)));
415 files.anonymizer = None;
416 Ok(())
417 }
418
419 /// Reloads the databases if their files changed since they were last read, and returns
420 /// whether it did.
421 ///
422 /// The files are those given to [`open`](Self::open) or the latest
423 /// [`reload`](Self::reload), and to [`with_anonymizer`](Self::with_anonymizer) or the latest
424 /// [`reload_anonymizer`](Self::reload_anonymizer). Each is compared by modification time and
425 /// size, which costs a `stat` per file; unchanged files are not read. The country and ASN
426 /// databases are reloaded together, the anonymizer database on its own. Databases from
427 /// bytes have no files and are never reloaded here.
428 ///
429 /// This blocks while it reads and parses changed files (tens of megabytes for a City
430 /// database), so call it off the async runtime, or use [`spawn_reload`](Self::spawn_reload).
431 ///
432 /// # Errors
433 ///
434 /// As for [`open`](Self::open), for example when a file is missing, or only partly written
435 /// by a tool that does not replace it atomically. The databases that failed then stay in
436 /// place and the next call tries again; the others are still reloaded. With failures on both
437 /// sides, the country or ASN error is returned.
438 pub fn reload_if_changed(&self) -> Result<bool, MaxMindError> {
439 let mut files = self.lock_files();
440 let geo = self.reload_geo_if_changed(&mut files.geo);
441 let anonymizer = self.reload_anonymizer_if_changed(&mut files.anonymizer);
442 Ok(geo? | anonymizer?)
443 }
444
445 fn reload_geo_if_changed(&self, files: &mut Option<Files>) -> Result<bool, MaxMindError> {
446 let Some(current) = files.as_ref() else {
447 return Ok(false);
448 };
449 if stamp(¤t.country, current.asn.as_deref())? == current.stamp {
450 return Ok(false);
451 }
452 let (databases, read) = Files::read(current.country.clone(), current.asn.clone())?;
453 self.databases.store(Arc::new(databases));
454 *files = Some(read);
455 Ok(true)
456 }
457
458 fn reload_anonymizer_if_changed(&self, file: &mut Option<AnonymizerFile>) -> Result<bool, MaxMindError> {
459 let Some(current) = file.as_ref() else {
460 return Ok(false);
461 };
462 if file_stamp(MaxMindDatabase::Anonymizer, ¤t.path)? == current.stamp {
463 return Ok(false);
464 }
465 let (reader, read) = AnonymizerFile::read(current.path.clone())?;
466 self.anonymizer.store(Some(Arc::new(reader)));
467 *file = Some(read);
468 Ok(true)
469 }
470
471 /// Runs [`reload_if_changed`](Self::reload_if_changed) now and then every `every` on the
472 /// tokio runtime, reading files on its blocking pool. Each reload is logged (`info`, with
473 /// the databases' build times) and each failure too (`warn`), with `tracing`. A failed check
474 /// does not stop the task and is retried on the next tick; abort the handle to stop it.
475 ///
476 /// `MaxMind` releases new databases at most twice a week, so an interval of minutes to an
477 /// hour is plenty, and a check of unchanged files is cheap.
478 ///
479 /// # Panics
480 ///
481 /// Outside a tokio runtime, or if `every` is zero.
482 #[cfg(feature = "tokio")]
483 #[cfg_attr(docsrs, doc(cfg(feature = "tokio")))]
484 pub fn spawn_reload(self: &Arc<Self>, every: std::time::Duration) -> tokio::task::JoinHandle<()> {
485 let intel = Arc::clone(self);
486 tokio::spawn(async move {
487 let mut ticker = tokio::time::interval(every);
488 ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
489 loop {
490 ticker.tick().await;
491 let check = Arc::clone(&intel);
492 match tokio::task::spawn_blocking(move || check.reload_if_changed()).await {
493 Ok(Ok(true)) => {
494 let databases = intel.databases.load();
495 tracing::info!(
496 country_built = databases.country.metadata().build_epoch,
497 asn_built = databases.asn.as_ref().map(|reader| reader.metadata().build_epoch),
498 anonymizer_built = intel
499 .anonymizer
500 .load()
501 .as_ref()
502 .map(|reader| reader.metadata().build_epoch),
503 "reloaded MaxMind databases"
504 );
505 }
506 Ok(Ok(false)) => {}
507 Ok(Err(error)) => tracing::warn!(%error, "reloading MaxMind databases failed"),
508 Err(error) => tracing::warn!(%error, "reloading MaxMind databases panicked"),
509 }
510 }
511 })
512 }
513
514 /// A reload that panicked left the previous state intact (it is only replaced on success),
515 /// so a poisoned lock is safe to reuse.
516 fn lock_files(&self) -> MutexGuard<'_, Watched> {
517 self.files.lock().unwrap_or_else(PoisonError::into_inner)
518 }
519}
520
521const COUNTRY: [PathElement<'static>; 2] = [PathElement::Key("country"), PathElement::Key("iso_code")];
522const REGISTERED_COUNTRY: [PathElement<'static>; 2] =
523 [PathElement::Key("registered_country"), PathElement::Key("iso_code")];
524const CITY: [PathElement<'static>; 3] = [
525 PathElement::Key("city"),
526 PathElement::Key("names"),
527 PathElement::Key("en"),
528];
529const ASN: [PathElement<'static>; 1] = [PathElement::Key("autonomous_system_number")];
530const NETWORK: [PathElement<'static>; 1] = [PathElement::Key("autonomous_system_organization")];
531
532/// Below this `anonymizer_confidence`, `is_anonymous_vpn` is ignored.
533const VPN_CONFIDENCE: u16 = 50;
534
535/// The fields shared by the Anonymous IP, Anonymous Plus and IP Risk databases. Flags are only
536/// present when true.
537#[derive(Default, Deserialize)]
538#[serde(default)]
539#[allow(clippy::struct_excessive_bools, reason = "mirrors the database's record")]
540struct AnonymizerRecord {
541 is_anonymous_vpn: bool,
542 is_hosting_provider: bool,
543 is_public_proxy: bool,
544 is_residential_proxy: bool,
545 is_tor_exit_node: bool,
546 anonymizer_confidence: Option<u16>,
547}
548
549impl AnonymizerRecord {
550 fn anonymity(&self) -> Anonymity {
551 if self.is_tor_exit_node {
552 Anonymity::TorExitNode
553 } else if self.is_public_proxy {
554 Anonymity::PublicProxy
555 } else if self.is_residential_proxy {
556 Anonymity::ResidentialProxy
557 } else if self.is_anonymous_vpn && self.anonymizer_confidence.is_none_or(|c| c >= VPN_CONFIDENCE) {
558 Anonymity::Vpn
559 } else if self.is_hosting_provider {
560 Anonymity::HostingProvider
561 } else {
562 Anonymity::Direct
563 }
564 }
565}
566
567impl IpIntel for MaxMindIntel {
568 fn lookup(&self, ip: IpAddr) -> IpInfo {
569 let ip = crate::client::canonical_ip(ip);
570 let databases = self.databases.load();
571 let geo = databases.country.lookup(ip).ok();
572 let country = geo.as_ref().and_then(|found| {
573 [&COUNTRY[..], ®ISTERED_COUNTRY[..]]
574 .into_iter()
575 .find_map(|path| found.decode_path::<&str>(path).ok().flatten())
576 .map(str::to_owned)
577 });
578 // Country editions have no `city`, so this is only found in City editions.
579 let city = geo.and_then(|found| found.decode_path::<String>(&CITY).ok().flatten());
580 let system = databases.asn.as_ref().and_then(|reader| reader.lookup(ip).ok());
581 let asn = system
582 .as_ref()
583 .and_then(|found| found.decode_path::<u32>(&ASN).ok().flatten());
584 let network = system.and_then(|found| found.decode_path::<String>(&NETWORK).ok().flatten());
585 // An address the database does not list is not known to be an anonymizer.
586 let anonymity = self.anonymizer.load().as_ref().and_then(|reader| {
587 let found = reader.lookup(ip).ok()?.decode::<AnonymizerRecord>().ok()?;
588 Some(found.map_or(Anonymity::Direct, |record| record.anonymity()))
589 });
590 IpInfo {
591 anonymity,
592 city,
593 network,
594 ..IpInfo::new(country, asn)
595 }
596 }
597}
598
599impl fmt::Debug for MaxMindIntel {
600 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
601 let databases = self.databases.load();
602 let anonymizer = self.anonymizer.load();
603 let files = self.files.lock().unwrap_or_else(PoisonError::into_inner);
604 let geo = files.geo.as_ref();
605 f.debug_struct("MaxMindIntel")
606 .field("country", &databases.country.metadata().database_type)
607 .field("asn", &databases.asn.as_ref().map(|r| &r.metadata().database_type))
608 .field("anonymizer", &anonymizer.as_ref().map(|r| &r.metadata().database_type))
609 .field("country_file", &geo.map(|files| &files.country))
610 .field("asn_file", &geo.and_then(|files| files.asn.as_ref()))
611 .field("anonymizer_file", &files.anonymizer.as_ref().map(|file| &file.path))
612 .finish()
613 }
614}
615
616#[cfg(test)]
617#[path = "maxmind_tests.rs"]
618mod tests;