Skip to content

Commit

Permalink
initial commit for DNS local resolver
Browse files Browse the repository at this point in the history
  • Loading branch information
jaytaph committed Nov 18, 2023
1 parent aa3571b commit 6281c9c
Show file tree
Hide file tree
Showing 9 changed files with 1,265 additions and 17 deletions.
582 changes: 568 additions & 14 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ clap = { version = "4.4.7", features = ["derive"] }
cli-table = "0.4.7"
textwrap = "0.16.0"
log = "0.4.20"
domain-lookup-tree = "0.1"
hickory-resolver = "0.24.0"
simple_logger = "4.2.0"

[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }
Expand Down
40 changes: 38 additions & 2 deletions src/config/settings.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,52 @@
{
"dns": [
{
"key": "local_resolver.enabled",
"key": "cache.max_entries",
"type": "u",
"default": "u:1000",
"description": "This setting defines the maximum number of entries that may be stored in the DNS cache before the oldest entry is evicted."
},
{
"key": "local.overrides.enabled",
"type": "b",
"default": "b:true",
"description": "This setting enables the local DNS override table. When enabled, Gosub will return any IP address that is defined in the local DNS override table."
},
{
"key": "local.overrides",
"type": "m",
"default": "m:''",
"description": "This setting defines the local DNS override table."
},
{
"key": "resolve.ttl.override.enabled",
"type": "b",
"default": "b:false",
"description": "This setting enables the local DNS resolver. When enabled, GosuB will use the local DNS resolver to resolve DNS queries. When disabled, GosuB will use the DNS resolver configured in the operating system."
"description": "When enabled, the TTL of each entry will be overridden with the value defined in resolve.ttl.override."
},
{
"key": "resolve.ttl.override.seconds",
"type": "u",
"default": "u:0",
"description": "Number of seconds to override the TTL with. When set to 0, the TTL will expire directly"
},
{
"key": "doh.enabled",
"type": "b",
"default": "b:false",
"description": "This setting enabled DNS over HTTPS. A secure way of communicating with DNS servers."
},
{
"key": "dot.enabled",
"type": "b",
"default": "b:false",
"description": "This setting enabled DNS over TLS. A secure way of communicating with DNS servers."
},
{
"key": "resolver.ips",
"type": "m",
"default": "m:''",
"description": "Any resolvers defined here will be used for DNS lookups. If no resolvers are defined, the system resolvers will be used."
}
],
"useragent": [
Expand Down
202 changes: 202 additions & 0 deletions src/dns.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
mod cache;
mod local;
mod remote;

use crate::types;
use crate::types::Result;
use derive_more::Display;
use log::{debug, info};
use std::net::IpAddr;
use std::time::{SystemTime, UNIX_EPOCH};

/// A DNS entry is a mapping of a domain to zero or more IP address mapping
#[derive(Default, Clone, Debug, PartialEq)]
struct DnsEntry {
// domain name
domain: String,
// // Ip type that is stored in this entry (could be Ipv4, IPv6 or Both)
// ip_type: ResolveType,
// List of addresses for this domain
ips: Vec<IpAddr>,

/// True when the ips list has ipv4 addresses
has_ipv4: bool,
/// True when the ips list has ipv6 addresses
has_ipv6: bool,

// Internal iterator pointer
iter: usize,
/// expiry time after epoch
expires: u64,
}

impl DnsEntry {
/// Instantiate a new domain name entry
pub(crate) fn new(domain: &str) -> DnsEntry {
DnsEntry {
domain: domain.to_string(),
..Default::default()
}
}

/// Returns true if the dns entry has expired
pub fn expired(&self) -> bool {
self.expires
< SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs()
}

fn ipv4(&self) -> Vec<IpAddr> {
self.ips.iter().filter(|x| x.is_ipv4()).copied().collect()
}

fn ipv6(&self) -> Vec<IpAddr> {
self.ips.iter().filter(|x| x.is_ipv6()).copied().collect()
}
}

impl Iterator for DnsEntry {
type Item = IpAddr;

/// With next() you can simply iterate over all the ips in the list
fn next(&mut self) -> Option<Self::Item> {
if self.iter >= self.ips.len() {
// reset iterator at the end
self.iter = 0;

return None;
}

let ip = self.ips[self.iter];
self.iter += 1;

Some(ip)
}
}

/// Type of DNS resolution
#[derive(Display, Debug, PartialEq, Clone)]
pub enum ResolveType {
/// Only resolve IPV4 addresses (A)
Ipv4,
/// Only resolve IPV6 addresses (AAAA)
Ipv6,
/// Resolve both IPV4 and IPV6 addresses
Both,
}

trait DnsResolver {
/// Resolves a domain name for a given resolver_type
fn resolve(&mut self, domain: &str, resolve_type: ResolveType) -> Result<DnsEntry>;
/// Announces the resolved dns entry for the domain to a resolver
fn announce(&mut self, domain: &str, entry: &DnsEntry);

// name for debugging purposes
fn name(&self) -> &'static str;
}

// impl fmt::Debug for dyn DnsResolver {
// fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// write!(f, "dns resolver")
// }
// }

trait DnsCache {
/// Flush all domains
fn flush_all(&mut self);
/// Flush a single domain
fn flush_entry(&mut self, domain: &str);
}

struct Dns {
resolvers: Vec<Box<dyn DnsResolver>>,
}

impl Dns {
pub fn new() -> Self {
let mut resolvers: Vec<Box<dyn DnsResolver>> = vec![];
resolvers.push(Box::new(cache::CacheResolver::new(1000)));
resolvers.push(Box::new(local::LocalTableResolver::new()));

let opts = remote::RemoteResolverOptions::default();
resolvers.push(Box::new(remote::RemoteResolver::new(opts)));

Dns { resolvers }
}

/// Resolves a domain name to a set of IP addresses based on the resolve_type.
/// It can resolve either Ipv4, ipv6 or both addresses.
///
/// Each request will be resolved by the resolvers in the order they are added.
/// The first resolver is usually the cache resolver, which caches any entries (according to their TTL)
/// The second resolver is usually the local table resolver, which resolves any local overrides
/// The third resolver is usually the remote resolver, which resolves any remote entries by querying external DNS server(s)
///
pub fn resolve(&mut self, domain: &str, resolve_type: ResolveType) -> Result<DnsEntry> {
let mut entry = None;

info!("Resolving {} for {:?}", domain, resolve_type);

for resolver in self.resolvers.iter_mut() {
debug!("Trying resolver: {}", resolver.name());

if let Ok(e) = resolver.resolve(domain, resolve_type.clone()) {
debug!("Found entry {:?}", e);
entry = Some(e);
break;
}
}

if entry.is_none() {
return Err(types::Error::DnsDomainNotFound);
}

// Iterate all resolvers and add to all cache systems (normally, this is only the first resolver)
for resolver in self.resolvers.iter_mut() {
resolver.announce(domain, &entry.clone().unwrap().clone());
}

Ok(entry.unwrap().clone())
}
}

#[cfg(test)]
mod test {
use super::*;
use simple_logger::SimpleLogger;
use std::time::Instant;

#[test]
fn resolver() {
SimpleLogger::new().init().unwrap();

let mut dns = Dns::new();

let now = Instant::now();
let e = dns.resolve("example.org", ResolveType::Ipv4).unwrap();
let elapsed_time = now.elapsed();
e.ipv4().iter().for_each(|x| println!("ipv4: {}", x));
println!("Took {} microseconds.", elapsed_time.as_micros());

let now = Instant::now();
let e = dns.resolve("example.org", ResolveType::Ipv6).unwrap();
let elapsed_time = now.elapsed();
e.ipv6().iter().for_each(|x| println!("ipv6: {}", x));
println!("Took {} microseconds.", elapsed_time.as_micros());

let now = Instant::now();
let e = dns.resolve("example.org", ResolveType::Ipv4).unwrap();
let elapsed_time = now.elapsed();
e.ipv4().iter().for_each(|x| println!("ipv4: {}", x));
println!("Took {} microseconds.", elapsed_time.as_micros());

let now = Instant::now();
let e = dns.resolve("example.org", ResolveType::Both).unwrap();
let elapsed_time = now.elapsed();
e.ipv4().iter().for_each(|x| println!("ipv4: {}", x));
e.ipv6().iter().for_each(|x| println!("ipv6: {}", x));
println!("Took {} microseconds.", elapsed_time.as_micros());
}
}
Loading

0 comments on commit 6281c9c

Please sign in to comment.