12 Commits

Author SHA1 Message Date
d5c5d30d32 cargo update 2025-03-20 00:04:34 +01:00
38be0c05d0 Add links for looking up results on AbuseIPDB, CrowdSec and Spamhaus 2025-03-19 22:03:50 +01:00
e81ce74a2f Upgrade to hickory 0.25
This also solves a security advisory that could have lead to a denail of service via the ring crate.
2025-03-19 21:19:05 +01:00
7130c0d94a Version bump to 1.6.0 2025-02-09 16:41:38 +01:00
47fad2007b Add privacy recommendation for reverse proxies 2025-02-09 16:37:10 +01:00
ba34caf8fc Log not found errors to the debug channel
They are part of normal operation and shouldn't be logged in production.
2025-02-09 16:27:12 +01:00
caf47522e4 Use a fallback for when the requested dns resolver isn't available 2025-02-09 16:10:35 +01:00
b98bb67b4c Make clippy happy
Mostly cleaning up type system crimes from when I was still learning rust:
* Abuse of `match` and loops
* Non-use of helper functions (`is_empty`, `is_none`)
* Use of borrowed owned types (`&String`)
* Implementing `Into` instead of `From`
2025-02-09 15:45:23 +01:00
d902dae35d Use the log create instead of println 2025-02-09 15:11:17 +01:00
2aae2d6626 Version bump to 1.5.3 2025-02-09 14:51:44 +01:00
4079e24c43 cargo update 2025-02-09 14:47:49 +01:00
2b0c4eb3fb Updated to lib-humus 0.3 and axum 0.8 2025-02-09 14:41:12 +01:00
14 changed files with 1191 additions and 554 deletions

1285
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,30 +1,32 @@
[package] [package]
name = "echoip-slatecave" name = "echoip-slatecave"
version = "1.5.2" version = "1.6.0"
edition = "2021" edition = "2021"
authors = ["Slatian <baschdel@disroot.org>"] authors = ["Slatian <baschdel@disroot.org>"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
lib-humus = { version="0.2", features=["axum-view+cookie"] } lib-humus = { version="0.3", features=["axum-view+cookie"] }
axum = { version = "0.7", features = ["macros"] } axum-client-ip = "0.7"
axum-extra = { version = "0.9", features = ["cookie", "typed-header"] } axum-extra = { version = "0.10", features = ["cookie", "typed-header"] }
axum-client-ip = "0.6" axum = { version = "0.8", features = ["macros"] }
clap = { version = "4.5", features = ["derive"] } clap = { version = "4.5", features = ["derive"] }
env_logger = "0.11"
governor = "0.8" governor = "0.8"
hickory-proto = "0.25"
hickory-resolver = { version = "0.25", features = ["tls-ring","https-ring","quic-ring","rustls-platform-verifier","system-config"] }
http = "1.2"
idna = "1.0" idna = "1.0"
log = "0.4"
maxminddb = "0.24"
mime = "0.3"
parking_lot = "0.12" parking_lot = "0.12"
regex = "1.11" regex = "1.11"
serde = { version = "1", features = ["derive","rc"] } serde = { version = "1", features = ["derive","rc"] }
tokio = { version = "1", features = ["macros","signal"] }
tera = "1" tera = "1"
tokio = { version = "1", features = ["macros","signal"] }
toml = "0.8" toml = "0.8"
tower = "0.5" tower = "0.5"
tower-http = { version = "0.6", features = ["fs"] } tower-http = { version = "0.6", features = ["fs"] }
hickory-proto = "0.24"
hickory-resolver = { version = "0.24", features = ["dns-over-rustls","dns-over-https","dns-over-quic","native-certs"] }
maxminddb = "0.24"
mime = "0.3"
http = "1.2"

View File

@ -98,6 +98,10 @@ Most noably you can disable reverse dns lookups, hide domains with given suffixe
`echoip-slatecave` only exposes an unencrypted http interface to keep the service itself simple. `echoip-slatecave` only exposes an unencrypted http interface to keep the service itself simple.
For a public service you should use a reverse proxy like Caddy, apache2 or nginx and configure the `ip_header` option, see the echoip_config.toml file. Usually the preconfigured `RightmostXForwardedFor` is the correct one, but please doublecheck it matches your servers configuration, it should fail by simply not working, but no guarantees given. For a public service you should use a reverse proxy like Caddy, apache2 or nginx and configure the `ip_header` option, see the echoip_config.toml file. Usually the preconfigured `RightmostXForwardedFor` is the correct one, but please doublecheck it matches your servers configuration, it should fail by simply not working, but no guarantees given.
Consider hiding the values of the following in your server logs for increased privacy:
* The `query` URL query paramter
* All paths subpath to `/ip/` and `/dig/`
### Denail of Service ### 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. `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.

View File

@ -1,5 +1,5 @@
use serde::{Deserialize,Serialize}; use serde::{Deserialize,Serialize};
use hickory_resolver::config::Protocol; use hickory_proto::xfer::Protocol;
use hickory_resolver::config::ResolverConfig as HickoryResolverConfig; use hickory_resolver::config::ResolverConfig as HickoryResolverConfig;
use hickory_resolver::config::NameServerConfig; use hickory_resolver::config::NameServerConfig;
@ -43,6 +43,7 @@ pub struct DnsResolverConfig {
pub servers: Vec<SocketAddr>, pub servers: Vec<SocketAddr>,
pub protocol: DnsProtocol, pub protocol: DnsProtocol,
pub tls_dns_name: Option<Arc<str>>, pub tls_dns_name: Option<Arc<str>>,
pub http_endpoint: Option<Arc<str>>,
#[serde(skip_serializing)] //Don't leak our bind address to the outside #[serde(skip_serializing)] //Don't leak our bind address to the outside
pub bind_address: Option<SocketAddr>, pub bind_address: Option<SocketAddr>,
#[serde(default="default_true", alias="trust_nx_responses")] #[serde(default="default_true", alias="trust_nx_responses")]
@ -73,14 +74,14 @@ impl Default for DnsConfig {
} }
} }
impl Into<Protocol> for DnsProtocol { impl From<DnsProtocol> for Protocol {
fn into(self) -> Protocol { fn from(value: DnsProtocol) -> Self {
match self { match value {
Self::Udp => Protocol::Udp, DnsProtocol::Udp => Protocol::Udp,
Self::Tcp => Protocol::Tcp, DnsProtocol::Tcp => Protocol::Tcp,
Self::Tls => Protocol::Tls, DnsProtocol::Tls => Protocol::Tls,
Self::Https => Protocol::Https, DnsProtocol::Https => Protocol::Https,
Self::Quic => Protocol::Quic, DnsProtocol::Quic => Protocol::Quic,
} }
} }
} }
@ -95,8 +96,8 @@ impl DnsResolverConfig {
socket_addr: *server, socket_addr: *server,
protocol: self.protocol.clone().into(), protocol: self.protocol.clone().into(),
tls_dns_name: self.tls_dns_name.clone().map(|s| s.to_string()), tls_dns_name: self.tls_dns_name.clone().map(|s| s.to_string()),
http_endpoint: self.http_endpoint.as_deref().map(ToString::to_string),
trust_negative_responses: self.trust_negative_responses, trust_negative_responses: self.trust_negative_responses,
tls_config: None,
bind_addr: self.bind_address, bind_addr: self.bind_address,
}); });
} }

