diff --git a/README.md b/README.md index 775f877..6b1fdab 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,7 @@ Project or files: ├── __network │   ├── async-speed-limit │   ├── dingo +│   ├── dns-server-tutorial │   ├── fetch-rs │   ├── fuso-socks5-test │   ├── html-crawl-parse @@ -247,7 +248,6 @@ Project or files: ├── __translate │   └── retranslate ├── __wasm -│   ├── connect.ssh │   ├── deno_rust_wasm_import_functions │   ├── deno_rust_wasm_js_sandbox │   ├── deno_rust_wasm_qr_decode @@ -277,7 +277,6 @@ Project or files: ├── scripts │   └── build_readme_rs └── single_file_tests - ├── 99.htm ├── 99.rs ├── chain.rs ├── closure.rs @@ -300,6 +299,6 @@ Project or files: ├── vec.rs └── while.rs -269 directories, 40 files +271 directories, 38 files ``` diff --git a/__network/dns-server-tutorial/README.md b/__network/dns-server-tutorial/README.md new file mode 100644 index 0000000..5147b99 --- /dev/null +++ b/__network/dns-server-tutorial/README.md @@ -0,0 +1,49 @@ +# DNS Server in Rust + +This repository contains the code for a **DNS server implemented in Rust**, as part of the tutorial [**Building a DNS Server in Rust**](https://rust-trends.com/posts/building-a-dns-server-in-rust/) . The tutorial covers: + - Understanding DNS requests and responses. + - Handling UDP packets in Rust. + - Parsing and constructing DNS packets. + - Implementing decompression of DNS packets. + - Forwarding DNS queries to resolvers. + +## 📖 Tutorial +For a step-by-step guide, check out the full tutorial: +[Building a DNS Server in Rust: Part 1 of 2](https://rust-trends.com/posts/building-a-dns-server-in-rust/) + +## 🛠 Installation + +Ensure you have **Rust** installed: +```sh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +``` + +## Clone this repository: + +```sh +git clone https://github.com/Rust-Trends/dns-server-tutorial.git +cd dns-server-tutorial +``` + +## 🚀 Running the Server + +Goto to the step you want to explore, e.g. step1, and start the DNS server on port 1053: + +```sh +cd step1 +cargo run +``` + +## 🔍 Testing with dig + +To test your server, open another terminal and run: + +```sh +dig @localhost -p 1053 www.rust-trends.com +``` + +## 🤝 Contributions + +Feel free to open issues or submit pull requests to improve the project! + +📜 License: MIT diff --git a/__network/dns-server-tutorial/step1/Cargo.toml b/__network/dns-server-tutorial/step1/Cargo.toml new file mode 100644 index 0000000..4f27367 --- /dev/null +++ b/__network/dns-server-tutorial/step1/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "dns-server" +authors = ["Bob Peters "] +version = "0.1.0" +edition = "2021" + +[dependencies] +clap = { version = "4.5.29", features = ["derive"] } +thiserror = "2.0.11" diff --git a/__network/dns-server-tutorial/step1/src/dns.rs b/__network/dns-server-tutorial/step1/src/dns.rs new file mode 100644 index 0000000..c69659e --- /dev/null +++ b/__network/dns-server-tutorial/step1/src/dns.rs @@ -0,0 +1,78 @@ +// src/dns.rs +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum ErrorCondition { + #[error("Serialization Error: {0}")] + SerializationErr(String), + + #[error("Deserialization Error: {0}")] + DeserializationErr(String), +} + +#[derive(Debug)] +pub struct Header { + pub id: u16, // identifier + pub qr: bool, // 0 for query, 1 for response + pub opcode: u8, // 0 for standard query + pub aa: bool, // authoritative answer + pub tc: bool, // truncated message + pub rd: bool, // recursion desired + pub ra: bool, // recursion available + pub z: u8, // reserved for future use + pub rcode: u8, // 0 for no error + pub qdcount: u16, // number of entries in the question section + pub ancount: u16, // number of resource records in the answer section + pub nscount: u16, // number of name server resource records in the authority records section + pub arcount: u16, // number of resource records in the additional records section +} + +impl Header { + const DNS_HEADER_LEN: usize = 12; + + // Serialize the header to a byte array + pub fn to_bytes(&self) -> Vec { + let mut buf = Vec::with_capacity(Header::DNS_HEADER_LEN); + + buf.extend_from_slice(&self.id.to_be_bytes()); + buf.push( + (self.qr as u8) << 7 + | self.opcode << 3 + | (self.aa as u8) << 2 + | (self.tc as u8) << 1 + | self.rd as u8, + ); + buf.push((self.ra as u8) << 7 | self.z << 4 | self.rcode); + buf.extend_from_slice(&self.qdcount.to_be_bytes()); + buf.extend_from_slice(&self.ancount.to_be_bytes()); + buf.extend_from_slice(&self.nscount.to_be_bytes()); + buf.extend_from_slice(&self.arcount.to_be_bytes()); + + buf + } + + // Deserialize the header from a byte array + pub fn from_bytes(buf: &[u8]) -> Result { + if buf.len() < Header::DNS_HEADER_LEN { + return Err(ErrorCondition::DeserializationErr( + "Buffer length is less than header length".to_string(), + )); + } + + Ok(Header { + id: u16::from_be_bytes([buf[0], buf[1]]), + qr: (buf[2] & 0b1000_0000) != 0, + opcode: (buf[2] & 0b0111_1000) >> 3, + aa: (buf[2] & 0b0000_0100) != 0, + tc: (buf[2] & 0b0000_0010) != 0, + rd: (buf[2] & 0b0000_0001) != 0, + ra: (buf[3] & 0b1000_0000) != 0, + z: (buf[3] & 0b0111_0000) >> 4, + rcode: buf[3] & 0b0000_1111, + qdcount: u16::from_be_bytes([buf[4], buf[5]]), + ancount: u16::from_be_bytes([buf[6], buf[7]]), + nscount: u16::from_be_bytes([buf[8], buf[9]]), + arcount: u16::from_be_bytes([buf[10], buf[11]]), + }) + } +} diff --git a/__network/dns-server-tutorial/step1/src/main.rs b/__network/dns-server-tutorial/step1/src/main.rs new file mode 100644 index 0000000..d5c685b --- /dev/null +++ b/__network/dns-server-tutorial/step1/src/main.rs @@ -0,0 +1,44 @@ +// src/main.rs +use std::net::UdpSocket; + +mod dns; +use dns::Header; + +// Debug print hex bytes of a buffer 16 bytes width followed by the ASCII representation of the bytes +fn debug_print_bytes(buf: &[u8]) { + for (i, chunk) in buf.chunks(16).enumerate() { + print!("{:08x}: ", i * 16); + for byte in chunk { + print!("{:02x} ", byte); + } + for _ in 0..(16 - chunk.len()) { + print!(" "); + } + print!(" "); + for byte in chunk { + if *byte >= 32 && *byte <= 126 { + print!("{}", *byte as char); + } else { + print!("."); + } + } + println!(); + } +} + +fn main() { + let socket = UdpSocket::bind("0.0.0.0:1053").expect("Could not bind to port 1053"); + let mut buf = [0; 512]; + + println!("DNS server is running at port 1053"); + + loop { + let (len, addr) = socket.recv_from(&mut buf).expect("Could not receive data"); + + println!("\nReceived query from {} with length {} bytes", addr, len); + debug_print_bytes(&buf[..len]); + + let header = Header::from_bytes(&buf[..len]).expect("Could not parse DNS header"); + println!("\n{:?}", header); + } +} diff --git a/__network/dns-server-tutorial/step2/Cargo.toml b/__network/dns-server-tutorial/step2/Cargo.toml new file mode 100644 index 0000000..4f27367 --- /dev/null +++ b/__network/dns-server-tutorial/step2/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "dns-server" +authors = ["Bob Peters "] +version = "0.1.0" +edition = "2021" + +[dependencies] +clap = { version = "4.5.29", features = ["derive"] } +thiserror = "2.0.11" diff --git a/__network/dns-server-tutorial/step2/src/dns.rs b/__network/dns-server-tutorial/step2/src/dns.rs new file mode 100644 index 0000000..3b0b49c --- /dev/null +++ b/__network/dns-server-tutorial/step2/src/dns.rs @@ -0,0 +1,306 @@ +// src/dns.rs +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum ErrorCondition { + #[error("Serialization Error: {0}")] + SerializationErr(String), + + #[error("Deserialization Error: {0}")] + DeserializationErr(String), + + #[error("Invalid Label")] + InvalidLabel, +} + +/// Maximum DNS message size without EDNS0 +const MAX_DNS_MESSAGE_SIZE: usize = 512; + +#[derive(Debug)] +pub struct Header { + pub id: u16, // identifier + pub qr: bool, // 0 for query, 1 for response + pub opcode: u8, // 0 for standard query + pub aa: bool, // authoritative answer + pub tc: bool, // truncated message + pub rd: bool, // recursion desired + pub ra: bool, // recursion available + pub z: u8, // reserved for future use + pub rcode: u8, // 0 for no error + pub qdcount: u16, // number of entries in the question section + pub ancount: u16, // number of resource records in the answer section + pub nscount: u16, // number of name server resource records in the authority records section + pub arcount: u16, // number of resource records in the additional records section +} + +impl Header { + const DNS_HEADER_LEN: usize = 12; + + // Serialize the header to a byte array + pub fn to_bytes(&self) -> Vec { + let mut buf = Vec::with_capacity(Header::DNS_HEADER_LEN); + + buf.extend_from_slice(&self.id.to_be_bytes()); + buf.push( + (self.qr as u8) << 7 + | self.opcode << 3 + | (self.aa as u8) << 2 + | (self.tc as u8) << 1 + | self.rd as u8, + ); + buf.push((self.ra as u8) << 7 | self.z << 4 | self.rcode); + buf.extend_from_slice(&self.qdcount.to_be_bytes()); + buf.extend_from_slice(&self.ancount.to_be_bytes()); + buf.extend_from_slice(&self.nscount.to_be_bytes()); + buf.extend_from_slice(&self.arcount.to_be_bytes()); + + buf + } + + // Deserialize the header from a byte array + pub fn from_bytes(buf: &[u8]) -> Result { + if buf.len() < Header::DNS_HEADER_LEN { + return Err(ErrorCondition::DeserializationErr( + "Buffer length is less than header length".to_string(), + )); + } + + Ok(Header { + id: u16::from_be_bytes([buf[0], buf[1]]), + qr: (buf[2] & 0b1000_0000) != 0, + opcode: (buf[2] & 0b0111_1000) >> 3, + aa: (buf[2] & 0b0000_0100) != 0, + tc: (buf[2] & 0b0000_0010) != 0, + rd: (buf[2] & 0b0000_0001) != 0, + ra: (buf[3] & 0b1000_0000) != 0, + z: (buf[3] & 0b0111_0000) >> 4, + rcode: buf[3] & 0b0000_1111, + qdcount: u16::from_be_bytes([buf[4], buf[5]]), + ancount: u16::from_be_bytes([buf[6], buf[7]]), + nscount: u16::from_be_bytes([buf[8], buf[9]]), + arcount: u16::from_be_bytes([buf[10], buf[11]]), + }) + } +} + +#[derive(Debug, Clone)] +pub struct Question { + pub name: Vec