.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
/target
|
||||
.env
|
||||
nanodns.toml
|
||||
test.sh
|
||||
+57
-4
@@ -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<Entry>,
|
||||
}
|
||||
|
||||
pub struct ConfigData {
|
||||
pub port: u16,
|
||||
pub entries: Vec<Entry>,
|
||||
}
|
||||
|
||||
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<ConfigData> {
|
||||
let env_config = {
|
||||
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 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,
|
||||
})
|
||||
}
|
||||
|
||||
-179
@@ -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<DnsHeader> {
|
||||
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<DnsQuestion> {
|
||||
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<u8> {
|
||||
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<String> {
|
||||
let packet_size = packet.len();
|
||||
let mut domain_parts: Vec<String> = 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("."))
|
||||
}
|
||||
@@ -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<u8>,
|
||||
}
|
||||
|
||||
impl Answer {
|
||||
pub fn parse(datagram: &[u8], pos: &mut usize) -> Result<Answer> {
|
||||
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<Vec<u8>> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1,83 @@
|
||||
use anyhow::{Result, bail};
|
||||
|
||||
pub fn parse_domain(datagram: &[u8], pos: &mut usize) -> Result<String> {
|
||||
let mut domain_parts: Vec<String> = 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<Vec<u8>> {
|
||||
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)
|
||||
}
|
||||
@@ -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<Header> {
|
||||
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<Vec<u8>> {
|
||||
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())
|
||||
}
|
||||
}
|
||||
@@ -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<Question>,
|
||||
pub answers: Vec<Answer>,
|
||||
}
|
||||
|
||||
impl Message {
|
||||
pub fn parse(datagram: &[u8]) -> Result<Message> {
|
||||
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<Vec<u8>> {
|
||||
let mut result: Vec<u8> = 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![],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<u8> {
|
||||
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]);
|
||||
}
|
||||
}
|
||||
@@ -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<Question> {
|
||||
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<Vec<u8>> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#[macro_export]
|
||||
macro_rules! txid {
|
||||
($bytes:expr) => {
|
||||
format!("{:02x}{:02x}", $bytes[0], $bytes[1])
|
||||
};
|
||||
}
|
||||
+75
-11
@@ -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;
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user