7 Commits

11 changed files with 298 additions and 50 deletions

1
Cargo.lock generated
View File

@ -372,6 +372,7 @@ dependencies = [
"idna 0.3.0",
"lazy_static",
"maxminddb",
"parking_lot",
"regex",
"serde",
"tera",

View File

@ -13,6 +13,7 @@ clap = { version = "4", features = ["derive"] }
governor = "0.5"
idna = "0.3"
lazy_static = "1.4.0"
parking_lot = "0.12"
regex = "1.7"
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }

View File

@ -44,6 +44,12 @@ The default templates should make use of everything exposed to the templating pa
The templates are covered by the AGPL as well, please share them with your users if you modified them.
### GeoLite2 database
For geolocation to work you need a MaxMind format database, for full functionality you need the GeoLite2-ASN and GeoLite2-City databses. Unfortunately you have to sign up with [MaxMind](https://maxmind.com) to obtain them. Once you have a license key there is a helper script in [contrib/maxmind-download.sh](contrib/maxmind-download.sh) that helps you with keeping the databse updated.
Since v1.0 echoip-slatecave reloads the databses when it rececieves a `USR1` signal.
## Security Considerations
### Information disclosure
@ -59,7 +65,7 @@ For a public service you should use a reverse proxy like Caddy, apache2 or nginx
### Denail of Service
`echoip-slatecave` has some simle ratelimiting built in (see the `[ratelimit]` section in the configuration file) this should help you with too frequest automated requests causung high load.
The default configuration is pretty liberal so that the average human probably won't notice the rate limit, but a misbehavin bot will be limited to one request every 3 seconds after 15 requests.
The default configuration is pretty liberal so that the average human probably won't notice the rate limit, but a misbehavingig bot will be limited to one request every 3 seconds after 15 requests.
## License

174
contrib/maxmind-download.sh Normal file
View File

@ -0,0 +1,174 @@
#!/bin/bash
# Settings variables go here
GEOIP_LICENSE_KEY="$GEOIP_LICENSE_KEY"
PRODUCTS=()
DESTINATION_DIRECTORY="."
DOWNLOAD_LOCATION=""
COLOR_OUTPUT=""
[ -t 1 ] && COLOR_OUTPUT="y"
msg() {
COLOR=""
COLOR_RST=""
if [ -n "$COLOR_OUTPUT" ] ; then
COLOR_RST="\e[00m"
case "$2" in
error|init_error) COLOR="\e[01;31m" ;;
success) COLOR="\e[32m" ;;
*) ;;
esac
fi
printf "$COLOR%s$COLOR_RST\n" "$1" >&2
}
show_help() {
cat <<EOF
Usage: maxmind-download.sh [OPTIONS]...
OPTIONS
--license-key <key>
Set the licencse key that is unfortunately
needed for a successful download.
--product <id>
Which product to download
maxmind calls this the EditionID
--GeoLite2-mmdb-all
Selects all the GeoLite2 Products in mmdb
format, hoefully saves some headaces.
Will download:
* GeoLite2-ASN
* GeoLite2-City
* GeoLite2-Country
--to <destination>
Directory to place the downloded files in.
Filename will be <product>.(mmdb|csv)
--download-to <destination>
Directory to download to.
If specified, the files in the --to directory
will only be replaced if the download was successful.
--color
--no-color Explicitly enable or disable colored output.
--help Show this help message
ENVOIRNMENT
GEOIP_LICENSE_KEY can be used to set the licencse key.
EXIT CODES
1 Invalid paramters or filesystem envoirnment
2 Download failed
3 Expected file not found in download
4 Failed to extract download
EOF
}
while [[ "$#" -gt 0 ]]; do
case "$1" in
--license-key) GEOIP_LICENSE_KEY="$2"; shift 2;;
--product) PRODUCTS+=("$2"); shift 2;;
--GeoLite2-mmdb-all)
PRODUCTS+=("GeoLite2-ASN")
PRODUCTS+=("GeoLite2-City")
PRODUCTS+=("GeoLite2-Country")
shift 1;;
--to) DESTINATION_DIRECTORY="$2"; shift 2;;
--download-to) DOWNLOAD_LOCATION="$2"; shift 2;;
--color) COLOR_OUTPUT="y"; shift 1;;
--no-color) COLOR_OUTPUT=""; shift 1;;
--help) show_help; exit 0;;
*) printf "Unknown option: %s\n" "$1"; exit 1;;
esac
done
if [ -z "$GEOIP_LICENSE_KEY" ] ; then
msg "No License Key specified, the download won't work this way." init_error
exit 1
fi
[ -n "$DOWNLOAD_LOCATION" ] || DOWNLOAD_LOCATION="$DESTINATION_DIRECTORY"
if [ -d "$DESTINATION_DIRECTORY" ] || mkdir -p "$DESTINATION_DIRECTORY" ; then
true
else
msg "Destination is not a directory and can't be created!" init_error
exit 1
fi
if [ -d "$DOWNLOAD_LOCATION" ] || mkdir -p "$DOWNLOAD_LOCATION" ; then
true
else
msg "Dowload location is not a directory and can't be created!" init_error
exit 1
fi
if [ "${#PRODUCTS[@]}" -eq "0" ] ; then
msg "No products specified, nothing to do." init_error
exit 0
fi
get_product_file_ext() {
if printf "%s" "$1" | grep -q 'CSV$' ; then
echo csv
else
echo mmdb
fi
}
# PrductID,DOWNLOAD_LOCATION
download_maxmind_db() {
msg "Downloading Database $1" progess
# the path to download to
dl="$2/$1.tar.gz"
curl -fsSL -m 40 "https://download.maxmind.com/app/geoip_download?edition_id=$1&license_key=$GEOIP_LICENSE_KEY&suffix=tar.gz" > "$dl"
if [ "_$?" != "_0" ] ; then
msg "Databse download of $1 failed!" error
rm "$dl"
return 2
fi
EXT="$(get_product_file_ext "$1")"
FILE_TO_EXTRACT="$(tar -tzf "$dl" | grep "/$1\.$EXT$")"
if [ -z "$FILE_TO_EXTRACT" ] ; then
msg "No .$EXT file found in the downloaded data!" error
rm "$dl"
return 3
fi
msg "Extracting $FILE_TO_EXTRACT from downloaded archive …" progess
if tar -C "$2" --strip-components=1 -xzf "$dl" "$FILE_TO_EXTRACT" ; then
msg "File extracted successfully." success
rm "$dl"
return 0
else
msg "File extraction failed!" error
rm "$dl"
return 4
fi
}
EXIT_CODE=""
MSG_OUTPUT_TO_LOG="y"
for product in "${PRODUCTS[@]}" ; do
download_maxmind_db "$product" "$DOWNLOAD_LOCATION"
RETCODE="$?"
if [ "_$RETCODE" = "_0" ] ; then
filename="$product.$(get_product_file_ext "$product")"
if [ "_$DOWNLOAD_LOCATION" != "_$DESTINATION_DIRECTORY" ] ; then
msg "Moving destination file …" progess
if [ -e "$DESTINATION_DIRECTORY/$filename" ] ; then
[ -e "$DESTINATION_DIRECTORY/$filename.bak" ] && rm "$DESTINATION_DIRECTORY/$filename.bak"
mv "$DESTINATION_DIRECTORY/$filename" "$DESTINATION_DIRECTORY/$filename.bak"
fi
if mv "$DOWNLOAD_LOCATION/$filename" "$DESTINATION_DIRECTORY/$filename" ; then
msg "File $filename installed successfully." success
else
msg "Failed to install $filename!" error
fi
fi
else
[ -n "$EXIT_CODE" ] && [ "$EXIT_CODE" -lt "$RETCODE" ] || export EXIT_CODE="$RETCODE"
fi
done
exit $EXIT_CODE

