geoip: show full country name (country.names.en) in whois and GEOIP, keep iso for geoban
This commit is contained in:
parent
7a99f49ca4
commit
1969218d3f
1 changed files with 40 additions and 26 deletions
|
|
@ -23,6 +23,13 @@ const SEPARATOR: usize = 16;
|
||||||
/// A loaded MaxMind DB, cached in `Server.ext`.
|
/// A loaded MaxMind DB, cached in `Server.ext`.
|
||||||
pub struct GeoDb(pub Arc<Mmdb>);
|
pub struct GeoDb(pub Arc<Mmdb>);
|
||||||
|
|
||||||
|
/// A resolved country: the ISO 3166-1 alpha-2 code (for the `G:` geoban) and the
|
||||||
|
/// full English name (for display).
|
||||||
|
pub struct Country {
|
||||||
|
pub iso: String,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
/// A parsed `.mmdb` file: the raw bytes plus the tree geometry from its metadata.
|
/// A parsed `.mmdb` file: the raw bytes plus the tree geometry from its metadata.
|
||||||
pub struct Mmdb {
|
pub struct Mmdb {
|
||||||
data: Vec<u8>,
|
data: Vec<u8>,
|
||||||
|
|
@ -224,8 +231,8 @@ impl Mmdb {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The 2-letter ISO country code for `ip`, if the database has one.
|
/// The country for `ip` — ISO code plus English name — if the database has one.
|
||||||
pub fn country(&self, ip: IpAddr) -> Option<String> {
|
pub fn country(&self, ip: IpAddr) -> Option<Country> {
|
||||||
// build the bit path; IPv4 in an IPv6 db is prefixed with 96 zero bits
|
// build the bit path; IPv4 in an IPv6 db is prefixed with 96 zero bits
|
||||||
let mut bits: Vec<bool> = Vec::with_capacity(128);
|
let mut bits: Vec<bool> = Vec::with_capacity(128);
|
||||||
match ip {
|
match ip {
|
||||||
|
|
@ -264,7 +271,15 @@ impl Mmdb {
|
||||||
base: self.data_start,
|
base: self.data_start,
|
||||||
};
|
};
|
||||||
let country = d.map_get(abs, "country")?;
|
let country = d.map_get(abs, "country")?;
|
||||||
return d.string(d.map_get(country, "iso_code")?);
|
let iso = d.string(d.map_get(country, "iso_code")?)?;
|
||||||
|
// country.names.en — the `names` submap is usually a shared pointer;
|
||||||
|
// map_get/string follow it. Fall back to the code if it's absent.
|
||||||
|
let name = d
|
||||||
|
.map_get(country, "names")
|
||||||
|
.and_then(|names| d.map_get(names, "en"))
|
||||||
|
.and_then(|en| d.string(en))
|
||||||
|
.unwrap_or_else(|| iso.clone());
|
||||||
|
return Some(Country { iso, name });
|
||||||
}
|
}
|
||||||
node = rec;
|
node = rec;
|
||||||
}
|
}
|
||||||
|
|
@ -286,32 +301,32 @@ pub fn init(s: &mut Server) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The ISO country code of `ip` per the loaded database, uppercased.
|
/// The country of `ip` per the loaded database (ISO code uppercased).
|
||||||
pub fn country_of(s: &Server, ip: IpAddr) -> Option<String> {
|
pub fn lookup(s: &Server, ip: IpAddr) -> Option<Country> {
|
||||||
s.ext
|
s.ext.get::<GeoDb>().and_then(|db| db.0.country(ip)).map(|c| Country {
|
||||||
.get::<GeoDb>()
|
iso: c.iso.to_ascii_uppercase(),
|
||||||
.and_then(|db| db.0.country(ip))
|
name: c.name,
|
||||||
.map(|c| c.to_ascii_uppercase())
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The `G:<cc>` geoban match: does `uid`'s country equal (case-insensitively) the
|
/// The `G:<cc>` geoban match: does `uid`'s country code equal (case-insensitively)
|
||||||
/// country code in the extban? Dispatched from `Server::ban_list_hit`.
|
/// one of the codes in the extban? Dispatched from `Server::ban_list_hit`.
|
||||||
pub fn geoban_match(s: &Server, uid: Uid, spec: &str) -> bool {
|
pub fn geoban_match(s: &Server, uid: Uid, spec: &str) -> bool {
|
||||||
let Some(ip) = s.users.get(&uid).map(|u| u.addr.ip()) else {
|
let Some(ip) = s.users.get(&uid).map(|u| u.addr.ip()) else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
match country_of(s, ip) {
|
match lookup(s, ip) {
|
||||||
Some(cc) => spec
|
Some(c) => spec
|
||||||
.split(',')
|
.split(',')
|
||||||
.any(|want| want.trim().eq_ignore_ascii_case(&cc)),
|
.any(|want| want.trim().eq_ignore_ascii_case(&c.iso)),
|
||||||
None => false,
|
None => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A WHOIS line (opers only) showing the target's country.
|
/// A WHOIS line (opers only) naming the target's country.
|
||||||
pub fn whois_line(s: &Server, tuid: Uid) -> Option<String> {
|
pub fn whois_line(s: &Server, tuid: Uid) -> Option<String> {
|
||||||
let ip = s.users.get(&tuid).map(|u| u.addr.ip())?;
|
let ip = s.users.get(&tuid).map(|u| u.addr.ip())?;
|
||||||
country_of(s, ip).map(|cc| format!("is connecting from country {cc}"))
|
lookup(s, ip).map(|c| format!("is connecting from country {}", c.name))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn commands() -> Vec<Box<dyn Command>> {
|
pub fn commands() -> Vec<Box<dyn Command>> {
|
||||||
|
|
@ -345,8 +360,8 @@ impl Command for GeoIpCmd {
|
||||||
let nick = s.users.get(&uid).map(|u| u.nick.clone()).unwrap_or_default();
|
let nick = s.users.get(&uid).map(|u| u.nick.clone()).unwrap_or_default();
|
||||||
let msg = match ip {
|
let msg = match ip {
|
||||||
None => format!("GEOIP: no such nick, and {target} is not an IP"),
|
None => format!("GEOIP: no such nick, and {target} is not an IP"),
|
||||||
Some(ip) => match country_of(s, ip) {
|
Some(ip) => match lookup(s, ip) {
|
||||||
Some(cc) => format!("GEOIP: {target} ({ip}) is in country {cc}"),
|
Some(c) => format!("GEOIP: {target} ({ip}) is in {} ({})", c.name, c.iso),
|
||||||
None => format!("GEOIP: no country found for {target} ({ip})"),
|
None => format!("GEOIP: no country found for {target} ({ip})"),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -376,17 +391,16 @@ mod tests {
|
||||||
let Some(db) = load() else {
|
let Some(db) = load() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
// 8.8.8.8 (Google DNS) is US in every GeoLite2 vintage.
|
// 8.8.8.8 (Google DNS) is US, "United States", in every GeoLite2 vintage.
|
||||||
assert_eq!(
|
let us = db.country(IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8))).unwrap();
|
||||||
db.country(IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8))).as_deref(),
|
assert_eq!(us.iso, "US");
|
||||||
Some("US")
|
assert_eq!(us.name, "United States");
|
||||||
);
|
|
||||||
// A private address has no country record.
|
// A private address has no country record.
|
||||||
assert_eq!(db.country(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1))), None);
|
assert!(db.country(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1))).is_none());
|
||||||
// IPv6 traversal (Google public DNS) also resolves to US.
|
// IPv6 traversal (Google public DNS) also resolves to US.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
db.country("2001:4860:4860::8888".parse().unwrap()).as_deref(),
|
db.country("2001:4860:4860::8888".parse().unwrap()).unwrap().iso,
|
||||||
Some("US")
|
"US"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue