3 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
10 changed files with 946 additions and 357 deletions

1091
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -15,8 +15,8 @@ axum = { version = "0.8", features = ["macros"] }
clap = { version = "4.5", features = ["derive"] }
env_logger = "0.11"
governor = "0.8"
hickory-proto = "0.24"
hickory-resolver = { version = "0.24", features = ["dns-over-rustls","dns-over-https","dns-over-quic","native-certs"] }
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"
log = "0.4"

View File

@ -1,5 +1,5 @@
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::NameServerConfig;
@ -43,6 +43,7 @@ pub struct DnsResolverConfig {
pub servers: Vec<SocketAddr>,
pub protocol: DnsProtocol,
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
pub bind_address: Option<SocketAddr>,
#[serde(default="default_true", alias="trust_nx_responses")]
@ -95,8 +96,8 @@ impl DnsResolverConfig {
socket_addr: *server,
protocol: self.protocol.clone().into(),
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,
tls_config: None,
bind_addr: self.bind_address,
});
}

View File

@ -28,6 +28,7 @@ pub enum AddressScope {
Loopback,
Reserved,
Documentation,
Nat64,
#[default]
Unknown,
}
@ -78,6 +79,9 @@ impl AddressInfo {
// Test for the documentation address 2001:db8::/32
} else if segments[0]==0x2001 && segments[1]==0x0db8 && segments[2]==0 && segments[3]==0 {
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
} else if addr.is_multicast() {
address_cast = AddressCast::Multicast;

View File

@ -22,9 +22,10 @@ use axum_extra::headers;
use axum_extra::TypedHeader;
use clap::Parser;
use env_logger::Env;
use hickory_resolver::Name;
use hickory_resolver::TokioAsyncResolver;
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 serde::{Deserialize,Serialize};
use tower_http::services::ServeDir;
@ -50,6 +51,7 @@ mod config;
mod geoip;
mod idna;
mod ipinfo;
mod nat64;
mod ratelimit;
mod settings;
mod simple_dns;
@ -82,8 +84,28 @@ pub struct SearchQuery {
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)]
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,
hostname: Option<String>,
asn: Option<AsnResult>,
@ -108,7 +130,7 @@ pub struct DigResult {
struct ServiceSharedState {
templating_engine: TemplatingEngine,
dns_resolvers: HashMap<Arc<str>,TokioAsyncResolver>,
dns_resolvers: HashMap<Arc<str>,TokioResolver>,
dns_resolver_aliases: HashMap<Arc<str>,Arc<str>>,
asn_db: geoip::MMDBCarrier,
location_db: geoip::MMDBCarrier,
@ -215,21 +237,24 @@ async fn main() {
info!("Initalizing dns resolvers ...");
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();
if config.dns.enable_system_resolver {
info!("Initalizing System resolver ...");
let res = TokioAsyncResolver::tokio_from_system_conf();
let resolver = match res {
Ok(resolver) => resolver,
match initalize_system_resolver() {
Ok(resolver) => {
info!("System resolver successfully Initalized.");
dns_resolver_map.insert(
config.dns.system_resolver_id.clone(),
resolver
);
},
Err(e) => {
info!("Error while setting up dns resolver: {e}");
error!("Problem setting up system resolver: {e}");
::std::process::exit(1);
}
};
dns_resolver_map.insert(config.dns.system_resolver_id.clone(), resolver);
dns_resolver_selectables.push(Selectable {
id: config.dns.system_resolver_id.clone(),
name: config.dns.system_resolver_name.clone(),
@ -239,10 +264,10 @@ async fn main() {
for (key, resolver_config) in &config.dns.resolver {
info!("Initalizing {} resolver ...", key);
let resolver = TokioAsyncResolver::tokio(
let resolver = TokioResolver::builder_with_config(
resolver_config.to_hickory_resolver_config(),
Default::default()
);
).build();
dns_resolver_map.insert(key.clone(), resolver);
dns_resolver_selectables.push(Selectable {
id: key.clone(),
@ -331,6 +356,17 @@ async fn main() {
.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(
Query(query): Query<SettingsQuery>,
@ -583,6 +619,19 @@ 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(
address: &IpAddr,
lang: &str,
@ -592,10 +641,14 @@ async fn get_ip_result(
state: &ServiceSharedState,
) -> 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;
if state.config.dns.allow_reverse_lookup &&
address == client_ip &&
(address == client_ip || original_address == client_ip) &&
dns_disable_self_lookup
{
reverse_dns_disabled_for_privacy = true;
@ -608,6 +661,7 @@ async fn get_ip_result(
// 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 {
mapping: mapping,
address: *address,
hostname: None,
asn: None,
@ -646,6 +700,7 @@ async fn get_ip_result(
}
IpResult{
mapping: mapping,
address: *address,
hostname: hostname,
asn: asn_result,

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

@ -10,12 +10,13 @@ use hickory_proto::rr::{
RData,
record_type::RecordType,
};
use hickory_proto::ProtoErrorKind;
use hickory_resolver::{
error::ResolveError,
error::ResolveErrorKind,
lookup::Lookup,
Name,
TokioAsyncResolver,
ResolveError,
ResolveErrorKind,
TokioResolver,
};
use log::{warn,error};
@ -42,6 +43,7 @@ pub struct DnsLookupResult {
pub dns_error: bool,
pub nxdomain: bool,
pub timeout: bool,
pub too_busy: bool,
pub invalid_name: bool,
pub unkown_resolver: bool,
}
@ -74,7 +76,7 @@ pub struct SrvRecord {
/* Lookup Functions*/
pub async fn reverse_lookup(
resolver: &TokioAsyncResolver,
resolver: &TokioResolver,
address: &IpAddr,
) -> Option<String> {
let revese_res = resolver.reverse_lookup(*address);
@ -88,8 +90,16 @@ pub async fn reverse_lookup(
Err(e) => {
let kind = e.kind();
match kind {
ResolveErrorKind::NoRecordsFound { .. } => {
//Ignore, that just happens …
ResolveErrorKind::Proto(protocol_error) => {
match protocol_error.kind() {
ProtoErrorKind::NoRecordsFound { .. } => {
//Ignore, that just happens …
// TODO: Add NSec when adding support for dnssec
}
_ => {
error!("Reverse lookup on {address} failed with protocol error: {protocol_error}");
}
}
}
_ => {
error!("Reverse lookup on {address} failed: {kind}");
@ -179,9 +189,7 @@ pub fn integrate_lookup_result(dig_result: &mut DnsLookupResult, lookup_result:
let name = lookup.query().name();
for record in lookup.record_iter() {
if name == record.name() {
if let Some(data) = record.data() {
add_record_to_lookup_result(dig_result, data);
}
add_record_to_lookup_result(dig_result, record.data());
}
//TODO: handle additional responses
}
@ -189,27 +197,35 @@ pub fn integrate_lookup_result(dig_result: &mut DnsLookupResult, lookup_result:
Err(e) => {
match e.kind() {
ResolveErrorKind::Message(..) |
ResolveErrorKind::Msg(..) |
ResolveErrorKind::NoConnections |
ResolveErrorKind::Io(..) |
ResolveErrorKind::Proto(..) => {
dig_result.other_error = true;
error!("There was an error while doing a DNS Lookup: {e}");
},
ResolveErrorKind::Timeout => {
dig_result.timeout = true;
warn!("There was a timeout while doing a DNS Lookup.");
},
ResolveErrorKind::NoRecordsFound{response_code, ..} => {
match response_code {
ResponseCode::NXDomain => dig_result.nxdomain = true,
ResponseCode::NoError => {},
_ => {
error!("The DNS Server returned an error while doing a DNS Lookup: {response_code}");
dig_result.dns_error = true;
},
}
ResolveErrorKind::Msg(..) => {
error!("There was an error message while doing a DNS Lookup: {e}");
}
ResolveErrorKind::Proto(protocol_error) => {
match protocol_error.kind() {
ProtoErrorKind::Busy => {
dig_result.too_busy = true;
warn!("A resource was too busy for doing a DNS Lookup.");
},
ProtoErrorKind::Timeout => {
dig_result.timeout = true;
warn!("There was a timeout while doing a DNS Lookup.");
},
ProtoErrorKind::NoRecordsFound { response_code, .. } => {
match response_code {
ResponseCode::NXDomain => dig_result.nxdomain = true,
ResponseCode::NoError => {},
_ => {
error!("The DNS Server returned an error while doing a DNS Lookup: {response_code}");
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*/ },
}
}
@ -220,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)
// records will be fetched.
pub async fn lookup(
resolver: &TokioAsyncResolver,
resolver: &TokioResolver,
name: &Name,
do_full_lookup: bool,
) -> DnsLookupResult {

View File

@ -12,6 +12,12 @@
{% block content %}
{% 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>
<h2>Network Information</h2>
<dl>

View File

@ -8,6 +8,13 @@
{% set r = data.result -%}
# {% 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
* 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://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://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(":") %}
{# v4 only #}
<li><a target="_blank" href="https://www.virustotal.com/gui/ip-address/{{ address }}">… on virustotal.com</a></li>
{% endif %}
<li><a target="_blank" href="https://check.spamhaus.org/results?query={{ address }}">… on spamhaus.org</a></li>
</ul>
{% 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://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://check.spamhaus.org/results?query={{ name | urlencode_strict }}">… on spamhaus.org</a></li>
</ul>
{% 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://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://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://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>