diff --git a/.gitignore b/.gitignore index 4c5f124..1d20d8f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target .env +nanodns.toml test.sh \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index 95e8d4d..5569c94 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,16 +1,69 @@ -use anyhow::Result; +use anyhow::{Result, ensure}; use serde::Deserialize; #[derive(Deserialize)] -pub struct ConfigData { +pub struct Entry { + pub name: String, + pub address: String, + #[serde(skip)] + pub address_bytes: [u8; 4], +} + +#[derive(Deserialize)] +struct EnvConfigData { pub port: u16, } +#[derive(Deserialize)] +struct FileConfigData { + #[serde(default, rename = "entry")] + pub entries: Vec, +} + +pub struct ConfigData { + pub port: u16, + pub entries: Vec, +} + +impl ConfigData { + pub fn get_address_by_domain(&self, domain: &str) -> Option<[u8; 4]> { + self.entries + .iter() + .find(|e| e.name.as_str() == domain) + .map(|e| e.address_bytes) + } +} + pub fn load() -> Result { - let source = config::Environment::default(); - let builder = config::Config::builder() - .add_source(source) - .set_default("port", 53)?; - let config = builder.build()?.try_deserialize()?; - Ok(config) + let env_config = { + let source = config::Environment::default(); + let builder = config::Config::builder() + .add_source(source) + .set_default("port", 53)?; + let config: EnvConfigData = builder.build()?.try_deserialize()?; + config + }; + + let mut file_config = { + let source = config::File::with_name("nanodns").required(false); + let builder = config::Config::builder().add_source(source); + let config: FileConfigData = builder.build()?.try_deserialize()?; + config + }; + + for entry in &mut file_config.entries { + let address_parts: Vec<&str> = entry.address.split(".").collect(); + ensure!(address_parts.len() == 4, "Invalid IP address"); + entry.address_bytes = [ + address_parts[0].parse()?, + address_parts[1].parse()?, + address_parts[2].parse()?, + address_parts[3].parse()?, + ]; + } + + Ok(ConfigData { + port: env_config.port, + entries: file_config.entries, + }) } diff --git a/src/dns.rs b/src/dns.rs deleted file mode 100644 index 2d78827..0000000 --- a/src/dns.rs +++ /dev/null @@ -1,179 +0,0 @@ -#![allow(unused)] -use std::fmt::Display; - -use anyhow::{Result, bail}; - -const QUESTION_TYPE_A: u16 = 0x01; -const QUESTION_TYPE_NS: u16 = 0x02; -const QUESTION_TYPE_CNAME: u16 = 0x05; -const QUESTION_TYPE_AAAA: u16 = 0x1C; -const QUESTION_TYPE_MX: u16 = 0x0F; - -const QUESTION_CLASS_IN: u16 = 0x01; -const QUESTION_CLASS_CHAOS: u16 = 0x03; -const QUESTION_CLASS_HS: u16 = 0x04; -const QUESTION_CLASS_ANY: u16 = 0xFF; - -pub struct DnsHeader { - pub transaction_id: [u8; 2], - pub query_response: bool, - pub op_code: u8, // 4 bits - pub authoritative_answer: bool, - pub truncation: bool, - pub recursion_desired: bool, - pub recursion_available: bool, - pub authentic_data: bool, - pub checking_disabled: bool, - pub response_code: u8, // 4 bits - pub number_of_questions: u16, - pub number_of_answers: u16, - pub number_of_authority_resource_records: u16, - pub number_of_additional_resource_records: u16, -} - -impl DnsHeader { - pub fn parse(packet: &[u8]) -> Result { - if packet.len() < 12 { - bail!("malformed datagram"); - } - - let datagram = DnsHeader { - transaction_id: [packet[0], packet[1]], - query_response: packet[2] & (1 << 7) > 0, - op_code: (packet[2] >> 3) & 0x0F, - authoritative_answer: packet[2] & (1 << 2) > 0, - truncation: packet[2] & (1 << 1) > 0, - recursion_desired: packet[2] & (1 << 0) > 0, - recursion_available: packet[3] & (1 << 7) > 0, - authentic_data: packet[3] & (1 << 5) > 0, - checking_disabled: packet[3] & (1 << 4) > 0, - response_code: packet[3] & 0xF, - number_of_questions: u16::from_be_bytes([packet[4], packet[5]]), - number_of_answers: u16::from_be_bytes([packet[6], packet[7]]), - number_of_authority_resource_records: u16::from_be_bytes([packet[8], packet[9]]), - number_of_additional_resource_records: u16::from_be_bytes([packet[10], packet[11]]), - }; - - Ok(datagram) - } -} - -impl Display for DnsHeader { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - writeln!( - f, - "transaction_id: {:x}{:x}", - self.transaction_id[0], self.transaction_id[1] - )?; - writeln!(f, "query_response: {}", self.query_response)?; - writeln!(f, "op_code: {}", self.op_code)?; - writeln!(f, "authoritative_answer: {}", self.authoritative_answer)?; - writeln!(f, "truncation: {}", self.truncation)?; - writeln!(f, "recursion_desired: {}", self.recursion_desired)?; - writeln!(f, "recursion_available: {}", self.recursion_available)?; - writeln!(f, "authentic_data: {}", self.authentic_data)?; - writeln!(f, "checking_disabled: {}", self.checking_disabled)?; - writeln!(f, "response_code: {}", self.response_code)?; - writeln!(f, "number_of_questions: {}", self.number_of_questions)?; - writeln!(f, "number_of_answers: {}", self.number_of_answers)?; - writeln!( - f, - "number_of_authority_rr: {}", - self.number_of_authority_resource_records - )?; - writeln!( - f, - "number_of_additional_rr: {}", - self.number_of_additional_resource_records - )?; - Ok(()) - } -} - -pub struct DnsQuestion { - pub domain: String, - pub question_type: u16, - pub question_class: u16, -} - -impl DnsQuestion { - pub fn parse(packet: &[u8], pos: &mut usize) -> Result { - let domain = parse_domain(packet, pos)?; - - if *pos + 4 > packet.len() { - bail!("malformed datagram"); - } - - let question_type = ((packet[*pos] as u16) << 8) | (packet[*pos + 1] as u16); - let question_class = ((packet[*pos + 2] as u16) << 8) | (packet[*pos + 3] as u16); - *pos += 4; - - let question = DnsQuestion { - domain, - question_type, - question_class, - }; - - Ok(question) - } -} - -impl Display for DnsQuestion { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}, {}, {}", - self.domain, self.question_type, self.question_class - ) - } -} - -pub struct DnsAnswer { - pub domain: String, - // TODO missing fields -} - -impl DnsAnswer { - pub fn to_bytes(&self) -> Vec { - let mut result = vec![]; - - for part in self.domain.split(".") { - result.push(part.len() as u8); - part.bytes().for_each(|c| result.push(c)); - } - - result.push(0); - result - } -} - -fn parse_domain(packet: &[u8], pos: &mut usize) -> Result { - let packet_size = packet.len(); - let mut domain_parts: Vec = vec![]; - - loop { - if *pos >= packet_size { - bail!("malformed datagram"); - } - - let len = packet[*pos] as usize; - if len > 63 { - bail!("malformed datagram"); - } - if len == 0 { - *pos += 1; - break; - } - - if (*pos + 1 + len) > packet_size { - bail!("malformed datagram"); - } - - let domain_part_bytes = &packet[(*pos + 1)..(*pos + 1 + len)]; - let domain_part = String::from_utf8_lossy(&domain_part_bytes); - domain_parts.push(domain_part.to_string()); - *pos += 1 + len; - } - - Ok(domain_parts.join(".")) -} diff --git a/src/dns/answer.rs b/src/dns/answer.rs new file mode 100644 index 0000000..2b67d31 --- /dev/null +++ b/src/dns/answer.rs @@ -0,0 +1,60 @@ +use anyhow::{Result, bail}; + +use crate::dns::{domain_to_bytes, parse_domain}; + +#[derive(Debug, Clone)] +pub struct Answer { + pub domain: String, + pub record_type: u16, + pub class: u16, + pub ttl: u32, + pub data: Vec, +} + +impl Answer { + pub fn parse(datagram: &[u8], pos: &mut usize) -> Result { + let domain = parse_domain(datagram, pos)?; + + if *pos + 10 > datagram.len() { + bail!("malformed datagram"); + } + + let record_type = u16::from_be_bytes([datagram[*pos], datagram[*pos + 1]]); + let class = u16::from_be_bytes([datagram[*pos + 2], datagram[*pos + 3]]); + let ttl = u32::from_be_bytes([ + datagram[*pos + 4], + datagram[*pos + 5], + datagram[*pos + 6], + datagram[*pos + 7], + ]); + let data_length = u16::from_be_bytes([datagram[*pos + 8], datagram[*pos + 9]]) as usize; + *pos += 10; + + if *pos + data_length > datagram.len() { + bail!("malformed datagram"); + } + + let data = datagram[*pos..*pos + data_length].to_vec(); + *pos += data_length; + + Ok(Answer { + domain, + record_type, + class, + ttl, + data, + }) + } + + pub fn to_bytes(&self) -> Result> { + let mut result = vec![]; + result.extend(domain_to_bytes(&self.domain)?); + result.extend(self.record_type.to_be_bytes()); + result.extend(self.class.to_be_bytes()); + result.extend(self.ttl.to_be_bytes()); + result.extend((self.data.len() as u16).to_be_bytes()); + result.extend_from_slice(&self.data); + + Ok(result) + } +} diff --git a/src/dns/constants.rs b/src/dns/constants.rs new file mode 100644 index 0000000..46f92f6 --- /dev/null +++ b/src/dns/constants.rs @@ -0,0 +1,25 @@ +pub const RECORD_TYPE_A: u16 = 0x01; +#[allow(dead_code)] +pub const RECORD_TYPE_NS: u16 = 0x02; +#[allow(dead_code)] +pub const RECORD_TYPE_CNAME: u16 = 0x05; +#[allow(dead_code)] +pub const RECORD_TYPE_AAAA: u16 = 0x1C; +#[allow(dead_code)] +pub const RECORD_TYPE_MX: u16 = 0x0F; + +pub const CLASS_IN: u16 = 0x01; +#[allow(dead_code)] +pub const CLASS_CHAOS: u16 = 0x03; +#[allow(dead_code)] +pub const CLASS_HS: u16 = 0x04; +#[allow(dead_code)] +pub const CLASS_ANY: u16 = 0xFF; + +pub const RESPONSE_CODE_NO_ERROR: u8 = 0x00; +#[allow(dead_code)] +pub const RESPONSE_CODE_NXDOMAIN: u8 = 0x03; +#[allow(dead_code)] +pub const RESPONSE_CODE_NOT_IMPLEMENTED: u8 = 0x04; +#[allow(dead_code)] +pub const RESPONSE_CODE_REFUSED: u8 = 0x05; diff --git a/src/dns/domainname.rs b/src/dns/domainname.rs new file mode 100644 index 0000000..652fd10 --- /dev/null +++ b/src/dns/domainname.rs @@ -0,0 +1,83 @@ +use anyhow::{Result, bail}; + +pub fn parse_domain(datagram: &[u8], pos: &mut usize) -> Result { + let mut domain_parts: Vec = vec![]; + let mut cursor = *pos; + let mut jumped = false; + let mut jumps = 0usize; + + loop { + if cursor >= datagram.len() { + bail!("malformed datagram"); + } + + let len = datagram[cursor] as usize; + if (len & 0xC0) == 0xC0 { + if cursor + 1 >= datagram.len() { + bail!("malformed datagram"); + } + + let pointer = ((len & 0x3F) << 8) | datagram[cursor + 1] as usize; + if pointer >= datagram.len() { + bail!("malformed datagram"); + } + if !jumped { + *pos = cursor + 2; + jumped = true; + } + + jumps += 1; + if jumps > datagram.len() { + bail!("malformed datagram"); + } + + cursor = pointer; + continue; + } + if (len & 0xC0) != 0 { + bail!("malformed datagram"); + } + if len > 63 { + bail!("malformed datagram"); + } + if len == 0 { + if !jumped { + *pos = cursor + 1; + } + break; + } + + if (cursor + 1 + len) > datagram.len() { + bail!("malformed datagram"); + } + + let domain_part_bytes = &datagram[(cursor + 1)..(cursor + 1 + len)]; + let domain_part = String::from_utf8_lossy(&domain_part_bytes); + domain_parts.push(domain_part.to_string()); + cursor += 1 + len; + } + + Ok(domain_parts.join(".")) +} + +pub fn domain_to_bytes(domain: &str) -> Result> { + let mut result = Vec::new(); + if domain.is_empty() { + result.push(0); + return Ok(result); + } + + for part in domain.split(".") { + if part.len() > 63 { + bail!("domain label too long"); + } + if part.is_empty() { + bail!("empty domain label"); + } + + result.push(part.len() as u8); + result.extend_from_slice(part.as_bytes()); + } + result.push(0); + Ok(result) +} diff --git a/src/dns/header.rs b/src/dns/header.rs new file mode 100644 index 0000000..a693be1 --- /dev/null +++ b/src/dns/header.rs @@ -0,0 +1,92 @@ +use anyhow::{Result, bail}; + +#[derive(Debug, Clone)] +pub struct Header { + pub transaction_id: [u8; 2], + pub query_response: bool, + pub op_code: u8, // 4 bits + pub authoritative_answer: bool, + pub truncation: bool, + pub recursion_desired: bool, + pub recursion_available: bool, + pub authentic_data: bool, + pub checking_disabled: bool, + pub response_code: u8, // 4 bits + pub number_of_questions: u16, + pub number_of_answers: u16, + pub number_of_authority_resource_records: u16, + pub number_of_additional_resource_records: u16, +} + +impl Header { + pub fn parse(datagram: &[u8]) -> Result
{ + if datagram.len() < 12 { + bail!("malformed datagram"); + } + + let datagram = Header { + transaction_id: [datagram[0], datagram[1]], + query_response: datagram[2] & (1 << 7) > 0, + op_code: (datagram[2] >> 3) & 0x0F, + authoritative_answer: datagram[2] & (1 << 2) > 0, + truncation: datagram[2] & (1 << 1) > 0, + recursion_desired: datagram[2] & (1 << 0) > 0, + recursion_available: datagram[3] & (1 << 7) > 0, + authentic_data: datagram[3] & (1 << 5) > 0, + checking_disabled: datagram[3] & (1 << 4) > 0, + response_code: datagram[3] & 0xF, + number_of_questions: u16::from_be_bytes([datagram[4], datagram[5]]), + number_of_answers: u16::from_be_bytes([datagram[6], datagram[7]]), + number_of_authority_resource_records: u16::from_be_bytes([datagram[8], datagram[9]]), + number_of_additional_resource_records: u16::from_be_bytes([datagram[10], datagram[11]]), + }; + + Ok(datagram) + } + + pub fn to_bytes(&self) -> Result> { + let mut buffer = [0u8; 12]; + let number_of_questions = self.number_of_questions.to_be_bytes(); + let number_of_answers = self.number_of_answers.to_be_bytes(); + let number_of_authority_resource_records = + self.number_of_authority_resource_records.to_be_bytes(); + let number_of_additional_resource_records = + self.number_of_additional_resource_records.to_be_bytes(); + + buffer[0] = self.transaction_id[0]; + buffer[1] = self.transaction_id[1]; + if self.query_response { + buffer[2] |= 1 << 7; + } + buffer[2] |= (self.op_code & 0x0F) << 3; + if self.authoritative_answer { + buffer[2] |= 1 << 2; + } + if self.truncation { + buffer[2] |= 1 << 1; + } + if self.recursion_desired { + buffer[2] |= 1 << 0; + } + if self.recursion_available { + buffer[3] |= 1 << 7; + } + if self.authentic_data { + buffer[3] |= 1 << 5; + } + if self.checking_disabled { + buffer[3] |= 1 << 4; + } + buffer[3] |= self.response_code & 0x0F; + buffer[4] = number_of_questions[0]; + buffer[5] = number_of_questions[1]; + buffer[6] = number_of_answers[0]; + buffer[7] = number_of_answers[1]; + buffer[8] = number_of_authority_resource_records[0]; + buffer[9] = number_of_authority_resource_records[1]; + buffer[10] = number_of_additional_resource_records[0]; + buffer[11] = number_of_additional_resource_records[1]; + + Ok(buffer.into()) + } +} diff --git a/src/dns/message.rs b/src/dns/message.rs new file mode 100644 index 0000000..925dbe2 --- /dev/null +++ b/src/dns/message.rs @@ -0,0 +1,69 @@ +use anyhow::Result; + +use crate::dns::{Answer, Header, Question, RESPONSE_CODE_NO_ERROR}; + +#[derive(Debug, Clone)] +pub struct Message { + pub header: Header, + pub questions: Vec, + pub answers: Vec, +} + +impl Message { + pub fn parse(datagram: &[u8]) -> Result { + let header = Header::parse(datagram)?; + + let mut message = Message { + header: header, + questions: vec![], + answers: vec![], + }; + + let mut pos = 12usize; + for _ in 0..(message.header.number_of_questions) { + let question = Question::parse(datagram, &mut pos)?; + message.questions.push(question); + } + for _ in 0..(message.header.number_of_answers) { + let answer = Answer::parse(datagram, &mut pos)?; + message.answers.push(answer); + } + + Ok(message) + } + + pub fn to_bytes(&self) -> Result> { + let mut result: Vec = vec![]; + result.extend(self.header.to_bytes()?); + for question in &self.questions { + result.extend(question.to_bytes()?); + } + for answer in &self.answers { + result.extend(answer.to_bytes()?); + } + Ok(result) + } + + pub fn prepare_response(&self) -> Message { + Message { + header: Header { + transaction_id: self.header.transaction_id, + query_response: true, + op_code: self.header.op_code, + authoritative_answer: true, + truncation: false, + recursion_desired: self.header.recursion_desired, + recursion_available: false, + authentic_data: false, + checking_disabled: self.header.checking_disabled, + response_code: RESPONSE_CODE_NO_ERROR, + number_of_questions: self.header.number_of_questions, + number_of_answers: 0, + number_of_authority_resource_records: 0, + number_of_additional_resource_records: 0, + }, + questions: self.questions.clone(), + answers: vec![], + } + } +} diff --git a/src/dns/mod.rs b/src/dns/mod.rs new file mode 100644 index 0000000..717d9b4 --- /dev/null +++ b/src/dns/mod.rs @@ -0,0 +1,64 @@ +mod answer; +mod constants; +mod domainname; +mod header; +mod message; +mod question; + +pub use answer::*; +pub use constants::*; +pub use domainname::*; +pub use header::*; +pub use message::*; +pub use question::*; + +#[cfg(test)] +mod tests { + use super::*; + + fn a_query_datagram() -> Vec { + let mut datagram = vec![ + 0x12, 0x34, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ]; + datagram.extend_from_slice(&domain_to_bytes("example.com").unwrap()); + datagram.extend_from_slice(&RECORD_TYPE_A.to_be_bytes()); + datagram.extend_from_slice(&CLASS_IN.to_be_bytes()); + datagram + } + + #[test] + fn response_keeps_original_question_section() { + let query = Message::parse(&a_query_datagram()).unwrap(); + let mut response = query.prepare_response(); + response.answers.push(Answer { + domain: "example.com".to_string(), + record_type: RECORD_TYPE_A, + class: CLASS_IN, + ttl: 1, + data: vec![127, 0, 0, 1], + }); + response.header.number_of_answers = 1; + + let bytes = response.to_bytes().unwrap(); + assert_eq!(u16::from_be_bytes([bytes[4], bytes[5]]), 1); + assert_eq!(u16::from_be_bytes([bytes[6], bytes[7]]), 1); + assert_eq!(&bytes[12..29], &a_query_datagram()[12..29]); + } + + #[test] + fn parses_compressed_answer_names() { + let mut datagram = a_query_datagram(); + datagram[2] = 0x81; + datagram[3] = 0x80; + datagram[6] = 0x00; + datagram[7] = 0x01; + datagram.extend_from_slice(&[ + 0xC0, 0x0C, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x04, 127, 0, 0, 1, + ]); + + let message = Message::parse(&datagram).unwrap(); + assert_eq!(message.answers.len(), 1); + assert_eq!(message.answers[0].domain, "example.com"); + assert_eq!(message.answers[0].data, vec![127, 0, 0, 1]); + } +} diff --git a/src/dns/question.rs b/src/dns/question.rs new file mode 100644 index 0000000..37eb353 --- /dev/null +++ b/src/dns/question.rs @@ -0,0 +1,39 @@ +use anyhow::{Result, bail}; + +use crate::dns::{domain_to_bytes, parse_domain}; + +#[derive(Debug, Clone)] +pub struct Question { + pub domain: String, + pub question_type: u16, + pub question_class: u16, +} + +impl Question { + pub fn parse(datagram: &[u8], pos: &mut usize) -> Result { + let domain = parse_domain(datagram, pos)?; + + if *pos + 4 > datagram.len() { + bail!("malformed datagram"); + } + + let question_type = ((datagram[*pos] as u16) << 8) | (datagram[*pos + 1] as u16); + let question_class = ((datagram[*pos + 2] as u16) << 8) | (datagram[*pos + 3] as u16); + *pos += 4; + + let question = Question { + domain, + question_type, + question_class, + }; + + Ok(question) + } + + pub fn to_bytes(&self) -> Result> { + let mut result = domain_to_bytes(&self.domain)?; + result.extend(self.question_type.to_be_bytes()); + result.extend(self.question_class.to_be_bytes()); + Ok(result) + } +} diff --git a/src/macros.rs b/src/macros.rs new file mode 100644 index 0000000..96a8bb7 --- /dev/null +++ b/src/macros.rs @@ -0,0 +1,6 @@ +#[macro_export] +macro_rules! txid { + ($bytes:expr) => { + format!("{:02x}{:02x}", $bytes[0], $bytes[1]) + }; +} diff --git a/src/main.rs b/src/main.rs index 4fa74c5..33f7ee6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,42 +1,106 @@ mod config; mod dns; mod logging; +mod macros; + +use std::net::SocketAddr; use anyhow::Result; use dotenv::dotenv; use tokio::net::UdpSocket; +use tracing::{Instrument, Level}; use crate::dns::*; #[tokio::main] async fn main() -> Result<()> { dotenv().ok(); - let config = config::load()?; logging::setup(); + let config = config::load()?; tracing::info!("start server on port {}", config.port); let sock = UdpSocket::bind(("0.0.0.0", config.port)).await?; - let mut buffer = [0u8; 512]; loop { let (size, addr) = sock.recv_from(&mut buffer).await?; - tracing::info!("Received datagram from {}", addr.to_string()); - if let Err(err) = handle_datagram(&buffer[..size]) { + let result = handle_datagram(&config, &sock, &addr, &buffer[..size]).await; + if let Err(err) = result { tracing::error!("failed to handle datagram: {err}"); } } } -fn handle_datagram(packet: &[u8]) -> Result<()> { - let header = DnsHeader::parse(packet)?; - println!("Received:\n{}", header); +async fn handle_datagram( + config: &config::ConfigData, + sock: &UdpSocket, + addr: &SocketAddr, + datagram: &[u8], +) -> Result<()> { + let message = Message::parse(datagram)?; + let span = tracing::span!( + Level::INFO, + "handle_datagram", + transaction_id = %txid!(message.header.transaction_id), + client = %addr, + ); - let mut pos = 12usize; - for _ in 0..(header.number_of_questions) { - let question = DnsQuestion::parse(packet, &mut pos)?; - println!("Question: {}", question); + let result: Result<()> = async { + tracing::info!("received message"); + + if message.header.query_response { + tracing::error!("message is response, drop"); + return Ok(()); + } + if message.header.op_code != 0 { + tracing::error!("op code is not 0, drop"); + return Ok(()); + } + + let mut response = message.prepare_response(); + for question in message.questions { + if question.question_type != RECORD_TYPE_A { + tracing::error!( + "only A records are supported yet, requested: {}", + question.question_type + ); + return Ok(()); + } + if question.question_class != CLASS_IN { + tracing::error!( + "only IN class records are supported yet, requested: {}", + question.question_class + ); + return Ok(()); + } + + let address = config.get_address_by_domain(&question.domain); + match address { + Some(address) => { + let answer = Answer { + domain: question.domain, + record_type: RECORD_TYPE_A, + class: CLASS_IN, + ttl: 1, + data: address.to_vec(), + }; + + response.answers.push(answer); + response.header.number_of_answers += 1; + } + None => { + response.header.response_code = RESPONSE_CODE_NXDOMAIN; + } + } + } + + let response_bytes = response.to_bytes()?; + sock.send_to(&response_bytes, addr).await?; + + Ok(()) } + .instrument(span) + .await; - Ok(()) + result }