View File

@ -6,6 +6,8 @@
use maxminddb;
use maxminddb::geoip2;
use parking_lot::RwLock;
use std::collections::BTreeMap;
use std::net::IpAddr;
use std::path::Path;
@ -47,8 +49,9 @@ pub struct AsnResult {
}
pub struct MMDBCarrier {
pub mmdb: Option<maxminddb::Reader<Vec<u8>>>,
pub mmdb: RwLock<Option<maxminddb::Reader<Vec<u8>>>>,
pub name: String,
pub path: Option<String>,
}
pub trait QueryLocation {
@ -122,7 +125,8 @@ pub fn geoip2_subdivision_to_named_location(item: geoip2::city::Subdivision, lan
impl QueryAsn for MMDBCarrier {
fn query_asn_for_ip(&self, address: &IpAddr) -> Option<AsnResult> {
match &self.mmdb {
let mmdb = self.mmdb.read();
match &*mmdb {
Some(mmdb) => {
match mmdb.lookup::<geoip2::Asn>(*address) {
Ok(res) => {
@ -144,7 +148,8 @@ impl QueryAsn for MMDBCarrier {
impl QueryLocation for MMDBCarrier {
fn query_location_for_ip(&self, address: &IpAddr, languages: &Vec<&String>) -> Option<LocationResult> {
match &self.mmdb {
let mmdb = self.mmdb.read();
match &*mmdb {
Some(mmdb) => {
match mmdb.lookup::<geoip2::City>(*address) {
Ok(res) => {
@ -210,22 +215,38 @@ impl QueryLocation for MMDBCarrier {
}
impl MMDBCarrier {
pub fn load_database_from_path(&mut self, path: &Path) -> Result<(),maxminddb::MaxMindDBError> {
pub fn new(name: String, path: Option<String>) -> MMDBCarrier {
MMDBCarrier {
mmdb: RwLock::new(None),
name: name,
path: path,
}
}
pub fn reload_database(&self) -> Result<(),maxminddb::MaxMindDBError> {
match &self.path {
Some(path) => self.load_database_from_path(Path::new(&path)),
None => Ok(()),
}
}
pub fn load_database_from_path(&self, path: &Path) -> Result<(),maxminddb::MaxMindDBError> {
let mut mmdb = self.mmdb.write();
println!("Loading {} from '{}' ...", &self.name, path.display());
match maxminddb::Reader::open_readfile(path) {
Ok(reader) => {
let wording = if self.mmdb.is_some() {
let wording = if mmdb.is_some() {
"Replaced old"
} else {
"Loaded new"
};
self.mmdb = Some(reader);
*mmdb = Some(reader);
println!("{} {} with new one.", wording, &self.name);
Ok(())
},
Err(e) => {
println!("Error while reading {}: {}", &self.name, &e);
if self.mmdb.is_some() {
if mmdb.is_some() {
println!("Not replacing old database.");
}
Err(e)

View File

@ -27,10 +27,15 @@ use trust_dns_resolver::{
// config::ResolverConfig,
};
use tokio::signal::unix::{
signal,
SignalKind,
};
use tokio::task;
use std::fs;
use std::net::IpAddr;
use std::sync::Arc;
use std::path::Path;
mod config;
mod geoip;
@ -205,23 +210,19 @@ async fn main() {
// Initalize GeoIP Database
let mut asn_db = geoip::MMDBCarrier {
mmdb: None,
name: "GeoIP ASN Database".to_string(),
};
match &config.geoip.asn_database {
Some(path) => { asn_db.load_database_from_path(Path::new(&path)).ok(); },
None => {},
}
let asn_db = geoip::MMDBCarrier::new(
"GeoIP ASN Database".to_string(),
config.geoip.asn_database.clone()
);
let mut location_db = geoip::MMDBCarrier {
mmdb: None,
name: "GeoIP Location Database".to_string(),
};
match &config.geoip.location_database {
Some(path) => { location_db.load_database_from_path(Path::new(&path)).ok(); },
None => {},
}
asn_db.reload_database().ok();
let location_db = geoip::MMDBCarrier::new(
"GeoIP Location Database".to_string(),
config.geoip.location_database.clone()
);
location_db.reload_database().ok();
// Initalize DNS resolver with os defaults
println!("Initalizing dns resolver ...");
@ -250,6 +251,26 @@ async fn main() {
config: config.clone(),
});
let signal_usr1_handlers_state = shared_state.clone();
task::spawn(async move {
println!("Trying to register USR1 signal for reloading geoip databases");
let mut signal_stream = match signal(SignalKind::user_defined1()) {
Ok(signal_stream) => signal_stream,
Err(e) => {
println!("Error while registring signal handler: {e}");
println!("Continuing without ...");
return;
}
};
loop {
if None == signal_stream.recv().await { return; }
println!("Received signal USR1, reloading geoip databses!");
signal_usr1_handlers_state.location_db.reload_database().ok();
signal_usr1_handlers_state.asn_db.reload_database().ok();
}
});
// Initalize axum server
let app = Router::new()
.route("/", get(handle_default_route))

View File

@ -18,6 +18,8 @@ use trust_dns_resolver::{
TokioAsyncResolver,
};
use tokio::join;
use std::net::IpAddr;
@ -211,34 +213,50 @@ pub async fn lookup(
name: &String,
do_full_lookup: bool,
) -> DnsLookupResult {
let ipv4_lookup_res = resolver.lookup(name, RecordType::A);
let ipv6_lookup_res = resolver.lookup(name, RecordType::AAAA);
let cname_lookup_res = resolver.lookup(name, RecordType::CNAME);
let aname_lookup_res = resolver.lookup(name, RecordType::ANAME);
let (
ipv4_lookup_res,
ipv6_lookup_res,
cname_lookup_res,
aname_lookup_res
) = join!(
resolver.lookup(name, RecordType::A),
resolver.lookup(name, RecordType::AAAA),
resolver.lookup(name, RecordType::CNAME),
resolver.lookup(name, RecordType::ANAME),
);
// initlize an empty lookup result
let mut dig_result: DnsLookupResult = Default::default();
integrate_lookup_result(&mut dig_result, ipv4_lookup_res.await);
integrate_lookup_result(&mut dig_result, ipv6_lookup_res.await);
integrate_lookup_result(&mut dig_result, cname_lookup_res.await);
integrate_lookup_result(&mut dig_result, aname_lookup_res.await);
integrate_lookup_result(&mut dig_result, ipv4_lookup_res);
integrate_lookup_result(&mut dig_result, ipv6_lookup_res);
integrate_lookup_result(&mut dig_result, cname_lookup_res);
integrate_lookup_result(&mut dig_result, aname_lookup_res);
//Don't do an extented lookup if the domain seemingly doesn't exist
if do_full_lookup && !dig_result.nxdomain {
let mx_lookup_res = resolver.lookup(name, RecordType::MX);
let ns_lookup_res = resolver.lookup(name, RecordType::NS);
let soa_lookup_res = resolver.lookup(name, RecordType::SOA);
let caa_lookup_res = resolver.lookup(name, RecordType::CAA);
let srv_lookup_res = resolver.lookup(name, RecordType::SRV);
let txt_lookup_res = resolver.lookup(name, RecordType::TXT);
let (
mx_lookup_res,
ns_lookup_res,
soa_lookup_res,
caa_lookup_res,
srv_lookup_res,
txt_lookup_res
) = join!(
resolver.lookup(name, RecordType::MX),
resolver.lookup(name, RecordType::NS),
resolver.lookup(name, RecordType::SOA),
resolver.lookup(name, RecordType::CAA),
resolver.lookup(name, RecordType::SRV),
resolver.lookup(name, RecordType::TXT),
);
integrate_lookup_result(&mut dig_result, mx_lookup_res.await);
integrate_lookup_result(&mut dig_result, ns_lookup_res.await);
integrate_lookup_result(&mut dig_result, soa_lookup_res.await);
integrate_lookup_result(&mut dig_result, caa_lookup_res.await);
integrate_lookup_result(&mut dig_result, srv_lookup_res.await);
integrate_lookup_result(&mut dig_result, txt_lookup_res.await);
integrate_lookup_result(&mut dig_result, mx_lookup_res);
integrate_lookup_result(&mut dig_result, ns_lookup_res);
integrate_lookup_result(&mut dig_result, soa_lookup_res);
integrate_lookup_result(&mut dig_result, caa_lookup_res);
integrate_lookup_result(&mut dig_result, srv_lookup_res);
integrate_lookup_result(&mut dig_result, txt_lookup_res);
}

View File

@ -39,7 +39,7 @@
{% endif %}
{% if r.cname %}
<p>This domain has a cname set, this means its contents are full replaced by the linked record.</p>
<p>This domain has a <code>CNAME</code> set, this means its contents are full replaced by the linked record.</p>
<p class="button-paragraph">{{ helper::dig(extra=extra, name=r.cname[0]) }}</p>
@ -194,7 +194,7 @@
<h2>Programatic Lookup</h2>
<p>If you want to look up this information in another program the short answer is <b>don't, look up the names using your local DNS!</b></p>
<p>On most systems on the commandline you have commands like <code>host</code> and <code>dig</code> even when not present you can probably use <code>ping</code> as a workaround as it resolves the name and gives you the IP-Address it is pinging.</p>
<h3>Why queryting this service is still useful</h3>
<h3>Why querying this service is still useful</h3>
<p>This service most probably doesn't share its cache with your local resolver, this way you have a way to see if your DNS-change had the effect it should have.</p>
<p>It may also be useful for debugging other dns problems or to get around a local resolver that is lying to you because your ISP is a <i>something</i>.</p>
<h3>How?</h3>

View File

@ -60,7 +60,7 @@
{% endif %}
{% if r.location.coordinates %}
<dt>Coordinates</dt>
<dd><a href="{{ links::map_link(lat=r.location.coordinates.lat, lon=r.location.coordinates.lon)}}">lat: {{r.location.coordinates.lat}}, lon: {{r.location.coordinates.lon}}</a></dd>
<dd><a target="_blank" href="{{ links::map_link(lat=r.location.coordinates.lat, lon=r.location.coordinates.lon)}}">lat: {{r.location.coordinates.lat}}, lon: {{r.location.coordinates.lon}}</a></dd>
{% endif %}
</dl>
<!--We have to put that there to comply with maxminds licensing-->

View File

@ -3,6 +3,7 @@
<ul class="link-list">
<li><a target="_blank" href="https://apps.db.ripe.net/db-web-ui/query?bflag=true&dflag=false&rflag=true&searchtext={{ address }}&source=RIPE">… in the RIPE Database</a></li>
<li><a target="_blank" href="https://apps.db.ripe.net/db-web-ui/query?bflag=true&dflag=false&rflag=true&searchtext={{ address }}&source=GRS">… in the RIPE Global Resources Service</a></li>
<li><a target="_blank" href="https://client.rdap.org/?type=ip&object={{ address }}">… on client.rdap.org <small>(a modern whois, make sure to allow xhr to 3rd parties)</small></a></li>
<li><a target="_blank" href="https://www.shodan.io/host/{{ address }}">… on shodan.io <small>(limited querys per day, wants an account)</small></a></li>
<li><a target="_blank" href="https://search.censys.io/search?resource=hosts&sort=RELEVANCE&per_page=25&virtual_hosts=EXCLUDE&q=ip%3D{{ address }}">… on search.censys.io <small>(10 querys per day, wants an account)</small></a></li>
{% if not address is matching(":") %}
@ -18,6 +19,9 @@
<li><a target="_blank" href="https://www.shodan.io/domain/{{ name }}">… on shodan.io <small>(limited querys per day, wants an account)</small></a></li>
<li><a target="_blank" href="https://search.censys.io/search?resource=hosts&sort=RELEVANCE&per_page=25&virtual_hosts=EXCLUDE&q={{ name }}">… on search.censys.io <small>(10 querys per day, wants an account)</small></a></li>
<li><a target="_blank" href="https://www.virustotal.com/gui/domain/{{ name }}">… on virustotal.com</a></li>
<li><a target="_blank" href="https://observatory.mozilla.org/analyze/{{ name }}">… on the Mozilla Observatory (http and tls checks)</a></li>
<li><a target="_blank" href="https://internet.nl/site/{{ name }}">… on the Internet.nl Website test</a></li>
<li><a target="_blank" href="https://client.rdap.org/?type=domain&object={{ name }}">… on client.rdap.org <small>(a modern whois, make sure to allow xhr to 3rd parties)</small></a></li>
</ul>
{% endmacro domain_name_links %}
@ -27,6 +31,7 @@
<li><a target="_blank" href="https://bgp.he.net/AS{{asn}}">… on Hurricane Electric BGP Toolkit</a></li>
<li><a target="_blank" href="https://radar.qrator.net/as{{asn}}">… on radar.qrator.net (BGP Tool)</a></li>
<li><a target="_blank" href="https://search.censys.io/search?resource=hosts&sort=RELEVANCE&per_page=25&virtual_hosts=EXCLUDE&q=autonomous_system.asn%3D{{asn}}">… on search.censys.io <small>(10 querys per day, wants an account)</small></a></li>
<li><a target="_blank" href="https://client.rdap.org/?type=autnum&object={{ asn }}">… on client.rdap.org <small>(a modern whois, make sure to allow xhr to 3rd parties)</small></a></li>
<li><a target="_blank" href="https://query.wikidata.org/#%23Select%20Wikipedia%20articles%20that%20belong%20to%20a%20given%20asn%0ASELECT%20DISTINCT%20%3Fitem%20%3Fwebsite%20%3FitemLabel%20%3FitemDescription%20%3Flang%20%3Farticle%20WHERE%20%7B%0A%20%20VALUES%20%3Fasn%20%7B%0A%20%20%20%20%22{{ asn }}%22%0A%20%20%7D%0A%20%20%3Fasn%20%5Ewdt%3AP3797%20%3Fitem.%0A%20%20OPTIONAL%20%7B%20%3Fitem%20wdt%3AP856%20%3Fwebsite.%20%7D%0A%20%20OPTIONAL%20%7B%0A%20%20%20%20%3Fitem%20%5Eschema%3Aabout%20%3Farticle.%0A%20%20%20%20%3Farticle%20schema%3AisPartOf%20_%3Ab64.%0A%20%20%20%20_%3Ab64%20wikibase%3AwikiGroup%20%22wikipedia%22.%0A%20%20%20%20%3Farticle%20schema%3AinLanguage%20%3Flang%3B%0A%20%20%20%20%20%20schema%3Aname%20%3Farticlename.%0A%20%20%20%20FILTER(((%3Flang%20%3D%20%22%5BAUTO_LANGUAGE%5D%22)%20%7C%7C%20(%3Flang%20%3D%20%22en%22))%20%7C%7C%20(%3Flang%20%3D%20%22de%22))%0A%20%20%7D%0A%20%20SERVICE%20wikibase%3Alabel%20%7B%0A%20%20%20%20bd%3AserviceParam%20wikibase%3Alanguage%20%22%5BAUTO_LANGUAGE%5D%2Cen%22.%0A%20%20%20%20%3Fitem%20rdfs%3Alabel%20%3FitemLabel%3B%0A%20%20%20%20%20%20schema%3Adescription%20%3FitemDescription.%0A%20%20%7D%0A%7D%0AORDER%20BY%20(UCASE(%3FitemLabel))">… on Wikidata and Wikipedia <small>(Press the run button in the sidebar to get results)</small></a></li>
</ul>
{% endmacro asn_links %}

View File

@ -1,2 +1,3 @@
User-agent: *
Disallow: /*?
Disallow: /ip/
Disallow: /dig/