View File

@ -3,9 +3,10 @@
* that provides the results ready for templating. * that provides the results ready for templating.
*/ */
use maxminddb; use log::{debug,info,warn,error};
use maxminddb::geoip2; use maxminddb::geoip2;
use maxminddb::MaxMindDBError::AddressNotFoundError;
use parking_lot::RwLock; use parking_lot::RwLock;
use std::collections::BTreeMap; use std::collections::BTreeMap;
@ -55,7 +56,7 @@ pub struct MMDBCarrier {
} }
pub trait QueryLocation { pub trait QueryLocation {
fn query_location_for_ip(&self, address: &IpAddr, laguages: &Vec<&String>) -> Option<LocationResult>; fn query_location_for_ip(&self, address: &IpAddr, laguages: &[&str]) -> Option<LocationResult>;
} }
pub trait QueryAsn { pub trait QueryAsn {
@ -66,12 +67,12 @@ pub trait QueryAsn {
pub fn extract_localized_name( pub fn extract_localized_name(
names: &Option<BTreeMap<&str, &str>>, names: &Option<BTreeMap<&str, &str>>,
languages: &Vec<&String>) languages: &[&str])
-> Option<String> { -> Option<String> {
match names { match names {
Some(names) => { Some(names) => {
for language in languages { for language in languages {
if let Some(name) = names.get(language.as_str()){ if let Some(name) = names.get(language){
return Some(name.to_string()) return Some(name.to_string())
} }
} }
@ -81,7 +82,7 @@ names: &Option<BTreeMap<&str, &str>>,
} }
} }
pub fn geoip2_city_to_named_location(item: geoip2::city::City, languages: &Vec<&String>) -> NamedLocation { pub fn geoip2_city_to_named_location(item: geoip2::city::City, languages: &[&str]) -> NamedLocation {
NamedLocation { NamedLocation {
iso_code: None, iso_code: None,
geoname_id: item.geoname_id, geoname_id: item.geoname_id,
@ -89,7 +90,7 @@ pub fn geoip2_city_to_named_location(item: geoip2::city::City, languages: &Vec<&
} }
} }
pub fn geoip2_continent_to_named_location(item: geoip2::country::Continent, languages: &Vec<&String>) -> NamedLocation { pub fn geoip2_continent_to_named_location(item: geoip2::country::Continent, languages: &[&str]) -> NamedLocation {
NamedLocation { NamedLocation {
iso_code: item.code.map(ToString::to_string), iso_code: item.code.map(ToString::to_string),
geoname_id: item.geoname_id, geoname_id: item.geoname_id,
@ -97,7 +98,7 @@ pub fn geoip2_continent_to_named_location(item: geoip2::country::Continent, lang
} }
} }
pub fn geoip2_country_to_named_location(item: geoip2::country::Country, languages: &Vec<&String>) -> NamedLocation { pub fn geoip2_country_to_named_location(item: geoip2::country::Country, languages: &[&str]) -> NamedLocation {
NamedLocation { NamedLocation {
iso_code: item.iso_code.map(ToString::to_string), iso_code: item.iso_code.map(ToString::to_string),
geoname_id: item.geoname_id, geoname_id: item.geoname_id,
@ -105,7 +106,7 @@ pub fn geoip2_country_to_named_location(item: geoip2::country::Country, language
} }
} }
pub fn geoip2_represented_country_to_named_location(item: geoip2::country::RepresentedCountry, languages: &Vec<&String>) -> NamedLocation { pub fn geoip2_represented_country_to_named_location(item: geoip2::country::RepresentedCountry, languages: &[&str]) -> NamedLocation {
NamedLocation { NamedLocation {
iso_code: item.iso_code.map(ToString::to_string), iso_code: item.iso_code.map(ToString::to_string),
geoname_id: item.geoname_id, geoname_id: item.geoname_id,
@ -113,7 +114,7 @@ pub fn geoip2_represented_country_to_named_location(item: geoip2::country::Repre
} }
} }
pub fn geoip2_subdivision_to_named_location(item: geoip2::city::Subdivision, languages: &Vec<&String>) -> NamedLocation { pub fn geoip2_subdivision_to_named_location(item: geoip2::city::Subdivision, languages: &[&str]) -> NamedLocation {
NamedLocation { NamedLocation {
iso_code: item.iso_code.map(ToString::to_string), iso_code: item.iso_code.map(ToString::to_string),
geoname_id: item.geoname_id, geoname_id: item.geoname_id,
@ -135,9 +136,15 @@ impl QueryAsn for MMDBCarrier {
name: res.autonomous_system_organization.map(ToString::to_string), name: res.autonomous_system_organization.map(ToString::to_string),
}) })
}, },
Err(AddressNotFoundError(_)) => {
// Log to the debug channel.
// This isn't severe, and shouldn't be logged in production.
debug!("ASN not found in database for {address}.");
None
},
Err(e) => { Err(e) => {
println!("Error while looking up ASN for {address}: {e}"); error!("Error while looking up ASN for {address}: {e}");
Default::default() None
} }
} }
}, },
@ -147,7 +154,7 @@ impl QueryAsn for MMDBCarrier {
} }
impl QueryLocation for MMDBCarrier { impl QueryLocation for MMDBCarrier {
fn query_location_for_ip(&self, address: &IpAddr, languages: &Vec<&String>) -> Option<LocationResult> { fn query_location_for_ip(&self, address: &IpAddr, languages: &[&str]) -> Option<LocationResult> {
let mmdb = self.mmdb.read(); let mmdb = self.mmdb.read();
match &*mmdb { match &*mmdb {
Some(mmdb) => { Some(mmdb) => {
@ -203,9 +210,15 @@ impl QueryLocation for MMDBCarrier {
}, },
}) })
}, },
Err(AddressNotFoundError(_)) => {
// Log to the debug channel.
// This isn't severe, and shouldn't be logged in production.
debug!("IP location not found in database for {address}");
None
},
Err(e) => { Err(e) => {
println!("Error while looking up ASN for {address}: {e}"); error!("Error while looking up IP location for {address}: {e}");
Default::default() None
} }
} }
}, },
@ -232,7 +245,7 @@ impl MMDBCarrier {
pub fn load_database_from_path(&self, path: &Path) -> Result<(),maxminddb::MaxMindDBError> { pub fn load_database_from_path(&self, path: &Path) -> Result<(),maxminddb::MaxMindDBError> {
let mut mmdb = self.mmdb.write(); let mut mmdb = self.mmdb.write();
println!("Loading {} from '{}' ...", &self.name, path.display()); info!("Loading {} from '{}' ...", &self.name, path.display());
match maxminddb::Reader::open_readfile(path) { match maxminddb::Reader::open_readfile(path) {
Ok(reader) => { Ok(reader) => {
let wording = if mmdb.is_some() { let wording = if mmdb.is_some() {
@ -241,13 +254,13 @@ impl MMDBCarrier {
"Loaded new" "Loaded new"
}; };
*mmdb = Some(reader); *mmdb = Some(reader);
println!("{} {} with new one.", wording, &self.name); info!("{} {} with new one.", wording, &self.name);
Ok(()) Ok(())
}, },
Err(e) => { Err(e) => {
println!("Error while reading {}: {}", &self.name, &e); error!("Error while reading {}: {}", &self.name, &e);
if mmdb.is_some() { if mmdb.is_some() {
println!("Not replacing old database."); warn!("Not replacing old database.");
} }
Err(e) Err(e)
}, },

View File

@ -14,7 +14,7 @@ pub enum NameType {
#[default] #[default]
Ascii, Ascii,
Unicode, Unicode,
IDN, Idn,
} }
// Note, that the // Note, that the
@ -32,8 +32,8 @@ pub struct IdnaName {
} }
impl IdnaName { impl IdnaName {
pub fn from_string(s: &String) -> Self { pub fn from_str(s: &str) -> Self {
if s == "" { if s.is_empty() {
return Default::default(); return Default::default();
} }
@ -41,17 +41,17 @@ impl IdnaName {
let unicode: String; let unicode: String;
let decoder_error; let decoder_error;
if s.starts_with("xn--") && s.is_ascii() { if s.starts_with("xn--") && s.is_ascii() {
original_was = NameType::IDN; original_was = NameType::Idn;
let (uc, ures) = idna::domain_to_unicode(s); let (uc, ures) = idna::domain_to_unicode(s);
unicode = uc; unicode = uc;
decoder_error = ures.map_or_else(|e| Some(e.to_string()), |_| None); decoder_error = ures.map_or_else(|e| Some(e.to_string()), |_| None);
} else { } else {
unicode = s.clone(); unicode = s.to_owned();
decoder_error = None; decoder_error = None;
}; };
let (idn, encoder_error) = match idna::domain_to_ascii(s) { let (idn, encoder_error) = match idna::domain_to_ascii(s) {
Ok(idn) => { Ok(idn) => {
if &idn != s || original_was == NameType::IDN { if idn != s || original_was == NameType::Idn {
(Some(idn), None) (Some(idn), None)
} else { } else {
original_was = NameType::Ascii; original_was = NameType::Ascii;

View File

@ -28,6 +28,7 @@ pub enum AddressScope {
Loopback, Loopback,
Reserved, Reserved,
Documentation, Documentation,
Nat64,
#[default] #[default]
Unknown, Unknown,
} }
@ -78,6 +79,9 @@ impl AddressInfo {
// Test for the documentation address 2001:db8::/32 // Test for the documentation address 2001:db8::/32
} else if segments[0]==0x2001 && segments[1]==0x0db8 && segments[2]==0 && segments[3]==0 { } else if segments[0]==0x2001 && segments[1]==0x0db8 && segments[2]==0 && segments[3]==0 {
address_scope = AddressScope::Documentation; address_scope = AddressScope::Documentation;
// Test for NAT64 address 64:ff9b::/96
} else if segments[0]==0x64 && segments[1]==0xff9b {
address_scope = AddressScope::Nat64;
// Test for multicase scope // Test for multicase scope
} else if addr.is_multicast() { } else if addr.is_multicast() {
address_cast = AddressCast::Multicast; address_cast = AddressCast::Multicast;
@ -111,6 +115,7 @@ impl AddressInfo {
scope: address_scope scope: address_scope
} }
} }
} }

View File

@ -1,3 +1,7 @@
#![allow(clippy::redundant_field_names)]
#![allow(clippy::needless_return)]
use axum::{ use axum::{
body::Body, body::Body,
extract::{ extract::{
@ -17,12 +21,15 @@ use axum_client_ip::SecureClientIp;
use axum_extra::headers; use axum_extra::headers;
use axum_extra::TypedHeader; use axum_extra::TypedHeader;
use clap::Parser; use clap::Parser;
use env_logger::Env;
use hickory_resolver::{name_server::TokioConnectionProvider, system_conf::read_system_conf, Name, ResolveError, Resolver};
use hickory_resolver::TokioResolver;
use log::{info,warn,error};
use nat64::resolve_nat64_address;
use regex::Regex; use regex::Regex;
use serde::{Deserialize,Serialize}; use serde::{Deserialize,Serialize};
use tower::ServiceBuilder;
use tower_http::services::ServeDir; use tower_http::services::ServeDir;
use hickory_resolver::Name; use tower::ServiceBuilder;
use hickory_resolver::TokioAsyncResolver;
use tokio::signal::unix::{ use tokio::signal::unix::{
signal, signal,
@ -44,6 +51,7 @@ mod config;
mod geoip; mod geoip;
mod idna; mod idna;
mod ipinfo; mod ipinfo;
mod nat64;
mod ratelimit; mod ratelimit;
mod settings; mod settings;
mod simple_dns; mod simple_dns;
@ -59,7 +67,7 @@ use crate::idna::IdnaName;
use crate::simple_dns::DnsLookupResult; use crate::simple_dns::DnsLookupResult;
use crate::settings::*; use crate::settings::*;
use crate::view::View; use crate::view::View;
use crate::ipinfo::{AddressCast,AddressInfo,AddressScope}; use crate::ipinfo::{AddressInfo,AddressScope};
type TemplatingEngine = HumusEngine<View,QuerySettings,ResponseFormat>; type TemplatingEngine = HumusEngine<View,QuerySettings,ResponseFormat>;
@ -76,8 +84,28 @@ pub struct SearchQuery {
query: Option<String>, query: Option<String>,
} }
/// Enumerates possible mapping strategies
#[derive(Deserialize, Serialize, Clone)]
#[serde(rename_all="snake_case")]
pub enum IpMappingStrategy {
/// See: https://en.wikipedia.org/wiki/NAT64
Nat64,
}
#[derive(Serialize, Clone)]
pub struct IpMapping {
strategy: IpMappingStrategy,
from_address: IpAddr,
to_address: IpAddr,
}
#[derive(Serialize, Clone)] #[derive(Serialize, Clone)]
pub struct IpResult { pub struct IpResult {
/// When the mapping is set the queried for address
/// was automtically replaced with the mapped to address.
mapping: Option<IpMapping>,
/// The address that was queried for or the mapping resulted in.
address: IpAddr, address: IpAddr,
hostname: Option<String>, hostname: Option<String>,
asn: Option<AsnResult>, asn: Option<AsnResult>,
@ -102,7 +130,7 @@ pub struct DigResult {
struct ServiceSharedState { struct ServiceSharedState {
templating_engine: TemplatingEngine, templating_engine: TemplatingEngine,
dns_resolvers: HashMap<Arc<str>,TokioAsyncResolver>, dns_resolvers: HashMap<Arc<str>,TokioResolver>,
dns_resolver_aliases: HashMap<Arc<str>,Arc<str>>, dns_resolver_aliases: HashMap<Arc<str>,Arc<str>>,
asn_db: geoip::MMDBCarrier, asn_db: geoip::MMDBCarrier,
location_db: geoip::MMDBCarrier, location_db: geoip::MMDBCarrier,
@ -131,7 +159,7 @@ struct CliArgs {
static_location: Option<String>, static_location: Option<String>,
} }
fn match_domain_hidden_list(domain: &String, hidden_list: &Vec<String>) -> bool { fn match_domain_hidden_list(domain: &str, hidden_list: &Vec<String>) -> bool {
let name = domain.trim_end_matches("."); let name = domain.trim_end_matches(".");
for suffix in hidden_list { for suffix in hidden_list {
if name.ends_with(suffix) { if name.ends_with(suffix) {
@ -143,6 +171,10 @@ fn match_domain_hidden_list(domain: &String, hidden_list: &Vec<String>) -> bool
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
// Initalize logger:
env_logger::Builder::from_env(Env::default().default_filter_or("info")).init();
// Parse Command line arguments // Parse Command line arguments
let cli_args = CliArgs::parse(); let cli_args = CliArgs::parse();
@ -152,9 +184,9 @@ async fn main() {
match read_toml_from_file::<config::EchoIpServiceConfig>(&config_path) { match read_toml_from_file::<config::EchoIpServiceConfig>(&config_path) {
Ok(c) => c, Ok(c) => c,
Err(e) => { Err(e) => {
println!("Could not read confuration file!"); error!("Could not read confuration file!");
println!("{e}"); error!("{e}");
println!("Exiting ..."); error!("Exiting ...");
::std::process::exit(1); ::std::process::exit(1);
} }
} }
@ -174,7 +206,7 @@ async fn main() {
let templating_engine = match template_loader.load_templates() { let templating_engine = match template_loader.load_templates() {
Ok(t) => t.into(), Ok(t) => t.into(),
Err(e) => { Err(e) => {
println!("{e}"); error!("{e}");
::std::process::exit(1); ::std::process::exit(1);
} }
}; };
@ -183,7 +215,7 @@ async fn main() {
let static_file_directory = template_loader.base_dir()+"/static"; let static_file_directory = template_loader.base_dir()+"/static";
println!("Static files will be served from: {static_file_directory}"); info!("Static files will be served from: {static_file_directory}");
// Initalize GeoIP Database // Initalize GeoIP Database
@ -202,24 +234,27 @@ async fn main() {
location_db.reload_database().ok(); location_db.reload_database().ok();
// Initalize DNS resolver with os defaults // Initalize DNS resolver with os defaults
println!("Initalizing dns resolvers ..."); info!("Initalizing dns resolvers ...");
let mut dns_resolver_selectables = Vec::<Selectable>::new(); let mut dns_resolver_selectables = Vec::<Selectable>::new();
let mut dns_resolver_map: HashMap<Arc<str>,TokioAsyncResolver> = HashMap::new(); let mut dns_resolver_map: HashMap<Arc<str>,TokioResolver> = HashMap::new();
let mut dns_resolver_aliases: HashMap<Arc<str>,Arc<str>> = HashMap::new(); let mut dns_resolver_aliases: HashMap<Arc<str>,Arc<str>> = HashMap::new();
if config.dns.enable_system_resolver { if config.dns.enable_system_resolver {
println!("Initalizing System resolver ..."); info!("Initalizing System resolver ...");
let res = TokioAsyncResolver::tokio_from_system_conf(); match initalize_system_resolver() {
let resolver = match res { Ok(resolver) => {
Ok(resolver) => resolver, info!("System resolver successfully Initalized.");
dns_resolver_map.insert(
config.dns.system_resolver_id.clone(),
resolver
);
},
Err(e) => { Err(e) => {
println!("Error while setting up dns resolver: {e}"); error!("Problem setting up system resolver: {e}");
::std::process::exit(1); ::std::process::exit(1);
} }
}; };
dns_resolver_map.insert(config.dns.system_resolver_id.clone(), resolver);
dns_resolver_selectables.push(Selectable { dns_resolver_selectables.push(Selectable {
id: config.dns.system_resolver_id.clone(), id: config.dns.system_resolver_id.clone(),
name: config.dns.system_resolver_name.clone(), name: config.dns.system_resolver_name.clone(),
@ -228,11 +263,11 @@ async fn main() {
} }
for (key, resolver_config) in &config.dns.resolver { for (key, resolver_config) in &config.dns.resolver {
println!("Initalizing {} resolver ...", key); info!("Initalizing {} resolver ...", key);
let resolver = TokioAsyncResolver::tokio( let resolver = TokioResolver::builder_with_config(
resolver_config.to_hickory_resolver_config(), resolver_config.to_hickory_resolver_config(),
Default::default() Default::default()
); ).build();
dns_resolver_map.insert(key.clone(), resolver); dns_resolver_map.insert(key.clone(), resolver);
dns_resolver_selectables.push(Selectable { dns_resolver_selectables.push(Selectable {
id: key.clone(), id: key.clone(),
@ -259,7 +294,7 @@ async fn main() {
}); });
dns_resolver_selectables.sort_by(|a,b| b.weight.cmp(&a.weight)); dns_resolver_selectables.sort_by(|a,b| b.weight.cmp(&a.weight));
let default_resolver = dns_resolver_selectables.get(0) let default_resolver = dns_resolver_selectables.first()
.map(|s| s.id.clone() ) .map(|s| s.id.clone() )
.unwrap_or("none".into()); .unwrap_or("none".into());
let derived_config = DerivedConfiguration { let derived_config = DerivedConfiguration {
@ -270,18 +305,18 @@ async fn main() {
let signal_usr1_handlers_state = shared_state.clone(); let signal_usr1_handlers_state = shared_state.clone();
task::spawn(async move { task::spawn(async move {
println!("Trying to register USR1 signal for reloading geoip databases"); info!("Trying to register USR1 signal for reloading geoip databases");
let mut signal_stream = match signal(SignalKind::user_defined1()) { let mut signal_stream = match signal(SignalKind::user_defined1()) {
Ok(signal_stream) => signal_stream, Ok(signal_stream) => signal_stream,
Err(e) => { Err(e) => {
println!("Error while registring signal handler: {e}"); error!("Error while registring signal handler: {e}");
println!("Continuing without ..."); warn!("Continuing without geoip reaload signal ...");
return; return;
} }
}; };
loop { loop {
if None == signal_stream.recv().await { return; } if signal_stream.recv().await.is_none() { return; }
println!("Received signal USR1, reloading geoip databses!"); info!("Received signal USR1, reloading geoip databses!");
signal_usr1_handlers_state.location_db.reload_database().ok(); signal_usr1_handlers_state.location_db.reload_database().ok();
signal_usr1_handlers_state.asn_db.reload_database().ok(); signal_usr1_handlers_state.asn_db.reload_database().ok();
} }
@ -290,9 +325,9 @@ async fn main() {
// Initalize axum server // Initalize axum server
let app = Router::new() let app = Router::new()
.route("/", get(handle_default_route)) .route("/", get(handle_default_route))
.route("/dig/:name", get(handle_dig_route_with_path)) .route("/dig/{name}", get(handle_dig_route_with_path))
.route("/ip/:address", get(handle_ip_route_with_path)) .route("/ip/{address}", get(handle_ip_route_with_path))
.route("/dns_resolver/:resolver", get(handle_dns_resolver_route_with_path)) .route("/dns_resolver/{resolver}", get(handle_dns_resolver_route_with_path))
.route("/dns_resolver", get(handle_dns_resolver_route)) .route("/dns_resolver", get(handle_dns_resolver_route))
.route("/ua", get(user_agent_handler)) .route("/ua", get(user_agent_handler))
.route("/hi", get(hello_world_handler)) .route("/hi", get(hello_world_handler))
@ -300,7 +335,7 @@ async fn main() {
ServeDir::new(static_file_directory) ServeDir::new(static_file_directory)
.fallback(not_found_handler.with_state(shared_state.clone())) .fallback(not_found_handler.with_state(shared_state.clone()))
) )
.with_state(shared_state) .with_state(shared_state.clone())
.layer( .layer(
ServiceBuilder::new() ServiceBuilder::new()
.layer(ip_header.into_extension()) .layer(ip_header.into_extension())
@ -309,11 +344,11 @@ async fn main() {
.layer(middleware::from_fn(ratelimit::rate_limit_middleware)) .layer(middleware::from_fn(ratelimit::rate_limit_middleware))
.layer(Extension(config)) .layer(Extension(config))
.layer(Extension(derived_config)) .layer(Extension(derived_config))
.layer(middleware::from_fn(settings_query_middleware)) .layer(middleware::from_fn_with_state(shared_state, settings_query_middleware))
) )
; ;
println!("Starting Server on {} ...",listen_on); info!("Starting Server on {} ...",listen_on);
let listener = tokio::net::TcpListener::bind(&listen_on).await.unwrap(); let listener = tokio::net::TcpListener::bind(&listen_on).await.unwrap();
axum::serve(listener, app.into_make_service_with_connect_info::<std::net::SocketAddr>()) axum::serve(listener, app.into_make_service_with_connect_info::<std::net::SocketAddr>())
@ -321,27 +356,50 @@ async fn main() {
.unwrap(); .unwrap();
} }
fn initalize_system_resolver() -> Result<TokioResolver, ResolveError> {
let (system_conf, system_options) = read_system_conf()?;
let mut builder = Resolver::builder_with_config(
system_conf,
TokioConnectionProvider::default()
);
*builder.options_mut() = system_options;
return Ok(builder.build());
}
#[allow(clippy::too_many_arguments)]
async fn settings_query_middleware( async fn settings_query_middleware(
Query(query): Query<SettingsQuery>, Query(query): Query<SettingsQuery>,
State(arc_state): State<Arc<ServiceSharedState>>,
Extension(config): Extension<config::EchoIpServiceConfig>, Extension(config): Extension<config::EchoIpServiceConfig>,
Extension(derived_config): Extension<DerivedConfiguration>, Extension(derived_config): Extension<DerivedConfiguration>,
cookie_header: Option<TypedHeader<headers::Cookie>>, cookie_header: Option<TypedHeader<headers::Cookie>>,
user_agent_header: Option<TypedHeader<headers::UserAgent>>, user_agent_header: Option<TypedHeader<headers::UserAgent>>,
mut req: Request<Body>, mut req: Request<Body>,
next: Next next: Next,
) -> Response { ) -> Response {
let state = Arc::clone(&arc_state);
let mut format = query.format; let mut format = query.format;
let mut dns_resolver_id = derived_config.default_resolver;
let mut dns_resolver_id = derived_config.default_resolver.clone();
let mut test_for_resolver = false;
if let Some(resolver_id) = query.dns { if let Some(resolver_id) = query.dns {
dns_resolver_id = resolver_id.into(); dns_resolver_id = resolver_id.into();
test_for_resolver = true;
} else if let Some(cookie_header) = cookie_header { } else if let Some(cookie_header) = cookie_header {
if let Some(resolver_id) = cookie_header.0.get("dns_resolver") { if let Some(resolver_id) = cookie_header.0.get("dns_resolver") {
dns_resolver_id = resolver_id.into(); dns_resolver_id = resolver_id.into();
test_for_resolver = true;
} }
} }
// Falls back to the default resolver if an invalid resolver id ws requested.
// This may be the case for bookmarked links or old cookies of a resolver was removed.
if test_for_resolver && !state.dns_resolvers.contains_key(&dns_resolver_id) {
dns_resolver_id = derived_config.default_resolver;
}
// Try to guess type from user agent // Try to guess type from user agent
if format.is_none() { if format.is_none() {
if let Some(TypedHeader(user_agent)) = user_agent_header { if let Some(TypedHeader(user_agent)) = user_agent_header {
@ -430,10 +488,8 @@ async fn handle_default_route(
&state, &state,
).await; ).await;
let user_agent: Option<String> = match user_agent_header { let user_agent: Option<String> = user_agent_header
Some(TypedHeader(user_agent)) => Some(user_agent.to_string()), .map(|TypedHeader(user_agent)| user_agent.to_string());
None => None,
};
state.templating_engine.render_view( state.templating_engine.render_view(
&settings, &settings,
@ -460,7 +516,7 @@ async fn handle_search_request(
//If someone asked for an asn, give an asn answer //If someone asked for an asn, give an asn answer
if let Some(asn_cap) = ASN_REGEX.captures(&search_query) { if let Some(asn_cap) = ASN_REGEX.captures(&search_query) {
if let Some(asn) = asn_cap.get(1).map_or(None, |m| m.as_str().parse::<u32>().ok()) { if let Some(asn) = asn_cap.get(1).and_then(|m| m.as_str().parse::<u32>().ok()) {
// Render a dummy template that can at least link to other pages // Render a dummy template that can at least link to other pages
let state = Arc::clone(&arc_state); let state = Arc::clone(&arc_state);
return state.templating_engine.render_view( return state.templating_engine.render_view(
@ -563,28 +619,49 @@ async fn handle_ip_request(
) )
} }
fn get_ip_mapping(address: &IpAddr) -> Option<IpMapping> {
if let IpAddr::V6(v6_address) = address {
if let Some(nat64_result) = resolve_nat64_address(*v6_address) {
return Some(IpMapping {
from_address: *address,
to_address: IpAddr::V4(nat64_result),
strategy: IpMappingStrategy::Nat64,
});
}
}
return None;
}
async fn get_ip_result( async fn get_ip_result(
address: &IpAddr, address: &IpAddr,
lang: &String, lang: &str,
dns_resolver_name: &Arc<str>, dns_resolver_name: &Arc<str>,
dns_disable_self_lookup: bool, dns_disable_self_lookup: bool,
client_ip: &IpAddr, client_ip: &IpAddr,
state: &ServiceSharedState, state: &ServiceSharedState,
) -> IpResult { ) -> IpResult {
let mapping = get_ip_mapping(address);
let original_address = address;
let address = &mapping.clone().map_or(*original_address, |m| m.to_address);
let mut reverse_dns_disabled_for_privacy = false; let mut reverse_dns_disabled_for_privacy = false;
if state.config.dns.allow_reverse_lookup { if state.config.dns.allow_reverse_lookup &&
if address == client_ip && dns_disable_self_lookup { (address == client_ip || original_address == client_ip) &&
dns_disable_self_lookup
{
reverse_dns_disabled_for_privacy = true; reverse_dns_disabled_for_privacy = true;
} }
}
let ip_info = AddressInfo::new(&address); let ip_info = AddressInfo::new(address);
if !(ip_info.scope == AddressScope::Global || ip_info.scope == AddressScope::Shared) || ip_info.cast != AddressCast::Unicast { // Return dummy result if:
if !((ip_info.scope == AddressScope::Private || ip_info.scope == AddressScope::LinkLocal) && state.config.server.allow_private_ip_lookup) { //
// The address falls into a private range and lookup of private addresses is not allowed.
if (!state.config.server.allow_private_ip_lookup) && (ip_info.scope == AddressScope::Private || ip_info.scope == AddressScope::LinkLocal) {
return IpResult { return IpResult {
mapping: mapping,
address: *address, address: *address,
hostname: None, hostname: None,
asn: None, asn: None,
@ -594,14 +671,13 @@ async fn get_ip_result(
reverse_dns_disabled_for_privacy: reverse_dns_disabled_for_privacy, reverse_dns_disabled_for_privacy: reverse_dns_disabled_for_privacy,
} }
} }
}
// do reverse lookup // do reverse lookup
let mut hostname: Option<String> = None; let mut hostname: Option<String> = None;
let mut used_dns_resolver: Option<Arc<str>> = None; let mut used_dns_resolver: Option<Arc<str>> = None;
if state.config.dns.allow_reverse_lookup && !reverse_dns_disabled_for_privacy { if state.config.dns.allow_reverse_lookup && !reverse_dns_disabled_for_privacy {
if let Some(dns_resolver) = &state.dns_resolvers.get(dns_resolver_name) { if let Some(dns_resolver) = &state.dns_resolvers.get(dns_resolver_name) {
hostname = simple_dns::reverse_lookup(&dns_resolver, &address).await; hostname = simple_dns::reverse_lookup(dns_resolver, address).await;
used_dns_resolver = Some(dns_resolver_name.clone()); used_dns_resolver = Some(dns_resolver_name.clone());
} }
} }
@ -612,18 +688,19 @@ async fn get_ip_result(
// location lookup // location lookup
let location_result = state.location_db.query_location_for_ip( let location_result = state.location_db.query_location_for_ip(
address, address,
&vec![lang, &"en".to_string()] &[lang, "en"]
); );
// filter reverse lookup // filter reverse lookup
if let Some(name) = &hostname { if let Some(name) = &hostname {
if match_domain_hidden_list(&name, &state.config.dns.hidden_suffixes) { if match_domain_hidden_list(name, &state.config.dns.hidden_suffixes) {
hostname = None; hostname = None;
used_dns_resolver = None; used_dns_resolver = None;
} }
} }
IpResult{ IpResult{
mapping: mapping,
address: *address, address: *address,
hostname: hostname, hostname: hostname,
asn: asn_result, asn: asn_result,
@ -666,21 +743,21 @@ async fn handle_dig_request(
} }
async fn get_dig_result( async fn get_dig_result(
dig_query: &String, dig_query: &str,
dns_resolver_name: &Arc<str>, dns_resolver_name: &Arc<str>,
state: &ServiceSharedState, state: &ServiceSharedState,
do_full_lookup: bool, do_full_lookup: bool,
) -> DigResult { ) -> DigResult {
let name = &dig_query.trim().trim_end_matches(".").to_string(); let name = &dig_query.trim().trim_end_matches(".").to_string();
let idna_name = IdnaName::from_string(&name); let idna_name = IdnaName::from_str(name);
if let Some(dns_resolver) = state.dns_resolvers.get(dns_resolver_name) { if let Some(dns_resolver) = state.dns_resolvers.get(dns_resolver_name) {
if let Ok(domain_name) = Name::from_str_relaxed(name.to_owned()+".") { if let Ok(domain_name) = Name::from_str_relaxed(name.to_owned()+".") {
if match_domain_hidden_list(&name, &state.config.dns.hidden_suffixes) { if match_domain_hidden_list(name, &state.config.dns.hidden_suffixes) {
// Try to hide the fact that we didn't do dns resolution at all // Try to hide the fact that we didn't do dns resolution at all
// We resolve example.org as basic avoidance of timing sidechannels. // We resolve example.org as basic avoidance of timing sidechannels.
// WARNING: this timing sidechannel avoidance is very crude. // WARNING: this timing sidechannel avoidance is very crude.
simple_dns::lookup( simple_dns::lookup(
&dns_resolver, dns_resolver,
&Name::from_ascii("example.org.").expect("Static Dummy Name"), &Name::from_ascii("example.org.").expect("Static Dummy Name"),
do_full_lookup).await; do_full_lookup).await;
return DigResult { return DigResult {
@ -692,7 +769,7 @@ async fn get_dig_result(
} else { } else {
return DigResult { return DigResult {
records: simple_dns::lookup( records: simple_dns::lookup(
&dns_resolver, dns_resolver,
&domain_name, &domain_name,
do_full_lookup).await, do_full_lookup).await,
idn: idna_name, idn: idna_name,

23
src/nat64.rs Normal file
View File

@ -0,0 +1,23 @@
use std::net::Ipv4Addr;
use std::net::Ipv6Addr;
/// Resolves a NAT64 address if it is in the range of 64:ff9b::/96
pub fn resolve_nat64_address(from: Ipv6Addr) -> Option<Ipv4Addr> {
if is_nat64_address(&from) {
let segments = from.segments();
return Some(Ipv4Addr::new(
((segments[6] & 0xff00) >> 8) as u8,
(segments[6] & 0x00ff) as u8,
((segments[7] & 0xff00) >> 8) as u8,
(segments[7] & 0x00ff) as u8,
));
} else {
return None;
}
}
pub fn is_nat64_address(address: &Ipv6Addr) -> bool {
let segments = address.segments();
segments[0]==0x64 && segments[1]==0xff9b
}

View File

@ -18,6 +18,7 @@ use governor::{
RateLimiter, RateLimiter,
state::keyed::DefaultKeyedStateStore, state::keyed::DefaultKeyedStateStore,
}; };
use log::debug;
use std::net::IpAddr; use std::net::IpAddr;
use std::num::NonZeroU32; use std::num::NonZeroU32;
@ -55,10 +56,10 @@ pub async fn rate_limit_middleware(
if limiter.check_key(&IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED)).is_ok() { if limiter.check_key(&IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED)).is_ok() {
let oldlen = limiter.len(); let oldlen = limiter.len();
if oldlen > 100 { if oldlen > 100 {
println!("Doing limiter cleanup ..."); debug!("Doing limiter cleanup ...");
limiter.retain_recent(); limiter.retain_recent();
limiter.shrink_to_fit(); limiter.shrink_to_fit();
println!("Old limiter store size: {oldlen} New limiter store size: {}", limiter.len()); debug!("Old limiter store size: {oldlen} New limiter store size: {}", limiter.len());
} }
} }
next.run(req).await next.run(req).await

View File

@ -10,13 +10,15 @@ use hickory_proto::rr::{
RData, RData,
record_type::RecordType, record_type::RecordType,
}; };
use hickory_proto::ProtoErrorKind;
use hickory_resolver::{ use hickory_resolver::{
error::ResolveError,
error::ResolveErrorKind,
lookup::Lookup, lookup::Lookup,
Name, Name,
TokioAsyncResolver, ResolveError,
ResolveErrorKind,
TokioResolver,
}; };
use log::{warn,error};
use tokio::join; use tokio::join;
@ -41,6 +43,7 @@ pub struct DnsLookupResult {
pub dns_error: bool, pub dns_error: bool,
pub nxdomain: bool, pub nxdomain: bool,
pub timeout: bool, pub timeout: bool,
pub too_busy: bool,
pub invalid_name: bool, pub invalid_name: bool,
pub unkown_resolver: bool, pub unkown_resolver: bool,
} }
@ -73,13 +76,13 @@ pub struct SrvRecord {
/* Lookup Functions*/ /* Lookup Functions*/
pub async fn reverse_lookup( pub async fn reverse_lookup(
resolver: &TokioAsyncResolver, resolver: &TokioResolver,
address: &IpAddr, address: &IpAddr,
) -> Option<String> { ) -> Option<String> {
let revese_res = resolver.reverse_lookup(*address); let revese_res = resolver.reverse_lookup(*address);
match revese_res.await { match revese_res.await {
Ok(lookup) => { Ok(lookup) => {
for name in lookup { if let Some(name) = lookup.iter().next() {
return Some(name.to_string()) return Some(name.to_string())
} }
None None
@ -87,11 +90,19 @@ pub async fn reverse_lookup(
Err(e) => { Err(e) => {
let kind = e.kind(); let kind = e.kind();
match kind { match kind {
ResolveErrorKind::NoRecordsFound { .. } => { ResolveErrorKind::Proto(protocol_error) => {
match protocol_error.kind() {
ProtoErrorKind::NoRecordsFound { .. } => {
//Ignore, that just happens … //Ignore, that just happens …
// TODO: Add NSec when adding support for dnssec
} }
_ => { _ => {
println!("Reverse lookup on {address} failed: {kind}"); error!("Reverse lookup on {address} failed with protocol error: {protocol_error}");
}
}
}
_ => {
error!("Reverse lookup on {address} failed: {kind}");
} }
} }
None None
@ -153,7 +164,9 @@ pub fn add_record_to_lookup_result(result: &mut DnsLookupResult, record: &RData)
); );
} }
}, },
_ => { println!("Tried to add an unkown DNS record to results: {record}"); }, _ => {
warn!("Tried to add an unkown DNS record to results: {record}");
},
} }
} }
@ -176,9 +189,7 @@ pub fn integrate_lookup_result(dig_result: &mut DnsLookupResult, lookup_result:
let name = lookup.query().name(); let name = lookup.query().name();
for record in lookup.record_iter() { for record in lookup.record_iter() {
if name == record.name() { if name == record.name() {
if let Some(data) = record.data() { add_record_to_lookup_result(dig_result, record.data());
add_record_to_lookup_result(dig_result, data);
}
} }
//TODO: handle additional responses //TODO: handle additional responses
} }
@ -186,27 +197,35 @@ pub fn integrate_lookup_result(dig_result: &mut DnsLookupResult, lookup_result:
Err(e) => { Err(e) => {
match e.kind() { match e.kind() {
ResolveErrorKind::Message(..) | ResolveErrorKind::Message(..) |
ResolveErrorKind::Msg(..) | ResolveErrorKind::Msg(..) => {
ResolveErrorKind::NoConnections | error!("There was an error message while doing a DNS Lookup: {e}");
ResolveErrorKind::Io(..) | }
ResolveErrorKind::Proto(..) => { ResolveErrorKind::Proto(protocol_error) => {
dig_result.other_error = true; match protocol_error.kind() {
println!("There was an error while doing a DNS Lookup: {e}"); ProtoErrorKind::Busy => {
dig_result.too_busy = true;
warn!("A resource was too busy for doing a DNS Lookup.");
}, },
ResolveErrorKind::Timeout => { ProtoErrorKind::Timeout => {
dig_result.timeout = true; dig_result.timeout = true;
println!("There was a timeout while doing a DNS Lookup."); warn!("There was a timeout while doing a DNS Lookup.");
}, },
ResolveErrorKind::NoRecordsFound{response_code, ..} => { ProtoErrorKind::NoRecordsFound { response_code, .. } => {
match response_code { match response_code {
ResponseCode::NXDomain => dig_result.nxdomain = true, ResponseCode::NXDomain => dig_result.nxdomain = true,
ResponseCode::NoError => {}, ResponseCode::NoError => {},
_ => { _ => {
println!("The DNS Server returned an error while doing a DNS Lookup: {response_code}"); error!("The DNS Server returned an error while doing a DNS Lookup: {response_code}");
dig_result.dns_error = true; dig_result.dns_error = true;
}, },
} }
} }
_ => {
dig_result.other_error = true;
error!("There was an error while doing a DNS Lookup: {protocol_error}");
}
}
},
_ => { /*Ignore for now*/ }, _ => { /*Ignore for now*/ },
} }
} }
@ -217,7 +236,7 @@ pub fn integrate_lookup_result(dig_result: &mut DnsLookupResult, lookup_result:
// If do_full_lookup is false only the A and AAAA (CNAMEs planned for the future) // If do_full_lookup is false only the A and AAAA (CNAMEs planned for the future)
// records will be fetched. // records will be fetched.
pub async fn lookup( pub async fn lookup(
resolver: &TokioAsyncResolver, resolver: &TokioResolver,
name: &Name, name: &Name,
do_full_lookup: bool, do_full_lookup: bool,
) -> DnsLookupResult { ) -> DnsLookupResult {

View File

@ -12,6 +12,12 @@
{% block content %} {% block content %}
{% set r = data.result %} {% set r = data.result %}
{% if r.mapping %}
<section>
<h2>{{ r.mapping.strategy | title }} Mapping</h2>
<p>The address <code>{{ r.mapping.from_address }}</code> was automatically translated to <code>{{ r.mapping.to_address }}</code> using {{ r.mapping.strategy | title }}.</p>
</section>
{% endif %}
<section> <section>
<h2>Network Information</h2> <h2>Network Information</h2>
<dl> <dl>

View File

@ -8,6 +8,13 @@
{% set r = data.result -%} {% set r = data.result -%}
# {% block title %}Lookup {{ data.result.address }}{% endblock %} # {% block title %}Lookup {{ data.result.address }}{% endblock %}
{%- if r.mapping %}
## {{ r.mapping.strategy | title }} Mapping
The address {{ r.mapping.from_address }} was automatically translated to {{ r.mapping.to_address }} using {{ r.mapping.strategy | title }}.
{%- endif %}
## Network information ## Network information
* Type of Address: {{ helper::ip_info(ip_info=r.ip_info) }} * Type of Address: {{ helper::ip_info(ip_info=r.ip_info) }}

View File

@ -6,10 +6,13 @@
<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://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 queries per day, wants an account)</small></a></li> <li><a target="_blank" href="https://www.shodan.io/host/{{ address }}">… on shodan.io <small>(limited queries 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 query 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 query per day, wants an account)</small></a></li>
<li><a target="_blank" href="https://www.abuseipdb.com/check/{{ address }}">… an AbuseIPDB.com</a></li>
<li><a target="_blank" href="https://app.crowdsec.net/cti/{{ address }}">… on CrowdSec.net CTI <small>(10 query's per day, wants an account)</small></a></li>
{% if not address is matching(":") %} {% if not address is matching(":") %}
{# v4 only #} {# v4 only #}
<li><a target="_blank" href="https://www.virustotal.com/gui/ip-address/{{ address }}">… on virustotal.com</a></li> <li><a target="_blank" href="https://www.virustotal.com/gui/ip-address/{{ address }}">… on virustotal.com</a></li>
{% endif %} {% endif %}
<li><a target="_blank" href="https://check.spamhaus.org/results?query={{ address }}">… on spamhaus.org</a></li>
</ul> </ul>
{% endmacro ip_address_links %} {% endmacro ip_address_links %}
@ -23,6 +26,7 @@
<li><a target="_blank" href="https://internet.nl/site/{{ name | urlencode_strict }}">… on the Internet.nl Website test</a></li> <li><a target="_blank" href="https://internet.nl/site/{{ name | urlencode_strict }}">… on the Internet.nl Website test</a></li>
<li><a target="_blank" href="https://client.rdap.org/?type=domain&object={{ name | urlencode_strict }}">… 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://client.rdap.org/?type=domain&object={{ name | urlencode_strict }}">… 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://crt.sh/?Identity={{ name | urlencode_strict }}&match==">… on crt.sh <small>(Certificate Transparancy Monitor)</small></a></li> <li><a target="_blank" href="https://crt.sh/?Identity={{ name | urlencode_strict }}&match==">… on crt.sh <small>(Certificate Transparancy Monitor)</small></a></li>
<li><a target="_blank" href="https://check.spamhaus.org/results?query={{ name | urlencode_strict }}">… on spamhaus.org</a></li>
</ul> </ul>
{% endmacro domain_name_links %} {% endmacro domain_name_links %}
@ -32,6 +36,8 @@
<li><a target="_blank" href="https://bgp.he.net/AS{{asn}}">… on Hurricane Electric BGP Toolkit</a></li> <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://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 query's 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=autonomous_system.asn%3D{{asn}}">… on search.censys.io <small>(10 query's per day, wants an account)</small></a></li>
<li><a target="_blank" href="https://app.crowdsec.net/cti?q=as_num%3A{{ asn }}&page=1">… on CrowdSec.net <small>(30 queries per week, wants an account)</small></a></li>
<li><a target="_blank" href="https://check.spamhaus.org/results?query=AS{{ asn }}">… on spamhaus.org</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://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> <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> </ul>