diff --git a/services/dns/src/api.rs b/services/dns/src/api.rs index fb9734805..67894e73c 100644 --- a/services/dns/src/api.rs +++ b/services/dns/src/api.rs @@ -12,7 +12,10 @@ pub(crate) enum Opcode { Lookup, Flush, - /// issuing this opcode causes all future attempts to change the DNS server configs to be ignored + /// used internally to update the TTL field, and eventually expire the cache (unless cache is frozen) + UpdateTtl, + + /// issuing this opcode causes all future attempts to change the DNS server configs to be ignored. This also freezes the cache. FreezeConfig, /// this allows automatic updates to the DNS server configs based on DHCP. This is the default state. ThawConfig, diff --git a/services/dns/src/main.rs b/services/dns/src/main.rs index 68b69c703..330407930 100644 --- a/services/dns/src/main.rs +++ b/services/dns/src/main.rs @@ -6,12 +6,14 @@ use api::*; use net::{NetIpAddr, Duration}; use num_traits::*; +use xous::msg_scalar_unpack; -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; use std::collections::HashMap; use std::io::ErrorKind; use xous_ipc::{String, Buffer}; use std::convert::TryInto; +use std::thread; // KISS DNS @@ -97,6 +99,180 @@ impl Message { } } + fn fast_foward_name(&self, start: usize) -> Result { + use DnsResponseCode::FormatError; + let mut index = start; + loop { + log::info!("cname index: {}", index); + if *(self.datagram.get(index).ok_or(FormatError)?) == 0 { + index += 1; + break; + } else { + index += *(self.datagram.get(index).ok_or(FormatError)?) as usize; + index += 1; + } + } + Ok(index) + } + + pub fn parse_response(&self) -> Result, DnsResponseCode> { + use DnsResponseCode::FormatError; + log::info!("parsing packet: {:?}", self.datagram); + + let mut map = HashMap::::new(); + // ASSUME: the query ID and response bit fields have already been checked + // and that the rcode is valid + let qdcount = u16::from_be_bytes(self.datagram[4..6].try_into().unwrap()); + let ancount = u16::from_be_bytes(self.datagram[6..8].try_into().unwrap()); + + let mut index = 12; + // fast forward past the qname + for queries in 0..qdcount { + log::info!("parsing query{}, index {}", queries, index); + index = self.fast_foward_name(index)?; + log::info!("fast forward through qname to {}", index); + // index is now at qtype + let qtype = u16::from_be_bytes(self.datagram[index..index+2].try_into().unwrap()); + if qtype != 1 && qtype != 28 { // A = 1, AAAA = 28 + log::error!("Problem parsing qname, qtype is not 1 or 28: {}", qtype); + return Err(FormatError) + } + index += 2; + let qclass = u16::from_be_bytes(self.datagram[index..index+2].try_into().unwrap()); + if qclass != 1 { + log::error!("Problem parsing qname, qclass is not 1: {}", qclass); + return Err(FormatError) + } + index += 2; + } + // index is now at the aname section + for aname in 0..ancount { + log::info!("parsing aname{}, index {}", aname, index); + // first check to see if we're dealing with a pointer or a name + if self.datagram[index] >= 0xc0 { + // pointer + index += 1; + if self.datagram[index] != 0xc { + log::error!("Found aname pointer, but value does not conform to length of aname header"); + return Err(FormatError) + } + index += 1; + } else { + // name, fast forward past the name + index = self.fast_foward_name(index)?; + log::info!("fast forward aname to {}", index); + } + // index is now at type + let atype = u16::from_be_bytes(self.datagram[index..index+2].try_into().unwrap()); + if atype != 1 && atype != 28 { // A = 1, AAAA = 28 + log::error!("Problem parsing aname, type is not 1 or 28: {}", atype); + return Err(FormatError) + } + index += 2; + let aclass = u16::from_be_bytes(self.datagram[index..index+2].try_into().unwrap()); + if aclass != 1 { + log::error!("Problem parsing aname, aclass is not 1: {}", aclass); + return Err(FormatError) + } + index += 2; + // this is our TTL + let ttl = u32::from_be_bytes(self.datagram[index..index+4].try_into().unwrap()); + index += 4; + // this is the payload length + let addr_len = u16::from_be_bytes(self.datagram[index..index+2].try_into().unwrap()); + index += 2; + match addr_len { + 4 => { // ipv4 + if atype != 1 { + log::error!("Got a 4-byte address, but ATYPE != A (1)"); + return Err(FormatError) + } + // this copy happens because I can't figure out how to get Ipv4Addr::from() to realize it's casting from a [u8;4] + let mut rdata: [u8; 4] = [0; 4]; + for (&src, dst) in self.datagram[index..index+4].iter().zip(rdata.iter_mut()) { + *dst = src; + } + let addr = IpAddr::V4(Ipv4Addr::from(rdata)); + index += 4; + map.insert(addr, ttl); + } + 16 => { // ipv6 + if atype != 28 { + log::error!("Got a 16-byte address, but ATYPE != AAAA (28)"); + return Err(FormatError) + } + // this copy happens because I can't figure out how to get Ipv6Addr::from() to realize it's casting from a [u8;4] + let mut rdata: [u8; 16] = [0; 16]; + for (&src, dst) in self.datagram[index..index+16].iter().zip(rdata.iter_mut()) { + *dst = src; + } + let addr = IpAddr::V6(Ipv6Addr::from(rdata)); + index += 16; + map.insert(addr, ttl); + } + _ => { + log::error!("Length field does not match a known record type"); + return Err(FormatError) + } + } + } + + Ok(map) + } + + /* + example response for: betrusted.io->185.199.111.153 +Header: + 61, ca, id + 81, 80, header + 0, 1, qdcount + 0, 4, ancount + 0, 0, nscount + 0, 0, arcount +qname: + 9, length 9 + 62, 65, 74, 72, 75, 73, 74, 65, 64, "betrusted" + 2, length 2 + 69, 6f, "io" + 0, end of name +qtype: + 0, 1, type A +qclass: + 0, 1, type IN +aname0: + c0, name is a pointer (any value > 192 is a pointer) + c, offset of 12 from start of aname0 + 0, 1, type A + 0, 1, class IN + 0, 0, e, 10, 0xe10 = 3600 seconds TTL + 0, 4, 4 bytes address + b9, c7, 6c, 99, address +aname1: + c0, name is a pointer + c, + 0, 1, type A + 0, 1, class IN + 0, 0, e, 10, TTL + 0, 4, 4 byte address + b9, c7, 6d, 99, address +aname2: + c0, + c, + 0, 1, + 0, 1, + 0, 0, e, 10, + 0, 4, + b9, c7, 6e, 99, +aname3: + c0, + c, + 0, 1, + 0, 1, + 0, 0, e, 10, + 0, 4, + b9, c7, 6f, 99 + */ + /* pub fn is_query(&self) -> bool { !self.is_response() @@ -122,6 +298,7 @@ pub struct Resolver { socket: net::UdpSocket, buf: [u8; DNS_PKT_MAX_LEN], trng: trng::Trng, + freeze: bool, } impl Resolver { pub fn new(xns: &xous_names::XousNames) -> Resolver { @@ -142,6 +319,7 @@ impl Resolver { socket, buf: [0; DNS_PKT_MAX_LEN], trng, + freeze: false, } } pub fn add_server(&mut self, addr: IpAddr) { @@ -154,9 +332,15 @@ impl Resolver { self.mgr.clear(); } pub fn set_freeze_config(&mut self, freeze: bool) { + self.freeze = freeze; self.mgr.set_freeze(freeze); } - pub fn resolve(&mut self, name: &str) -> Result { + pub fn get_freeze(&self) -> bool { self.freeze } + /// this allows us to re-use the TRNG object + pub fn trng_u32(&self) -> u32 { + self.trng.get_u32().unwrap() + } + pub fn resolve(&mut self, name: &str) -> Result, DnsResponseCode> { if let Some(dns_address) = self.mgr.get_random() { let dns_port = 53; let server = SocketAddr::new(dns_address, dns_port); @@ -171,22 +355,23 @@ impl Resolver { match self.socket.recv(&mut self.buf) { Ok(len) => { - log::info!("buf {}: {:x?}", len, &self.buf[..len]); let message = Message::from(&self.buf[..len]); if message.id() == query.id() && message.is_response() { return match message.rcode() { DnsResponseCode::NoError => { + /* + let mut map = HashMap::::new(); // TODO: Parse the datagram instead of // extracting the last 4 bytes. //let rdata = message.answer().rdata(); let n = message.datagram.len(); let mut rdata: [u8; 4] = [0; 4]; - log::info!("datagram{}: {:x?}", n, message.datagram); + log::trace!("datagram{}: {:x?}", n, message.datagram); for (&src, dst) in message.datagram[(n - 4)..].iter().zip(rdata.iter_mut()) { *dst = src; } - log::info!("rdata: {:?}", rdata); - Ok(IpAddr::V4(Ipv4Addr::from(rdata))) + map.insert(IpAddr::V4(Ipv4Addr::from(rdata)), 3600);*/ + message.parse_response() } rcode => { Err(rcode) @@ -229,9 +414,23 @@ fn xmain() -> ! { // if you wanted to force a server into the initial config, you can do it here, for example: // resolver.add_server(IpAddr::V4(Ipv4Addr::new(1,1,1,1))); - // perhaps eventually this should be expanded to include a TTL and periodic sweep is done to clear the cache. - // but for now, let's go for something simple. - let mut dns_cache = HashMap::::new(); + let mut dns_cache = HashMap::>::new(); + + // build a thread that pings the UpdateTtl function once every few minutes to expire the DNS cache + thread::spawn({ + let local_cid = xous::connect(dns_sid).unwrap(); + move || { + const TTL_INTERVAL_SECS: usize = 300; // every 5 minutes update the map + let tt = ticktimer_server::Ticktimer::new().unwrap(); + loop { + tt.sleep_ms(TTL_INTERVAL_SECS * 1000).unwrap(); + xous::send_message(local_cid, + xous::Message::new_scalar(Opcode::UpdateTtl.to_usize().unwrap(), + TTL_INTERVAL_SECS, 0, 0, 0) + ).expect("couldn't increment DNS cache"); + } + } + }); log::trace!("ready to accept requests"); loop { @@ -241,23 +440,50 @@ fn xmain() -> ! { let mut buf = unsafe{Buffer::from_memory_message_mut(msg.body.memory_message_mut().unwrap())}; let name = buf.to_original::, _>().unwrap(); let name_std = std::string::String::from(name.as_str().unwrap()); - if let Some(ip_addr) = dns_cache.get(&name_std) { - log::info!("DNS cached: {}->{:?}", name, ip_addr); - let response = DnsResponse { - addr: Some(NetIpAddr::from(*ip_addr)), - code: DnsResponseCode::NoError, - }; - buf.replace(response).unwrap(); - } else { - match resolver.resolve(name.as_str().unwrap()) { - Ok(ip_addr) => { - log::info!("DNS queried: {}->{:?}", name, ip_addr); - dns_cache.insert(name_std, ip_addr); + if let Some(cache_entry) = dns_cache.get(&name_std) { + // pick a random entry + let rand = resolver.trng_u32() as usize % cache_entry.len(); + for (index, (ip_addr, _)) in cache_entry.iter().enumerate() { + if rand == index { + log::info!("DNS cached: {}->{:?}", name, ip_addr); let response = DnsResponse { - addr: Some(NetIpAddr::from(ip_addr)), + addr: Some(NetIpAddr::from(*ip_addr)), code: DnsResponseCode::NoError, }; buf.replace(response).unwrap(); + break; + } + } + } else { + match resolver.resolve(name.as_str().unwrap()) { + Ok(cache_entry) => { + if cache_entry.len() > 0 { + dns_cache.insert(name_std, cache_entry); + + // now pick the entry back out again, as it was consumed... + let name_std = std::string::String::from(name.as_str().unwrap()); + let cache_entry = dns_cache.get(&name_std).unwrap(); + + // pick a random entry from the query response + let rand = resolver.trng_u32() as usize % cache_entry.len(); + for (index, (ip_addr, _)) in cache_entry.iter().enumerate() { + if rand == index { + let response = DnsResponse { + addr: Some(NetIpAddr::from(*ip_addr)), + code: DnsResponseCode::NoError, + }; + buf.replace(response).unwrap(); + break; + } + } + } else { + // no names found + let response = DnsResponse { + addr: None, + code: DnsResponseCode::NameError, + }; + buf.replace(response).unwrap(); + } }, Err(e) => { log::info!("DNS query failed: {}->{:?}", name, e); @@ -270,6 +496,44 @@ fn xmain() -> ! { } } }, + Some(Opcode::UpdateTtl) => msg_scalar_unpack!(msg, incr_secs, _, _, _, { + let increment = if incr_secs < u32::MAX as usize { + incr_secs as u32 + } else { + u32::MAX + }; + if !resolver.get_freeze() { + let mut expired_names = Vec::::new(); + for (name, cache_map) in dns_cache.iter_mut() { + // each entry can have multiple names with a different TTL + // decrement the TTL, and note which go to zero + let mut expired_entries = Vec::::new(); + for (entry, ttl) in cache_map.iter_mut() { + if *ttl < increment { + *ttl = 0; + expired_entries.push(*entry); + } else { + *ttl = *ttl - increment as u32; + } + } + // remove the entries that are 0 + for entry in expired_entries { + log::info!("DNS cache expiring {:?}", entry); + cache_map.remove(&entry); + } + // if all the entries are removed, mark for removal from the cache entirely + if cache_map.len() == 0 { + // have to copy the name to a new object to track it + let name = std::string::String::from(name.as_str()); + expired_names.push(name); + } + } + for name in expired_names { + log::info!("DNS cache removing {}", &name); + dns_cache.remove(&name); + } + } + }), Some(Opcode::Flush) => { dns_cache.clear(); },