use std::net::IpAddr;
use std::path::Path;
use irontide_settings::Settings;
pub(crate) struct GeoIpResolver {
reader: maxminddb::Reader<Vec<u8>>,
}
impl std::fmt::Debug for GeoIpResolver {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("GeoIpResolver").finish_non_exhaustive()
}
}
impl GeoIpResolver {
pub(crate) fn open(path: &Path) -> Result<Self, maxminddb::MaxMindDbError> {
Ok(Self {
reader: maxminddb::Reader::open_readfile(path)?,
})
}
#[must_use]
pub(crate) fn lookup(&self, ip: IpAddr) -> Option<[u8; 2]> {
let result = self.reader.lookup(ip).ok()?;
let country = result.decode::<maxminddb::geoip2::Country>().ok()??;
let iso = country.country.iso_code?;
let bytes = iso.as_bytes();
if bytes.len() == 2 && bytes.iter().all(u8::is_ascii_uppercase) {
Some([bytes[0], bytes[1]])
} else {
None
}
}
}
#[must_use]
pub(crate) fn build_geoip_resolver(settings: &Settings) -> Option<GeoIpResolver> {
if !settings.resolve_peer_countries {
return None;
}
let path = settings.peer_country_db_path.as_deref()?;
match GeoIpResolver::open(path) {
Ok(r) => Some(r),
Err(e) => {
tracing::warn!(
path = %path.display(),
error = %e,
"resolve_peer_countries enabled but the GeoIP DB failed to open; \
peer country codes will be None"
);
None
}
}
}
pub(crate) fn stamp_country_codes(
peers: &mut [crate::types::PeerInfo],
resolver: Option<&GeoIpResolver>,
) {
let Some(resolver) = resolver else { return };
for peer in peers {
peer.country_code = resolver.lookup(peer.addr.ip());
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write as _;
use std::str::FromStr as _;
#[derive(serde::Serialize)]
struct CountryRec {
country: CountryInner,
}
#[derive(serde::Serialize)]
struct CountryInner {
iso_code: &'static str,
}
fn make_test_mmdb() -> Vec<u8> {
let mut db = maxminddb_writer::Database::default();
db.metadata.ip_version = maxminddb_writer::metadata::IpVersion::V4;
let gb = db
.insert_value(CountryRec {
country: CountryInner { iso_code: "GB" },
})
.expect("insert GB");
let xl = db
.insert_value(CountryRec {
country: CountryInner { iso_code: "XL" },
})
.expect("insert XL");
db.insert_node(
maxminddb_writer::paths::IpAddrWithMask::from_str("81.2.69.0/24").expect("cidr"),
gb,
);
db.insert_node(
maxminddb_writer::paths::IpAddrWithMask::from_str("127.0.0.0/8").expect("cidr"),
xl,
);
db.write_to(Vec::new()).expect("serialize mmdb")
}
fn fixture_file() -> tempfile::NamedTempFile {
let mut f = tempfile::NamedTempFile::new().expect("tmp");
f.write_all(&make_test_mmdb()).expect("write fixture");
f.flush().expect("flush");
f
}
fn test_peer(addr: &str) -> crate::types::PeerInfo {
crate::types::PeerInfo {
addr: addr.parse().unwrap(),
client: String::new(),
peer_choking: false,
peer_interested: false,
am_choking: false,
am_interested: false,
download_rate: 0,
upload_rate: 0,
num_pieces: 0,
source: irontide_session_types::PeerSource::Tracker,
supports_fast: false,
upload_only: false,
snubbed: false,
connected_duration_secs: 0,
num_pending_requests: 0,
num_incoming_requests: 0,
is_optimistic: false,
is_encrypted: false,
uses_utp: false,
uses_holepunch: false,
in_flight_requests: 0,
target_pipeline_depth: 0,
relevance: 0.0,
connection_kind: crate::types::PeerConnectionKind::Tcp,
progress: 0.0,
country_code: None,
}
}
#[test]
fn m255_lookup_known_ip_resolves_gb() {
let f = fixture_file();
let r = GeoIpResolver::open(f.path()).expect("open fixture");
assert_eq!(r.lookup("81.2.69.142".parse().unwrap()), Some(*b"GB"));
}
#[test]
fn m255_lookup_unmapped_ip_is_none() {
let f = fixture_file();
let r = GeoIpResolver::open(f.path()).expect("open fixture");
assert_eq!(r.lookup("10.1.2.3".parse().unwrap()), None);
}
#[test]
fn m255_open_missing_file_errors() {
assert!(GeoIpResolver::open(Path::new("/nonexistent/geo.mmdb")).is_err());
}
#[test]
fn m255_open_garbage_file_errors() {
let mut f = tempfile::NamedTempFile::new().expect("tmp");
f.write_all(b"not an mmdb").expect("write");
f.flush().expect("flush");
assert!(GeoIpResolver::open(f.path()).is_err());
}
#[test]
fn m255_build_resolver_disabled_is_none() {
let s = Settings::default();
assert!(build_geoip_resolver(&s).is_none());
}
#[test]
fn m255_build_resolver_enabled_bad_path_degrades_to_none() {
let s = Settings {
resolve_peer_countries: true,
peer_country_db_path: Some("/nonexistent/geo.mmdb".into()),
..Settings::default()
};
assert!(build_geoip_resolver(&s).is_none());
}
#[test]
fn m255_build_resolver_enabled_valid_is_some() {
let f = fixture_file();
let s = Settings {
resolve_peer_countries: true,
peer_country_db_path: Some(f.path().to_path_buf()),
..Settings::default()
};
assert!(build_geoip_resolver(&s).is_some());
}
#[test]
fn m255_stamp_country_codes_stamps_and_skips() {
let f = fixture_file();
let r = GeoIpResolver::open(f.path()).expect("open fixture");
let mut peers = vec![
test_peer("81.2.69.10:6881"),
test_peer("203.0.113.5:6881"), ];
stamp_country_codes(&mut peers, Some(&r));
assert_eq!(peers[0].country_code, Some(*b"GB"));
assert_eq!(peers[1].country_code, None);
let mut more = vec![test_peer("81.2.69.10:6881")];
stamp_country_codes(&mut more, None);
assert_eq!(more[0].country_code, None);
}
}