From 2fb23850044cc64485e1973f6bae128790143d5b Mon Sep 17 00:00:00 2001 From: Slatian Date: Fri, 17 Feb 2023 18:45:37 +0100 Subject: [PATCH] Write some major geolocation shuffling around code --- .gitignore | 1 + Cargo.lock | 11 +++ Cargo.toml | 1 + src/geoip.rs | 200 +++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 69 ++++++++++++++- templates/index.html | 3 + 6 files changed, 284 insertions(+), 1 deletion(-) create mode 100644 src/geoip.rs diff --git a/.gitignore b/.gitignore index ea8c4bf..836bcf1 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +/mmdb diff --git a/Cargo.lock b/Cargo.lock index 6fb4fbb..99d4e80 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -646,6 +646,16 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b87248edafb776e59e6ee64a79086f65890d3510f2c656c000bf2a7e8a0aea40" +[[package]] +name = "maxminddb" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d13fa57adcc4f3aca91e511b3cdaa58ed8cbcbf97f20e342a11218c76e127f51" +dependencies = [ + "log", + "serde", +] + [[package]] name = "memchr" version = "2.5.0" @@ -959,6 +969,7 @@ name = "rust-web-thingy" version = "0.1.0" dependencies = [ "axum", + "maxminddb", "rand", "serde", "tera", diff --git a/Cargo.toml b/Cargo.toml index 2424c40..a5cc548 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,3 +12,4 @@ serde = { version = "1", features = ["derive"] } tokio = { version = "1", features = ["full"] } tera = "1" trust-dns-resolver = "0.22" +maxminddb = "0.17" diff --git a/src/geoip.rs b/src/geoip.rs new file mode 100644 index 0000000..9e21786 --- /dev/null +++ b/src/geoip.rs @@ -0,0 +1,200 @@ +/* + * This module provides an abstraction over the maxmind geoip databases + * that provides the results ready for templating. + */ + +use maxminddb; +use maxminddb::geoip2; + +use std::collections::BTreeMap; + +use std::net::IpAddr; + + +/* Datatypes */ + +// TODO +pub enum IsTheNameOf { + Continent, + Country, + Subdivision, + City, +} + +#[derive(serde::Deserialize, serde::Serialize, Default)] +pub struct NamedLocation { + iso_code: Option, + name: Option, + geoname_id: Option, +} + +#[derive(serde::Deserialize, serde::Serialize, Default)] +pub struct LocationCoordinates { + latitude: f64, + logtitude: f64, +} + +#[derive(serde::Deserialize, serde::Serialize, Default)] +pub struct LocationResult { + continent: Option, + country: Option, + registered_country: Option, + represented_country: Option, + subdivisions: Option>, + city: Option, + + postal_code: Option, + time_zone: Option, +} + +#[derive(serde::Deserialize, serde::Serialize, Default)] +pub struct AsnResult { + asn: Option, + name: Option, +} + +struct MMDBCarrier { + mmdb: Option>>, +} + +trait QueryLocation { + fn query_location_for_ip(&self, address: IpAddr, laguages: &Vec) -> Option; +} + +trait QueryAsn { + fn query_asn_for_ip(&self, address: IpAddr) -> Option; +} + +/* Converters */ + +pub fn extract_localized_name( +names: &Option>, + languages: &Vec) +-> Option { + match names { + Some(names) => { + for language in languages { + if let Some(name) = names.get(language.as_str()){ + return Some(name.to_string()) + } + } + return None + }, + None => None + } +} + +pub fn geoip2_city_to_named_location(item: geoip2::model::City, languages: &Vec) -> NamedLocation { + NamedLocation { + iso_code: None, + geoname_id: item.geoname_id, + name: extract_localized_name(&item.names, languages), + } +} + +pub fn geoip2_continent_to_named_location(item: geoip2::model::Continent, languages: &Vec) -> NamedLocation { + NamedLocation { + iso_code: item.code.map(ToString::to_string), + geoname_id: item.geoname_id, + name: extract_localized_name(&item.names, languages), + } +} + +pub fn geoip2_country_to_named_location(item: geoip2::model::Country, languages: &Vec) -> NamedLocation { + NamedLocation { + iso_code: item.iso_code.map(ToString::to_string), + geoname_id: item.geoname_id, + name: extract_localized_name(&item.names, languages), + } +} + +pub fn geoip2_represented_country_to_named_location(item: geoip2::model::RepresentedCountry, languages: &Vec) -> NamedLocation { + NamedLocation { + iso_code: item.iso_code.map(ToString::to_string), + geoname_id: item.geoname_id, + name: extract_localized_name(&item.names, languages), + } +} + +pub fn geoip2_subdivision_to_named_location(item: geoip2::model::Subdivision, languages: &Vec) -> NamedLocation { + NamedLocation { + iso_code: item.iso_code.map(ToString::to_string), + geoname_id: item.geoname_id, + name: extract_localized_name(&item.names, languages), + } +} + +/* Implementation */ + +impl QueryAsn for MMDBCarrier { + fn query_asn_for_ip(&self, address: IpAddr) -> Option { + match self.mmdb { + Some(mmdb) => { + match mmdb.lookup::(address) { + Ok(res) => { + Some(AsnResult { + asn: res.autonomous_system_number, + name: res.autonomous_system_organization.map(ToString::to_string), + }) + }, + Err(e) => { + println!("Error while looking up ASN for {address}: {e}"); + Default::default() + } + } + }, + None => None, + } + } +} + +impl QueryLocation for MMDBCarrier { + fn query_location_for_ip(&self, address: IpAddr, languages: &Vec) -> Option { + match self.mmdb { + Some(mmdb) => { + match mmdb.lookup::(address) { + Ok(res) => { + Some(LocationResult { + continent: + res.continent.map(|c| geoip2_continent_to_named_location(c, languages)), + country: + res.country.map(|c| geoip2_country_to_named_location(c, languages)), + registered_country: + res.registered_country.map(|c| geoip2_country_to_named_location(c, languages)), + represented_country: + res.represented_country.map(|c| geoip2_represented_country_to_named_location(c, languages)), + city: + res.city.map(|c| geoip2_city_to_named_location(c, languages)), + + subdivisions: match res.subdivisions { + Some(sds) => { + let subdivisions = Vec::new(); + subdivisions.reserve_exact(sds.len()); + for sd in sds { + subdivisions.push(geoip2_subdivision_to_named_location(sd, languages)); + } + Some(subdivisions) + }, + None => None, + }, + postal_code: match res.postal { + Some(p) => p.code.map(ToString::to_string), + None => None, + }, + time_zone: match res.location { + Some(loc) => loc.time_zone.map(ToString::to_string), + None => None, + }, + }) + }, + Err(e) => { + println!("Error while looking up ASN for {address}: {e}"); + Default::default() + } + } + }, + None => None, + } + } +} + diff --git a/src/main.rs b/src/main.rs index 63c3af9..e2551af 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,8 @@ use axum::{ Router, routing::get, }; +use maxminddb; +use maxminddb::geoip2; use tera::Tera; use trust_dns_resolver::{ TokioAsyncResolver, @@ -15,8 +17,13 @@ use trust_dns_resolver::{ use std::net::{IpAddr, Ipv4Addr}; use std::sync::Arc; + mod simple_dns; mod templating_engine; +mod geoip; + +use geoip::AsnResult; +use geoip::LocationResult; use crate::templating_engine::View; use crate::templating_engine::ResponseFormat; @@ -36,11 +43,14 @@ pub struct DigQuery { #[derive(serde::Deserialize, serde::Serialize)] pub struct IpResult { hostname: Option, + asn: Option, } struct ServiceSharedState { templating_engine: templating_engine::Engine, dns_resolver: TokioAsyncResolver, + mmdb_asn: Option>>, + mmdb_location: Option>>, } #[tokio::main] @@ -56,6 +66,23 @@ async fn main() { ::std::process::exit(1); } }; + + // Initalize GeoIP Database + + println!("Opening GeoIP ASN Databse ..."); + let mmdb_asn = maxminddb::Reader::open_readfile("mmdb/GeoLite2-ASN.mmdb"); + match mmdb_asn { + Ok(_) => { /* NOP */ }, + Err(ref e) => println!("Error while opening GeoIP ASN Databse: {e}"), + } + + println!("Opening GeoIP Location Databse ..."); + let mmdb_location = maxminddb::Reader::open_readfile("mmdb/GeoLite2-City.mmdb"); + match mmdb_location { + Ok(_) => { /* NOP */ }, + Err(ref e) => println!("Error while opening GeoIP Location Databse: {e}"), + } + // Initalize DNS resolver with os defaults println!("Initalizing dns resolver ..."); @@ -74,6 +101,8 @@ async fn main() { tera: tera, }, dns_resolver: dns_resolver, + mmdb_asn: mmdb_asn.ok(), + mmdb_location: mmdb_location.ok(), }); // Initalize axum server @@ -115,9 +144,47 @@ async fn handle_default_route( // do reverse lookup let hostname = simple_dns::reverse_lookup(&state.dns_resolver, &address); + + let asn_result = match &state.mmdb_asn { + Some(mmdb_asn) => { + match mmdb_asn.lookup::(address) { + Ok(res) => { + Some(AsnResult { + asn: res.autonomous_system_number, + name: res.autonomous_system_organization.map(ToString::to_string), + }) + } + Err(e) => { + println!("Error while looking up ASN for {address}: {e}"); + Default::default() + } + } + }, + None => None, + }; + + let location_result = match &state.mmdb_location { + Some(mmdb_location) => { + match mmdb_location.lookup::(address) { + Ok(res) => { + Some(AsnResult { + asn: res.autonomous_system_number, + name: res.autonomous_system_organization.map(ToString::to_string), + }) + } + Err(e) => { + println!("Error while looking up ASN for {address}: {e}"); + Default::default() + } + } + }, + None => None, + }; + let result = IpResult{ hostname: hostname.await, + asn: asn_result, }; state.templating_engine.render_view( @@ -136,7 +203,7 @@ async fn handle_dig_route( let format = dig_query.format.unwrap_or(ResponseFormat::TextHtml); let dig_result = simple_dns::lookup(&state.dns_resolver, name, true).await; - + state.templating_engine.render_view( format, View::Dig{ query: dig_query, result: dig_result} diff --git a/templates/index.html b/templates/index.html index a2cc952..8eb07dd 100644 --- a/templates/index.html +++ b/templates/index.html @@ -9,5 +9,8 @@ {% if hostname %}

Hostname: {{data.result.hostname}}

{% endif %} + {% if data.result.asn %} +

This IP is part of AS{{ data.result.asn.asn }}, which belongs to: {{ data.result.asn.name }}

+ {% endif %}