Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

initial commit for DNS local resolver #262

Merged
merged 2 commits into from
Nov 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
6 changes: 3 additions & 3 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,11 +177,11 @@ mod test {
let mut store =
ConfigStore::from_storage(Box::new(MemoryStorageAdapter::new()), true).unwrap();
let setting = store.get("dns.local_resolver.enabled");
assert_eq!(setting, Setting::Bool(false));
assert_eq!(setting, Setting::Bool(true));

store.set("dns.local_resolver.enabled", Setting::Bool(true));
store.set("dns.local_resolver.enabled", Setting::Bool(false));
let setting = store.get("dns.local_resolver.enabled");
assert_eq!(setting, Setting::Bool(true));
assert_eq!(setting, Setting::Bool(false));
}

#[test]
Expand Down
38 changes: 37 additions & 1 deletion src/config/settings.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,52 @@
{
"dns": [
{
"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_resolver.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_resolver.table",
"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
195 changes: 195 additions & 0 deletions src/dns.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
mod cache;
mod local;
mod remote;

use crate::types;
use crate::types::Result;
use core::str::FromStr;
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,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

an entry has either resolved ipv4 addresses, resolver ipv6 addresses or both. WHen we have a record in cache, but we want to have ipv6 addresses for that record, we can see if these are already resolved for this record.

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

// Internal iterator pointer
iter: usize,
/// expiry time after epoch
expires: u64,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't use the expires time yet. This will be taken from a TTL that is returned by the remote resolver.

}

impl DnsEntry {
/// Instantiate a new domain name entry with set of ips
pub(crate) fn new(domain: &str, ips: Vec<&str>) -> DnsEntry {
let mut entry = DnsEntry {
domain: domain.to_string(),
..Default::default()
};

for ip in ips {
if let Ok(ip) = IpAddr::from_str(ip) {
if ip.is_ipv4() {
entry.has_ipv4 = true;
}
if ip.is_ipv6() {
entry.has_ipv6 = true;
}
entry.ips.push(ip);
}
}

entry
}

/// 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()
}

fn iter(&self) -> impl Iterator<Item = &IpAddr> {
self.ips.iter()
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After I thought about it more, I realized you might have been wanting to use the iterator trait as a state machine for implementing round-robin. If so, I think something other than trait Iterator would be used for this, e.g., a custom method that updates the state to the next ip address, possibly in a new struct that this struct delegates to.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i've actually removed the round robin inside the dns entries because it ended up a bit too complex. I leave it up to the caller to deal with round robin. There might be other ways they want to use the set of resolved ips: when one fails, use another, etc..

}

/// Type of DNS resolution
#[derive(Display, Debug, PartialEq, Clone)]
pub enum ResolveType {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know exactly who will be in charge of this, but somewhere we must decide if we resolve the ipv4 or ipv6 of a domain. This is the ResolveType we can add to the resolve() function.

/// 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;
}

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![];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let mut resolvers: Vec<Box<dyn DnsResolver>> = vec![];
let mut resolvers: Vec<Box<dyn DnsResolver>> = Vec::with_capacity(3);

Allocate the memory for the vec directly at the creation (Yes, I know, you'd like to make such things later on, but I think we forget such things if we don't do it now)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would not worry about this too much. It's not used in any thight loops, and the first push will do an allocation for 4 elements anyway.
So instead of with_capacity(3), you get a with_capacity(4), which is ok enough.

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());
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice use of the log crate.


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