This commit is contained in:
2026-07-04 01:40:03 +02:00
parent 77f24e2abf
commit b0e230d4d8
12 changed files with 576 additions and 199 deletions
+1
View File
@@ -1,3 +1,4 @@
/target
.env
nanodns.toml
test.sh
+57 -4
View File
@@ -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
View File
@@ -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("."))
}
+60
View File
@@ -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)
}
}
+25
View File
@@ -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;
+83
View File
@@ -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)
}
+92
View File
@@ -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())
}
}
+69
View File
@@ -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![],
}
}
}
+64
View File
@@ -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]);
}
}
+39
View File
@@ -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)
}
}
+6
View File
@@ -0,0 +1,6 @@
#[macro_export]
macro_rules! txid {
($bytes:expr) => {
format!("{:02x}{:02x}", $bytes[0], $bytes[1])
};
}
+75 -11
View File
@@ -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
}