├── query_packet.txt ├── response_packet.txt ├── Cargo.lock ├── src ├── protocol │ ├── mod.rs │ ├── resultcode.rs │ ├── main.rs │ ├── querytype.rs │ ├── dnsquestion.rs │ ├── dnsheader.rs │ ├── dnspacket.rs │ ├── byte_packet_buffer.rs │ └── dnsrecord.rs └── main.rs ├── Cargo.toml ├── .gitignore ├── Dockerfile ├── .github └── workflows │ ├── ci.yml │ ├── docker-release.yml │ └── github-release.yml ├── LICENSE ├── CONTRIBUTING.MD └── README.md /query_packet.txt: -------------------------------------------------------------------------------- 1 | @h googlecom -------------------------------------------------------------------------------- /response_packet.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SkySingh04/DNS-Server-Rust/HEAD/response_packet.txt -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "DNS-Server-Rust" 7 | version = "0.1.0" 8 | -------------------------------------------------------------------------------- /src/protocol/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod byte_packet_buffer; 2 | pub mod dnspacket; 3 | pub mod dnsrecord; 4 | pub mod querytype; 5 | pub mod resultcode; 6 | pub mod dnsheader; 7 | pub mod dnsquestion; 8 | pub mod main; -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "DNS-Server-Rust" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | 7 | # These are backup files generated by rustfmt 8 | **/*.rs.bk 9 | 10 | # MSVC Windows builds of rustc generate these, which store debugging information 11 | *.pdb 12 | 13 | 14 | # Added by cargo 15 | 16 | /target 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:1.73 as builder 2 | 3 | WORKDIR /app 4 | 5 | COPY Cargo.toml Cargo.lock ./ 6 | 7 | COPY . . 8 | 9 | 10 | RUN cargo build --release 11 | 12 | FROM debian:bullseye-slim 13 | 14 | RUN apt-get update && apt-get install -y \ 15 | ca-certificates && \ 16 | rm -rf /var/lib/apt/lists/* 17 | 18 | WORKDIR /app 19 | 20 | COPY --from=builder /app/target/release/DNS-Server-Rust /usr/local/bin/ 21 | 22 | CMD ["DNS-Server-Rust"] 23 | -------------------------------------------------------------------------------- /src/protocol/resultcode.rs: -------------------------------------------------------------------------------- 1 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 2 | pub enum ResultCode { 3 | NoError = 0, 4 | FormErr = 1, 5 | ServFail = 2, 6 | NXDomain = 3, 7 | NotImp = 4, 8 | Refused = 5, 9 | } 10 | 11 | impl ResultCode { 12 | pub fn from_num(num: u8) -> ResultCode { 13 | match num { 14 | 1 => ResultCode::FormErr, 15 | 2 => ResultCode::ServFail, 16 | 3 => ResultCode::NXDomain, 17 | 4 => ResultCode::NotImp, 18 | 5 => ResultCode::Refused, 19 | 0 => ResultCode::NoError, 20 | _ => ResultCode::NoError, 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Lint Test Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - staging 8 | pull_request: 9 | branches: 10 | - main 11 | - staging 12 | 13 | jobs: 14 | lint-build-test: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@v3 20 | 21 | - name: Set up Rust 22 | uses: actions-rs/toolchain@v1 23 | with: 24 | toolchain: stable 25 | profile: minimal 26 | 27 | - name: Install Clippy (Rust linter) 28 | run: rustup component add clippy 29 | 30 | - name: Run Linter 31 | run: cargo clippy -- -D warnings 32 | 33 | - name: Run Build 34 | run: cargo build --release 35 | 36 | - name: Run Tests 37 | run: cargo test 38 | -------------------------------------------------------------------------------- /.github/workflows/docker-release.yml: -------------------------------------------------------------------------------- 1 | name: Docker Release 2 | 3 | on: 4 | workflow_run: 5 | workflows: 6 | - Lint Test Build 7 | branches: 8 | - main 9 | types: 10 | - completed 11 | 12 | jobs: 13 | build-and-push: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v3 19 | 20 | - name: Log in to Docker Hub 21 | uses: docker/login-action@v2 22 | with: 23 | username: ${{ secrets.DOCKER_USERNAME }} 24 | password: ${{ secrets.DOCKER_PASSWORD }} 25 | 26 | - name: Build and Push Docker Image 27 | uses: docker/build-push-action@v5 28 | with: 29 | context: . 30 | push: true 31 | tags: ${{ secrets.DOCKER_USERNAME }}/dns-server-rust:${{ github.ref_name }} 32 | -------------------------------------------------------------------------------- /src/protocol/main.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | // use crate::File; 3 | use std::fs::File; 4 | use std::io::Read; 5 | use crate::protocol::byte_packet_buffer::BytePacketBuffer; 6 | use crate::protocol::dnspacket::DnsPacket; 7 | 8 | #[allow(dead_code)] 9 | fn test_with_response_packet() -> Result<(), Box> { 10 | let mut f = File::open("response_packet.Txt")?; 11 | let mut buffer = BytePacketBuffer::new(); 12 | let _ = f.read(&mut buffer.buf)?; 13 | 14 | let packet = DnsPacket::from_buffer(&mut buffer)?; 15 | println!("{:#?}", packet.header); 16 | 17 | for q in packet.questions { 18 | println!("{:#?}", q); 19 | } 20 | for rec in packet.answers { 21 | println!("{:#?}", rec); 22 | } 23 | for rec in packet.authorities { 24 | println!("{:#?}", rec); 25 | } 26 | for rec in packet.resources { 27 | println!("{:#?}", rec); 28 | } 29 | 30 | Ok(()) 31 | } 32 | -------------------------------------------------------------------------------- /src/protocol/querytype.rs: -------------------------------------------------------------------------------- 1 | #[derive(PartialEq, Eq, Debug, Clone, Hash, Copy)] 2 | pub enum QueryType { 3 | Unknown(u16), 4 | A, // 1 5 | NS, // 2 6 | Cname, // 5 7 | Soa, // 6 8 | MX , // 15 9 | Txt, // 16 10 | Aaaa, // 28 11 | } 12 | 13 | impl QueryType { 14 | pub fn to_num(self) -> u16 { 15 | match self { 16 | QueryType::Unknown(x) => x, 17 | QueryType::A => 1, 18 | QueryType::NS => 2, 19 | QueryType::Cname => 5, 20 | QueryType::Soa => 6, 21 | QueryType::MX => 15, 22 | QueryType::Txt => 16, 23 | QueryType::Aaaa => 28, 24 | } 25 | } 26 | 27 | pub fn from_num(num: u16) -> QueryType { 28 | match num { 29 | 1 => QueryType::A, 30 | 2 => QueryType::NS, 31 | 5 => QueryType::Cname, 32 | 6 => QueryType::Soa, 33 | 15 => QueryType::MX, 34 | 16 => QueryType::Txt, 35 | 28 => QueryType::Aaaa, 36 | _ => QueryType::Unknown(num), 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/protocol/dnsquestion.rs: -------------------------------------------------------------------------------- 1 | use crate::protocol::byte_packet_buffer::BytePacketBuffer; 2 | use crate::protocol::querytype::QueryType; 3 | use std::error::Error; 4 | 5 | #[derive(Debug, Clone, PartialEq, Eq)] 6 | pub struct DnsQuestion { 7 | pub name: String, 8 | pub qtype: QueryType, 9 | } 10 | 11 | impl DnsQuestion { 12 | pub fn new(name: String, qtype: QueryType) -> DnsQuestion { 13 | DnsQuestion { 14 | name, 15 | qtype, 16 | } 17 | } 18 | 19 | pub fn read(&mut self, buffer: &mut BytePacketBuffer) -> Result<(), Box> { 20 | buffer.read_qname(&mut self.name)?; 21 | self.qtype = QueryType::from_num(buffer.read_u16()?); // qtype 22 | let _ = buffer.read_u16()?; // class 23 | 24 | Ok(()) 25 | } 26 | pub fn write(&self, buffer: &mut BytePacketBuffer) -> Result<() , Box> { 27 | buffer.write_qname(&self.name)?; 28 | 29 | let typenum = self.qtype.to_num(); 30 | buffer.write_u16(typenum)?; 31 | buffer.write_u16(1)?; 32 | 33 | Ok(()) 34 | } 35 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Sky Singh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/github-release.yml: -------------------------------------------------------------------------------- 1 | name: Github Release 2 | 3 | on: 4 | workflow_run: 5 | workflows: 6 | - Lint Test Build 7 | branches: 8 | - main 9 | types: 10 | - completed 11 | 12 | env: 13 | REGISTRY: ghcr.io 14 | IMAGE_NAME: ${{ github.repository }} 15 | 16 | jobs: 17 | build-and-push-image: 18 | runs-on: ubuntu-latest 19 | permissions: 20 | contents: read 21 | packages: write 22 | steps: 23 | - name: Checkout repository 24 | uses: actions/checkout@v4 25 | - name: Log in to the Container registry 26 | uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 27 | with: 28 | registry: ${{ env.REGISTRY }} 29 | username: ${{ github.actor }} 30 | password: ${{ secrets.GITHUB_TOKEN }} 31 | - name: Extract metadata (tags, labels) for Docker 32 | id: meta 33 | uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 34 | with: 35 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 36 | - name: Build and push dns server Docker image 37 | id: gocrab 38 | uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4 39 | with: 40 | context: . 41 | push: true 42 | tags: ${{ steps.meta.outputs.tags }} 43 | labels: ${{ steps.meta.outputs.labels }} 44 | file: ./Dockerfile 45 | -------------------------------------------------------------------------------- /src/protocol/dnsheader.rs: -------------------------------------------------------------------------------- 1 | use crate::protocol::byte_packet_buffer::BytePacketBuffer; 2 | // use std::io::Error; 3 | use crate::protocol::resultcode::ResultCode; 4 | #[derive(Clone, Debug)] 5 | pub struct DnsHeader { 6 | pub id: u16, // 16 bits 7 | 8 | pub recursion_desired: bool, // 1 bit 9 | pub truncated_message: bool, // 1 bit 10 | pub authoritative_answer: bool, // 1 bit 11 | pub opcode: u8, // 4 bits 12 | pub response: bool, // 1 bit 13 | 14 | pub rescode: ResultCode, // 4 bits 15 | pub checking_disabled: bool, // 1 bit 16 | pub authed_data: bool, // 1 bit 17 | pub z: bool, // 1 bit 18 | pub recursion_available: bool, // 1 bit 19 | 20 | pub questions: u16, // 16 bits 21 | pub answers: u16, // 16 bits 22 | pub authoritative_entries: u16, // 16 bits 23 | pub resource_entries: u16, // 16 bits 24 | } 25 | 26 | //ye implementation mat puchna , its a lot of bit manipulation circus 27 | impl Default for DnsHeader { 28 | fn default() -> Self { 29 | DnsHeader::new() 30 | } 31 | } 32 | 33 | impl DnsHeader { 34 | pub fn new() -> DnsHeader { 35 | DnsHeader { 36 | id: 0, 37 | 38 | recursion_desired: false, 39 | truncated_message: false, 40 | authoritative_answer: false, 41 | opcode: 0, 42 | response: false, 43 | 44 | rescode: ResultCode::NoError, 45 | checking_disabled: false, 46 | authed_data: false, 47 | z: false, 48 | recursion_available: false, 49 | 50 | questions: 0, 51 | answers: 0, 52 | authoritative_entries: 0, 53 | resource_entries: 0, 54 | } 55 | } 56 | 57 | pub fn read(&mut self, buffer: &mut BytePacketBuffer) -> Result<(), Box> { 58 | self.id = buffer.read_u16()?; 59 | 60 | let flags = buffer.read_u16()?; 61 | let a = (flags >> 8) as u8; 62 | let b = (flags & 0xFF) as u8; 63 | self.recursion_desired = (a & (1 << 0)) > 0; 64 | self.truncated_message = (a & (1 << 1)) > 0; 65 | self.authoritative_answer = (a & (1 << 2)) > 0; 66 | self.opcode = (a >> 3) & 0x0F; 67 | self.response = (a & (1 << 7)) > 0; 68 | 69 | self.rescode = ResultCode::from_num(b & 0x0F); 70 | self.checking_disabled = (b & (1 << 4)) > 0; 71 | self.authed_data = (b & (1 << 5)) > 0; 72 | self.z = (b & (1 << 6)) > 0; 73 | self.recursion_available = (b & (1 << 7)) > 0; 74 | 75 | self.questions = buffer.read_u16()?; 76 | self.answers = buffer.read_u16()?; 77 | self.authoritative_entries = buffer.read_u16()?; 78 | self.resource_entries = buffer.read_u16()?; 79 | 80 | Ok(()) 81 | } 82 | pub fn write(&self, buffer: &mut BytePacketBuffer) -> Result<(),Box> { 83 | buffer.write_u16(self.id)?; 84 | 85 | buffer.write_u8( 86 | (self.recursion_desired as u8) 87 | | ((self.truncated_message as u8) << 1) 88 | | ((self.authoritative_answer as u8) << 2) 89 | | (self.opcode << 3) 90 | | ((self.response as u8) << 7), 91 | )?; 92 | 93 | buffer.write_u8( 94 | (self.rescode as u8) 95 | | ((self.checking_disabled as u8) << 4) 96 | | ((self.authed_data as u8) << 5) 97 | | ((self.z as u8) << 6) 98 | | ((self.recursion_available as u8) << 7), 99 | )?; 100 | 101 | buffer.write_u16(self.questions)?; 102 | buffer.write_u16(self.answers)?; 103 | buffer.write_u16(self.authoritative_entries)?; 104 | buffer.write_u16(self.resource_entries)?; 105 | 106 | Ok(()) 107 | } 108 | } -------------------------------------------------------------------------------- /src/protocol/dnspacket.rs: -------------------------------------------------------------------------------- 1 | use std::net::Ipv4Addr; 2 | 3 | use crate::protocol::byte_packet_buffer::BytePacketBuffer; 4 | use crate::protocol::dnsheader::DnsHeader; 5 | use crate::protocol::dnsquestion::DnsQuestion; 6 | use crate::protocol::dnsrecord::DnsRecord; 7 | use crate::protocol::querytype::QueryType; 8 | 9 | #[derive(Clone, Debug)] 10 | pub struct DnsPacket { 11 | pub header: DnsHeader, 12 | pub questions: Vec, 13 | pub answers: Vec, 14 | pub authorities: Vec, 15 | pub resources: Vec, 16 | } 17 | 18 | impl Default for DnsPacket { 19 | fn default() -> Self { 20 | DnsPacket::new() 21 | } 22 | } 23 | 24 | impl DnsPacket { 25 | pub fn new() -> DnsPacket { 26 | DnsPacket { 27 | header: DnsHeader::new(), 28 | questions: Vec::new(), 29 | answers: Vec::new(), 30 | authorities: Vec::new(), 31 | resources: Vec::new(), 32 | } 33 | } 34 | 35 | pub fn from_buffer(buffer: &mut BytePacketBuffer) -> Result> { 36 | let mut result = DnsPacket::new(); 37 | result.header.read(buffer)?; 38 | 39 | for _ in 0..result.header.questions { 40 | let mut question = DnsQuestion::new("".to_string(), QueryType::Unknown(0)); 41 | question.read(buffer)?; 42 | result.questions.push(question); 43 | } 44 | 45 | for _ in 0..result.header.answers { 46 | let rec = DnsRecord::read(buffer)?; 47 | result.answers.push(rec); 48 | } 49 | for _ in 0..result.header.authoritative_entries { 50 | let rec = DnsRecord::read(buffer)?; 51 | result.authorities.push(rec); 52 | } 53 | for _ in 0..result.header.resource_entries { 54 | let rec = DnsRecord::read(buffer)?; 55 | result.resources.push(rec); 56 | } 57 | 58 | Ok(result) 59 | } 60 | pub fn write(&mut self, buffer: &mut BytePacketBuffer) -> Result<() ,Box> { 61 | self.header.questions = self.questions.len() as u16; 62 | self.header.answers = self.answers.len() as u16; 63 | self.header.authoritative_entries = self.authorities.len() as u16; 64 | self.header.resource_entries = self.resources.len() as u16; 65 | 66 | self.header.write(buffer)?; 67 | 68 | for question in &self.questions { 69 | question.write(buffer)?; 70 | } 71 | for rec in &self.answers { 72 | rec.write(buffer)?; 73 | } 74 | for rec in &self.authorities { 75 | rec.write(buffer)?; 76 | } 77 | for rec in &self.resources { 78 | rec.write(buffer)?; 79 | } 80 | 81 | Ok(()) 82 | } 83 | 84 | //It is useful to be able to pick a random A record from a packet, 85 | //since when we get multiple nIP's for a single name, it doesn't matter which one we use. 86 | //Isliye random A record pick karne ke liye ye function banaya hai. 87 | pub fn get_random_a(&self) -> Option { 88 | self.answers.iter() 89 | .filter_map(|record| match record { 90 | DnsRecord::A {addr, ..} => Some(*addr), 91 | _ => None, 92 | }) 93 | .next() 94 | } 95 | 96 | //A helper function which returns an iterator over all name servers 97 | //in the authorities section, represented as (domain , host) tuples. 98 | pub fn get_ns<'a>(&'a self , qname : &'a str) -> impl Iterator { 99 | self.authorities.iter() 100 | //In practice, these are always NS records in well formed packages. 101 | //Convert the NS records to a tuple which has only the data we need 102 | //to make it easy to work with 103 | .filter_map(|record| match record { 104 | DnsRecord::NS { domain, host, .. } => Some((domain.as_str(), host.as_str())), 105 | _ => None, 106 | }) 107 | //Filter out the records which are not for the domain we are looking for 108 | .filter(move |(domain , _)| qname.ends_with(*domain)) 109 | } 110 | //We will use the fact that name servers often bundle the corresponding A records 111 | //When repluing to an NS query to implement a function that returns the actual IP 112 | //for an NS record if possible. 113 | pub fn get_resolved_ns (&self , qname : &str) -> Option { 114 | //Get an iterator over the nameservers in the authorities section 115 | self.get_ns(qname) 116 | //Now we need to look for a matching A record in the additional section. 117 | //Scince we just want the first one, we can just build a stream of matching records. 118 | .flat_map(|(_, host)| { 119 | self.resources.iter() 120 | // Filter for A records where the domain match the host 121 | // of the NS record that we are currently processing 122 | .filter_map(move |record| match record { 123 | DnsRecord::A { domain , addr , ..} if domain == host => Some(*addr), 124 | _ => None, 125 | }) 126 | }) 127 | //Finally pick the first valid entry 128 | .next() 129 | } 130 | /// However, not all name servers are as that nice. In certain cases there won't 131 | /// be any A records in the additional section, and we'll have to perform *another* 132 | /// lookup in the midst. For this, we introduce a method for returning the host 133 | /// name of an appropriate name server. 134 | pub fn get_unresolved_ns<'a>(&'a self, qname: &'a str) -> Option<&'a str> { 135 | // Get an iterator over the nameservers in the authorities section 136 | self.get_ns(qname) 137 | .map(|(_, host)| host) 138 | // Finally, pick the first valid entry 139 | .next() 140 | } 141 | } -------------------------------------------------------------------------------- /CONTRIBUTING.MD: -------------------------------------------------------------------------------- 1 | # Contributing to DNS-Server-Rust 2 | 3 | Thank you for considering contributing to the **DNS-Server-Rust** project! 🎉 This document provides guidelines to help you successfully contribute, whether you’re fixing bugs, improving documentation, or adding features. 4 | 5 | --- 6 | 7 | ## Table of Contents 8 | 1. [How Can I Contribute?](#how-can-i-contribute) 9 | - [Reporting Bugs](#reporting-bugs) 10 | - [Suggesting Features](#suggesting-features) 11 | - [Improving Documentation](#improving-documentation) 12 | - [Contributing Code](#contributing-code) 13 | 2. [Project Structure](#project-structure) 14 | 3. [Code Guidelines](#code-guidelines) 15 | 4. [Pull Request Process](#pull-request-process) 16 | 5. [Testing](#testing) 17 | 6. [Getting Started](#getting-started) 18 | 7. [Code of Conduct](#code-of-conduct) 19 | 20 | --- 21 | 22 | ## How Can I Contribute? 23 | 24 | ### Reporting Bugs 25 | 26 | If you encounter a bug or unexpected behavior: 27 | 1. Check if it has already been reported in [Issues](https://github.com/SkySingh04/DNS-Server-Rust/issues). 28 | 2. If not, open a new issue with the following details: 29 | - A clear and descriptive title. 30 | - Steps to reproduce the problem. 31 | - The version of Rust and operating system. 32 | - Any error messages or relevant logs. 33 | - Screenshots (if applicable). 34 | 35 | --- 36 | 37 | ### Suggesting Features 38 | 39 | We’re always open to suggestions! If you have an idea: 40 | 1. Check the [Issues](https://github.com/SkySingh04/DNS-Server-Rust/issues) to see if the feature has already been requested. 41 | 2. Open a new issue using the "Feature Request" template: 42 | - Describe the feature clearly. 43 | - Explain the use case or problem it solves. 44 | - Include any references or links that might help. 45 | 46 | --- 47 | 48 | ### Improving Documentation 49 | 50 | Documentation is key for a successful project. If you notice any gaps: 51 | - Update existing README or inline comments. 52 | - Add examples or clarify confusing areas. 53 | - Submit a Pull Request for documentation changes. 54 | 55 | --- 56 | 57 | ### Contributing Code 58 | 59 | Follow these steps to contribute code: 60 | 1. **Fork the Repository** 61 | Go to the project repository and click the **Fork** button. 62 | 63 | 2. **Clone the Repository** 64 | Clone your forked repository: 65 | ```bash 66 | git clone https://github.com/SkySingh04/DNS-Server-Rust/DNS-Server-Rust.git 67 | cd DNS-Server-Rust 68 | ``` 69 | 70 | 3. **Create a Branch** 71 | Use a descriptive branch name: 72 | ```bash 73 | git checkout -b feature/my-new-feature 74 | ``` 75 | 76 | 4. **Implement Your Changes** 77 | Follow the [Code Guidelines](#code-guidelines). 78 | 79 | 5. **Test Your Code** 80 | Ensure your changes do not break existing functionality: 81 | ```bash 82 | cargo test 83 | ``` 84 | 85 | 6. **Commit Your Changes** 86 | Write meaningful commit messages: 87 | ```bash 88 | git add . 89 | git commit -m "Add support for MX record type" 90 | ``` 91 | 92 | 7. **Push and Submit a PR** 93 | Push your changes: 94 | ```bash 95 | git push origin feature/my-new-feature 96 | ``` 97 | Then create a Pull Request on GitHub. 98 | 99 | --- 100 | 101 | ## Project Structure 102 | 103 | Here’s an overview of the key files and directories: 104 | 105 | ``` 106 | DNS-Server-Rust/ 107 | │ 108 | ├── src/ 109 | │ ├── main.rs # Entry point of the application 110 | │ ├── protocol/ 111 | │ │ ├── dnsheader.rs # DNS Header implementation 112 | │ │ ├── dnsquestion.rs # DNS Question section 113 | │ │ ├── dnsrecord.rs # DNS Record section 114 | │ │ ├── dnspacket.rs # DNS Packet handling 115 | │ │ ├── byte_packet_buffer.rs # Reads/writes DNS packets 116 | │ │ ├── querytype.rs # Query type implementation 117 | │ │ ├── resultcode.rs # DNS response codes 118 | │ │ 119 | ├── response_packet.txt # Sample DNS response packet 120 | ├── Cargo.toml # Rust dependencies 121 | ├── README.md # Project overview and notes 122 | └── CONTRIBUTING.md # Contribution guidelines 123 | ``` 124 | 125 | --- 126 | 127 | ## Code Guidelines 128 | 129 | Follow these coding standards for consistency: 130 | 1. **Formatting**: Use `rustfmt` for consistent formatting. 131 | ```bash 132 | rustfmt src/*.rs 133 | ``` 134 | 135 | 2. **Linting**: Ensure clean code using `clippy`. 136 | ```bash 137 | cargo clippy 138 | ``` 139 | 140 | 3. **Error Handling**: Use `Result` and proper error handling instead of panics. 141 | 142 | 4. **Testing**: Write unit tests for all new features and edge cases. 143 | 144 | 5. **Naming**: Use snake_case for variables and functions, and PascalCase for types. 145 | 146 | 6. **Documentation**: Add comments and Rust docstrings (`///`) for public modules and functions. 147 | 148 | --- 149 | 150 | ## Pull Request Process 151 | 152 | 1. Ensure your PR targets the **main** branch. 153 | 2. Include a concise description of the changes. 154 | 3. Reference any related issues (e.g., `Fixes #123`). 155 | 4. Ensure the code compiles without errors or warnings. 156 | 5. Write or update tests, if applicable. 157 | 6. Add documentation if needed. 158 | 159 | Once submitted, maintainers will review your PR. Be open to feedback and iterate as necessary. 160 | 161 | --- 162 | 163 | ## Testing 164 | 165 | Run tests locally using Cargo: 166 | ```bash 167 | cargo test 168 | ``` 169 | 170 | For any new feature, write tests in the corresponding file under `src/protocol/`. 171 | 172 | --- 173 | 174 | ## Getting Started 175 | 176 | To set up the project locally: 177 | 1. Ensure you have Rust installed. If not, install it using [rustup](https://rustup.rs/). 178 | 2. Clone the repository: 179 | ```bash 180 | git clone https://github.com/SkySingh04/DNS-Server-Rust/DNS-Server-Rust.git 181 | cd DNS-Server-Rust 182 | ``` 183 | 3. Build and run the project: 184 | ```bash 185 | cargo run 186 | ``` 187 | 188 | 4. Test the DNS server using `dig`: 189 | ```bash 190 | dig @127.0.0.1 -p 2053 google.com 191 | ``` 192 | 193 | --- 194 | 195 | ## Code of Conduct 196 | 197 | This project adheres to the [Contributor Covenant](https://www.contributor-covenant.org/). Please be respectful to others, whether you’re contributing code, opening an issue, or participating in discussions. 198 | 199 | --- 200 | 201 | Thank you for contributing to **DNS-Server-Rust**! Let’s build something awesome together. 🚀 202 | -------------------------------------------------------------------------------- /src/protocol/byte_packet_buffer.rs: -------------------------------------------------------------------------------- 1 | pub struct BytePacketBuffer { 2 | pub buf: [u8; 512], 3 | pub pos: usize, // Current position in the buffer 4 | } 5 | 6 | impl Default for BytePacketBuffer { 7 | fn default() -> Self { 8 | BytePacketBuffer::new() 9 | } 10 | } 11 | 12 | impl BytePacketBuffer { 13 | // Initialize a new buffer 14 | pub fn new() -> BytePacketBuffer { 15 | BytePacketBuffer { 16 | buf: [0; 512], 17 | pos: 0, 18 | } 19 | } 20 | 21 | // Get the current position 22 | pub fn pos(&self) -> usize { 23 | self.pos 24 | } 25 | 26 | // Advance the buffer position by a specific number of steps 27 | pub fn step(&mut self, steps: usize) -> Result<(), Box> { 28 | self.pos += steps; 29 | if self.pos >= 512 { 30 | return Err("End of buffer".into()); 31 | } 32 | Ok(()) 33 | } 34 | 35 | // Change the buffer position 36 | pub fn seek(&mut self, pos: usize) -> Result<(), Box> { 37 | self.pos = pos; 38 | if self.pos >= 512 { 39 | return Err("End of buffer".into()); 40 | } 41 | Ok(()) 42 | } 43 | 44 | // Read a single byte from the buffer and advance the position 45 | pub fn read(&mut self) -> Result> { 46 | if self.pos >= 512 { 47 | return Err("End of buffer".into()); 48 | } 49 | let res = self.buf[self.pos]; 50 | self.pos += 1; 51 | Ok(res) 52 | } 53 | 54 | // Get a single byte without changing the buffer position 55 | pub fn get(&self, pos: usize) -> Result> { 56 | if pos >= 512 { 57 | return Err("End of buffer".into()); 58 | } 59 | Ok(self.buf[pos]) 60 | } 61 | 62 | // Get a range of bytes from the buffer 63 | pub fn get_range(&self, start: usize, len: usize) -> Result<&[u8], Box> { 64 | if start + len > 512 { 65 | return Err("End of buffer".into()); 66 | } 67 | Ok(&self.buf[start..start + len]) 68 | } 69 | 70 | // Read two bytes and interpret as a u16 in network byte order 71 | pub fn read_u16(&mut self) -> Result> { 72 | let res = u16::from(self.read()?) << 8 | u16::from(self.read()?); 73 | Ok(res) 74 | } 75 | 76 | // Read four bytes and interpret as a u32 in network byte order 77 | pub fn read_u32(&mut self) -> Result> { 78 | let res = u32::from(self.read()?) << 24 79 | | u32::from(self.read()?) << 16 80 | | u32::from(self.read()?) << 8 81 | | u32::from(self.read()?); 82 | Ok(res) 83 | } 84 | 85 | // Read a domain name from the buffer 86 | pub fn read_qname(&mut self, outstr: &mut String) -> Result<(), Box> { 87 | let mut pos = self.pos; 88 | let mut jumped = false; 89 | let max_jumps = 5; 90 | let mut jumps = 0; 91 | let mut delim = ""; 92 | 93 | loop { 94 | if jumps > max_jumps { 95 | return Err("Limit of jumps exceeded! Possible loop detected.".into()); 96 | } 97 | 98 | let len = self.get(pos)?; 99 | if (len & 0xC0) == 0xC0 { 100 | if !jumped { 101 | self.seek(pos + 2)?; 102 | } 103 | let b2 = self.get(pos + 1)? as u16; 104 | let offset = (((len as u16) ^ 0xC0) << 8) | b2; 105 | pos = offset as usize; 106 | jumped = true; 107 | jumps += 1; 108 | continue; 109 | } else { 110 | pos += 1; 111 | if len == 0 { 112 | break; 113 | } 114 | outstr.push_str(delim); 115 | let str_buffer = self.get_range(pos, len as usize)?; 116 | outstr.push_str(&String::from_utf8_lossy(str_buffer).to_lowercase()); 117 | delim = "."; 118 | pos += len as usize; 119 | } 120 | } 121 | if !jumped { 122 | self.seek(pos)?; 123 | } 124 | Ok(()) 125 | } 126 | 127 | // Write a single byte to the buffer and advance the position 128 | pub fn write(&mut self, val: u8) -> Result<(), Box> { 129 | if self.pos >= 512 { 130 | return Err("End of buffer".into()); 131 | } 132 | self.buf[self.pos] = val; 133 | self.pos += 1; 134 | Ok(()) 135 | } 136 | 137 | // Write a u8 to the buffer 138 | pub fn write_u8(&mut self, val: u8) -> Result<(), Box> { 139 | self.write(val)?; 140 | Ok(()) 141 | } 142 | 143 | // Write a u16 to the buffer in network byte order 144 | pub fn write_u16(&mut self, val: u16) -> Result<(), Box> { 145 | self.write(((val >> 8) & 0xFF) as u8)?; 146 | self.write((val & 0xFF) as u8)?; 147 | Ok(()) 148 | } 149 | 150 | // Write a u32 to the buffer in network byte order 151 | pub fn write_u32(&mut self, val: u32) -> Result<(), Box> { 152 | self.write(((val >> 24) & 0xFF) as u8)?; 153 | self.write(((val >> 16) & 0xFF) as u8)?; 154 | self.write(((val >> 8) & 0xFF) as u8)?; 155 | self.write((val & 0xFF) as u8)?; 156 | Ok(()) 157 | } 158 | 159 | // Write a qname to the buffer 160 | pub fn write_qname(&mut self, qname: &str) -> Result<(), Box> { 161 | for part in qname.split('.') { 162 | if part.len() > 63 { 163 | return Err("Label too long".into()); 164 | } 165 | self.write(part.len() as u8)?; 166 | for c in part.chars() { 167 | self.write(c as u8)?; 168 | } 169 | } 170 | self.write(0)?; 171 | Ok(()) 172 | } 173 | 174 | pub fn set(&mut self, pos: usize, val: u8) -> Result<(),Box> { 175 | self.buf[pos] = val; 176 | 177 | Ok(()) 178 | } 179 | 180 | pub fn set_u16(&mut self, pos: usize, val: u16) -> Result<(),Box> { 181 | self.set(pos, (val >> 8) as u8)?; 182 | self.set(pos + 1, (val & 0xFF) as u8)?; 183 | 184 | Ok(()) 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | // use std::fs::File; 2 | // use std::io::Read; 3 | use std::net::UdpSocket; 4 | // use std::result; 5 | 6 | mod protocol; 7 | 8 | use protocol::byte_packet_buffer::BytePacketBuffer; 9 | use protocol::dnspacket::DnsPacket; 10 | use protocol::querytype::QueryType; 11 | use protocol::dnsquestion::DnsQuestion; 12 | use protocol::resultcode::ResultCode; 13 | use std::net::Ipv4Addr; 14 | 15 | fn lookup(qname : &str , qtype: QueryType , server : (Ipv4Addr , u16)) -> Result> { 16 | //Forward queries to Google's public DNS server 17 | // let server = ("8.8.8.8",53); 18 | 19 | let socket = UdpSocket::bind(("0.0.0.0" , 43210))?; 20 | 21 | let mut packet = DnsPacket::new(); 22 | 23 | packet.header.id = 6666; 24 | packet.header.questions =1; 25 | packet.header.recursion_desired = true; 26 | 27 | packet.questions.push(DnsQuestion::new(qname.to_string(), qtype)); 28 | 29 | let mut req_buffer = BytePacketBuffer::new(); 30 | let _ = packet.write(&mut req_buffer); 31 | socket.send_to(&req_buffer.buf[0..req_buffer.pos], server)?; 32 | 33 | let mut res_buffer = BytePacketBuffer::new(); 34 | socket.recv_from(&mut res_buffer.buf)?; 35 | 36 | DnsPacket::from_buffer(&mut res_buffer) 37 | } 38 | 39 | //Handle a single incoming packet with this 40 | fn handle_query(socket : &UdpSocket) -> Result<() , Box> { 41 | //With a socket ready, we can go ahead and read a packet. 42 | //This will block until one is received 43 | 44 | let mut req_buffer = BytePacketBuffer::new(); 45 | 46 | //The `recv_from` function will write the data into the provided buffer, 47 | //And return the length of the data read as well as the source address. 48 | //We are essentially not interested in length , but we need to keep track of 49 | //the source in order to send our reply later on. 50 | 51 | let (_ , src) = socket.recv_from(&mut req_buffer.buf)?; 52 | 53 | //Next, we'll parse the packet into a `DnsPacket` struct 54 | let mut request = DnsPacket::from_buffer(&mut req_buffer)?; 55 | 56 | //We'll create a new packet to hold our response 57 | let mut packet = DnsPacket::new(); 58 | 59 | packet.header.id = request.header.id; 60 | packet.header.response = true; 61 | packet.header.recursion_available = true; 62 | packet.header.recursion_desired = true; 63 | 64 | //In the normal case, exactly one question is present 65 | if let Some(question) = request.questions.pop() { 66 | println!("Received query: {:?}", question); 67 | 68 | //Since all is set up and as expectrd, the query can be forwarded to the 69 | //target server. There's always the possibility that the query will 70 | //fail, in which case the `SERVFAIL` response code is set to indicate as much to the client. 71 | //If rather everything goes and planned, the question and response records as copied into our response packet. 72 | 73 | // if let Ok(result) = lookup(&question.name, question.qtype) { 74 | // packet.questions.push(question); 75 | // packet.header.rescode = result.header.rescode; 76 | if let Ok(result) = recursive_lookup(&question.name, question.qtype) { 77 | packet.questions.push(question.clone()); 78 | packet.header.rescode = result.header.rescode; 79 | 80 | 81 | for rec in result.answers { 82 | println!("Answer: {:?}", rec); 83 | packet.answers.push(rec); 84 | } 85 | 86 | for rec in result.authorities { 87 | println!("Authority: {:?}", rec); 88 | packet.authorities.push(rec); 89 | } 90 | 91 | for rec in result.resources { 92 | println!("Resource: {:?}", rec); 93 | packet.resources.push(rec); 94 | } 95 | } 96 | else{ 97 | packet.header.rescode = ResultCode::ServFail; 98 | } 99 | } 100 | //We need to make sure that a question is actually present in the packet 101 | //If not , we'll set the response code to `FORMERR` and return an error 102 | else{ 103 | packet.header.rescode = ResultCode::FormErr; 104 | } 105 | 106 | //Now we can just encode our response and send it back to the client 107 | let mut res_buffer = BytePacketBuffer::new(); 108 | 109 | let _ = packet.write(&mut res_buffer); 110 | 111 | let len = res_buffer.pos(); 112 | let data = res_buffer.get_range(0, len)?; 113 | 114 | socket.send_to(data, src)?; 115 | 116 | Ok(()) 117 | } 118 | 119 | //Implementing recursive lookup 120 | pub fn recursive_lookup(qname : &str, qtype : QueryType) -> Result > { 121 | //for now we're always starting with *a.root-servers.net* 122 | let mut ns = "198.41.0.4".parse::().unwrap(); 123 | 124 | //Since it might take an arbitrary number of queries to get to the final answer, 125 | //We start the loop 126 | loop { 127 | println!("Attempting lookup of {:?} {} with ns {}", qtype, qname, ns); 128 | 129 | //The next step is to send the query to the active server 130 | let ns_copy = ns; 131 | 132 | let server = (ns_copy, 53); 133 | let response = lookup(qname , qtype , server)?; 134 | 135 | //If there are entries in the answer section, we can return the packet 136 | if !response.answers.is_empty() && response.header.rescode == ResultCode::NoError { 137 | return Ok(response); 138 | } 139 | 140 | // We might also get a `NXDOMAIN` reply, which is the authoritative name servers 141 | // way of telling us that the name doesn't exist. 142 | if response.header.rescode == ResultCode::NXDomain { 143 | return Ok(response); 144 | } 145 | 146 | // Otherwise, we'll try to find a new nameserver based on NS and a corresponding A 147 | // record in the additional section. If this succeeds, we can switch name server 148 | // and retry the loop. 149 | if let Some(new_ns) = response.get_resolved_ns(qname) { 150 | ns = new_ns; 151 | 152 | continue; 153 | } 154 | // If not, we'll have to resolve the ip of a NS record. If no NS records exist, 155 | // we'll go with what the last server told us. 156 | let new_ns_name = match response.get_unresolved_ns(qname) { 157 | Some(x) => x, 158 | None => return Ok(response), 159 | }; 160 | 161 | // Here we go down the rabbit hole by starting _another_ lookup sequence in the 162 | // midst of our current one. Hopefully, this will give us the IP of an appropriate 163 | // name server. 164 | let recursive_response = recursive_lookup(new_ns_name, QueryType::A)?; 165 | 166 | // Finally, we pick a random ip from the result, and restart the loop. If no such 167 | // record is available, we again return the last result we got. 168 | if let Some(new_ns) = recursive_response.get_random_a() { 169 | ns = new_ns; 170 | } else { 171 | return Ok(response); 172 | } 173 | } 174 | } 175 | 176 | 177 | 178 | fn main() -> Result<(), Box> { 179 | //Bind an UDP socket on port 2053 180 | let socket = UdpSocket::bind(("0.0.0.0" , 2053))?; 181 | 182 | println!("Server started successfully on port 2053"); 183 | //For now, queries area handled sequentially, so an infinite loop for requests is initiated 184 | loop { 185 | match handle_query(&socket) { 186 | Ok(_) =>{}, 187 | Err(e) =>eprintln!("An error occured : {}",e), 188 | } 189 | } 190 | } -------------------------------------------------------------------------------- /src/protocol/dnsrecord.rs: -------------------------------------------------------------------------------- 1 | use crate::protocol::byte_packet_buffer::BytePacketBuffer; 2 | use crate::protocol::querytype::QueryType; 3 | use std::net::Ipv4Addr; 4 | use std::net::Ipv6Addr; 5 | 6 | #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] 7 | #[allow(dead_code)] 8 | pub enum DnsRecord { 9 | Unknown { 10 | domain: String, 11 | qtype: u16, 12 | data_len: u16, 13 | ttl: u32, 14 | }, // 0 15 | A { 16 | domain: String, 17 | addr: Ipv4Addr, 18 | ttl: u32, 19 | }, // 1 20 | NS { 21 | domain: String, 22 | host: String, 23 | ttl: u32, 24 | }, // 2 25 | Cname { 26 | domain: String, 27 | host: String, 28 | ttl: u32, 29 | }, // 5 30 | Soa { 31 | domain: String, 32 | mname: String, 33 | rname: String, 34 | serial: u32, 35 | refresh: u32, 36 | retry: u32, 37 | expire: u32, 38 | minimum: u32, 39 | ttl: u32, 40 | }, // 6 41 | MX { 42 | domain: String, 43 | priority: u16, 44 | host: String, 45 | ttl: u32, 46 | }, // 15 47 | Txt { 48 | domain: String, 49 | text: String, 50 | ttl: u32, 51 | }, // 16 52 | Aaaa { 53 | domain: String, 54 | addr: Ipv6Addr, 55 | ttl: u32, 56 | }, // 28 57 | } 58 | 59 | impl DnsRecord { 60 | pub fn read(buffer: &mut BytePacketBuffer) -> Result> { 61 | let mut domain = String::new(); 62 | buffer.read_qname(&mut domain)?; 63 | 64 | let qtype_num = buffer.read_u16()?; 65 | let qtype = QueryType::from_num(qtype_num); 66 | let _ = buffer.read_u16()?; 67 | let ttl = buffer.read_u32()?; 68 | let data_len = buffer.read_u16()?; 69 | 70 | match qtype { 71 | QueryType::A => { 72 | let raw_addr = buffer.read_u32()?; 73 | let addr = Ipv4Addr::new( 74 | ((raw_addr >> 24) & 0xFF) as u8, 75 | ((raw_addr >> 16) & 0xFF) as u8, 76 | ((raw_addr >> 8) & 0xFF) as u8, 77 | ((raw_addr) & 0xFF) as u8, 78 | ); 79 | 80 | Ok(DnsRecord::A { 81 | domain, 82 | addr, 83 | ttl, 84 | }) 85 | } 86 | 87 | QueryType::Aaaa => { 88 | let raw_addr1 = buffer.read_u32()?; 89 | let raw_addr2 = buffer.read_u32()?; 90 | let raw_addr3 = buffer.read_u32()?; 91 | let raw_addr4 = buffer.read_u32()?; 92 | let addr = Ipv6Addr::new( 93 | ((raw_addr1 >> 16) & 0xFFFF) as u16, 94 | (raw_addr1 & 0xFFFF) as u16, 95 | ((raw_addr2 >> 16) & 0xFFFF) as u16, 96 | ((raw_addr2) & 0xFFFF) as u16, 97 | ((raw_addr3 >> 16) & 0xFFFF) as u16, 98 | ((raw_addr3) & 0xFFFF) as u16, 99 | ((raw_addr4 >> 16) & 0xFFFF) as u16, 100 | ((raw_addr4) & 0xFFFF) as u16, 101 | ); 102 | 103 | Ok(DnsRecord::Aaaa { 104 | domain, 105 | addr, 106 | ttl, 107 | }) 108 | } 109 | 110 | QueryType::NS => { 111 | let mut ns = String::new(); 112 | buffer.read_qname(&mut ns)?; 113 | 114 | Ok(DnsRecord::NS { 115 | domain, 116 | host: ns, 117 | ttl, 118 | }) 119 | } 120 | 121 | QueryType::Cname => { 122 | let mut cname = String::new(); 123 | buffer.read_qname(&mut cname)?; 124 | 125 | Ok(DnsRecord::Cname { 126 | domain, 127 | host: cname, 128 | ttl, 129 | }) 130 | } 131 | 132 | QueryType::Soa => { 133 | let mut mname = String::new(); 134 | buffer.read_qname(&mut mname)?; 135 | 136 | let mut rname = String::new(); 137 | buffer.read_qname(&mut rname)?; 138 | 139 | let serial = buffer.read_u32()?; 140 | let refresh = buffer.read_u32()?; 141 | let retry = buffer.read_u32()?; 142 | let expire = buffer.read_u32()?; 143 | let minimum = buffer.read_u32()?; 144 | 145 | Ok(DnsRecord::Soa { 146 | domain, 147 | mname, 148 | rname, 149 | serial, 150 | refresh, 151 | retry, 152 | expire, 153 | minimum, 154 | ttl, 155 | }) 156 | } 157 | 158 | QueryType::MX => { 159 | let priority = buffer.read_u16()?; 160 | 161 | let mut mx = String::new(); 162 | buffer.read_qname(&mut mx)?; 163 | 164 | Ok(DnsRecord::MX { 165 | domain, 166 | priority, 167 | host: mx, 168 | ttl, 169 | }) 170 | } 171 | 172 | QueryType::Txt => { 173 | let mut txt = String::new(); 174 | buffer.read_qname(&mut txt)?; 175 | 176 | Ok(DnsRecord::Txt { 177 | domain, 178 | text: txt, 179 | ttl, 180 | }) 181 | } 182 | 183 | QueryType::Unknown(_) => { 184 | buffer.step(data_len as usize)?; 185 | 186 | Ok(DnsRecord::Unknown { 187 | domain, 188 | qtype: qtype_num, 189 | data_len, 190 | ttl, 191 | }) 192 | } 193 | } 194 | } 195 | pub fn write(&self, buffer: &mut BytePacketBuffer) -> Result> { 196 | let start_pos = buffer.pos(); 197 | 198 | match *self { 199 | DnsRecord::A { 200 | ref domain, 201 | ref addr, 202 | ttl, 203 | } => { 204 | buffer.write_qname(domain)?; 205 | buffer.write_u16(QueryType::A.to_num())?; 206 | buffer.write_u16(1)?; 207 | buffer.write_u32(ttl)?; 208 | buffer.write_u16(4)?; 209 | 210 | let octets = addr.octets(); 211 | buffer.write_u8(octets[0])?; 212 | buffer.write_u8(octets[1])?; 213 | buffer.write_u8(octets[2])?; 214 | buffer.write_u8(octets[3])?; 215 | } 216 | DnsRecord::Unknown { .. } => { 217 | println!("Skipping record: {:?}", self); 218 | } 219 | DnsRecord::NS { 220 | ref domain, 221 | ref host, 222 | ttl, 223 | } => { 224 | buffer.write_qname(domain)?; 225 | buffer.write_u16(QueryType::NS.to_num())?; 226 | buffer.write_u16(1)?; 227 | buffer.write_u32(ttl)?; 228 | 229 | let pos = buffer.pos(); 230 | buffer.write_u16(0)?; 231 | 232 | buffer.write_qname(host)?; 233 | 234 | let size = buffer.pos() - (pos + 2); 235 | buffer.set_u16(pos, size as u16)?; 236 | } 237 | DnsRecord::Cname { 238 | ref domain, 239 | ref host, 240 | ttl, 241 | } => { 242 | buffer.write_qname(domain)?; 243 | buffer.write_u16(QueryType::Cname.to_num())?; 244 | buffer.write_u16(1)?; 245 | buffer.write_u32(ttl)?; 246 | 247 | let pos = buffer.pos(); 248 | buffer.write_u16(0)?; 249 | 250 | buffer.write_qname(host)?; 251 | 252 | let size = buffer.pos() - (pos + 2); 253 | buffer.set_u16(pos, size as u16)?; 254 | } 255 | DnsRecord::Soa { 256 | ref domain, 257 | ref mname, 258 | ref rname, 259 | serial, 260 | refresh, 261 | retry, 262 | expire, 263 | minimum, 264 | ttl, 265 | } => { 266 | buffer.write_qname(domain)?; 267 | buffer.write_u16(QueryType::Soa.to_num())?; 268 | buffer.write_u16(1)?; 269 | buffer.write_u32(ttl)?; 270 | 271 | let pos = buffer.pos(); 272 | buffer.write_u16(0)?; 273 | 274 | buffer.write_qname(mname)?; 275 | buffer.write_qname(rname)?; 276 | buffer.write_u32(serial)?; 277 | buffer.write_u32(refresh)?; 278 | buffer.write_u32(retry)?; 279 | buffer.write_u32(expire)?; 280 | buffer.write_u32(minimum)?; 281 | 282 | let size = buffer.pos() - (pos + 2); 283 | buffer.set_u16(pos, size as u16)?; 284 | } 285 | DnsRecord::MX { 286 | ref domain, 287 | priority, 288 | ref host, 289 | ttl, 290 | } => { 291 | buffer.write_qname(domain)?; 292 | buffer.write_u16(QueryType::MX.to_num())?; 293 | buffer.write_u16(1)?; 294 | buffer.write_u32(ttl)?; 295 | 296 | let pos = buffer.pos(); 297 | buffer.write_u16(0)?; 298 | 299 | buffer.write_u16(priority)?; 300 | buffer.write_qname(host)?; 301 | 302 | let size = buffer.pos() - (pos + 2); 303 | buffer.set_u16(pos, size as u16)?; 304 | } 305 | DnsRecord::Txt { 306 | ref domain, 307 | ref text, 308 | ttl, 309 | } => { 310 | buffer.write_qname(domain)?; 311 | buffer.write_u16(QueryType::Txt.to_num())?; 312 | buffer.write_u16(1)?; 313 | buffer.write_u32(ttl)?; 314 | 315 | let pos = buffer.pos(); 316 | buffer.write_u16(0)?; 317 | 318 | buffer.write_qname(text)?; 319 | 320 | let size = buffer.pos() - (pos + 2); 321 | buffer.set_u16(pos, size as u16)?; 322 | } 323 | DnsRecord::Aaaa { 324 | ref domain, 325 | ref addr, 326 | ttl, 327 | } => { 328 | buffer.write_qname(domain)?; 329 | buffer.write_u16(QueryType::Aaaa.to_num())?; 330 | buffer.write_u16(1)?; 331 | buffer.write_u32(ttl)?; 332 | buffer.write_u16(16)?; 333 | 334 | let segments = addr.segments(); 335 | buffer.write_u16(segments[0])?; 336 | buffer.write_u16(segments[1])?; 337 | buffer.write_u16(segments[2])?; 338 | buffer.write_u16(segments[3])?; 339 | buffer.write_u16(segments[4])?; 340 | buffer.write_u16(segments[5])?; 341 | buffer.write_u16(segments[6])?; 342 | buffer.write_u16(segments[7])?; 343 | } 344 | } 345 | Ok(buffer.pos() - start_pos) 346 | } 347 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A DNS Server : Built with RUST! 2 | Building a DNS Server from Scratch in RUST!(yes I consider this language to be the language of the cool kids and yes I struggle to write a simple function in it and yes I want to earn money without having to do full stack). 3 | 4 | I have written notes about all the things I have learned in this Readme file, they are written in Hinglish for my reference. 5 | 6 | 7 | ## Part 1 : Implementing the protocol 8 | 9 | DNS Packets ko bhejte h over UDP transport and limited to 512 bytes(Wo alag baat h there is exception where it can be sent over TCP as well and eDNS se packet size badha sakte h). 10 | 11 | DNS uses the same format in queries and responses. Mainly a DNS packet consists of: 12 | - Header : Isme hoga information about the query/response 13 | - Question Section : List of questions(hota ek hi h in practice) , each indicating the query name(domain) and the record type of interest 14 | - Answer Section : List of relevant records of the requested type 15 | - Authority Section : A list of name servers used for resolving queries recursively. 16 | - Additional Section : Additional useful info. 17 | 18 | Essentially 3 different objects ko support karna hoga: 19 | - Header : [dnsheader.rs](src/protocol/dnsheader.rs) mein implement kar diye : Iske liye we created another implementaion for the rescode field also in [resultcode.rs](src/protocol/resultcode.rs). RCode is set by the server to indicate the status of the response, i.e. whether or not it was successful or failed, and agar fail hua toh providing details about the cause of the failure. 20 | - Question : [dnsquestion.rs](src/protocol/dnsquestion.rs) : Iske liye we created another implementation of [querytype](src/protocol/querytype.rs), so that we can represent the *record type* being queried. 21 | - Record : [dnsrecord.rs](src/protocol/dnsrecord.rs) is used to represent the actual dns records and allow us to add new records later on easily. 22 | 23 | 24 | [byte_packet_buffer.rs](src/protocol/byte_packet_buffer.rs) asli problematic kaam karta h. The thing is DNS encodes each name into a sequence of labels, with each label prepended by a single byte indicating its length. Example would be *[3]www[6]google[3]com[0]*. Ye phir bhi theek h, but it gets even more problematic when jumps come into place. 25 | 26 | > Due to the original size constraints of DNS, of 512 bytes for a single packet, some type of compression was needed. Since most of the space required is for the domain names, and part of the same name tends to reoccur, there's some obvious space saving opportunity. 27 | 28 | To save space from reoccuring set of characters, DNS packets include a "jump directive", telling the packet parser to jump to another position, and finish reading the name there. This _jump_ can be read if the length byte has the two most significant bits set,iska matlab jump hai, and we need to follow the pointer. 29 | 30 | Ek aur baat to be taken care of is, this jumps can cause a cycle if some problematic person adds it to the packet, so wo check karna padega. This along with reading of the packets is all implemented in the byte_packet_buffer. 31 | 32 | Bas phir, we can put together all of this now in our [dnspacket.rs](src/protocol/dnspacket.rs) to finish our protocol implementation. 33 | 34 | To test it out, run the [main.rs](src/protocol/main.rs) file with our `response_packet.txt` 35 | 36 | The output will be : 37 | ``` 38 | cargo run 39 | Compiling DNS-Server-Rust v0.1.0 (/home/akash/Desktop/rust/DNS-Server-Rust) 40 | 41 | warning: `DNS-Server-Rust` (bin "DNS-Server-Rust") generated 8 warnings 42 | Finished dev [unoptimized + debuginfo] target(s) in 0.17s 43 | Running `target/debug/DNS-Server-Rust` 44 | DnsHeader { 45 | id: 16488, 46 | recursion_desired: true, 47 | truncated_message: false, 48 | authoritative_answer: false, 49 | opcode: 0, 50 | response: true, 51 | rescode: NOERROR, 52 | checking_disabled: false, 53 | authed_data: false, 54 | z: false, 55 | recursion_available: true, 56 | questions: 1, 57 | answers: 1, 58 | authoritative_entries: 0, 59 | resource_entries: 0, 60 | } 61 | DnsQuestion { 62 | name: "google.com", 63 | qtype: A, 64 | } 65 | A { 66 | domain: "google.com", 67 | addr: 142.250.183.238, 68 | ttl: 74, 69 | } 70 | ``` 71 | 72 | ## Part 2 : Building a stub resolver 73 | 74 | A stub resolver is a DNS Client that doesn't feature any built-in support for recursive lookup and that will only work with a DNS server that does. 75 | 76 | - We need to extend our [byte_packet_buffer.rs](src/protocol/byte_packet_buffer.rs) to add methods for writing bytes and for writing query names in labeled form. Additionally, we will be extending our Header, Record, Question and Packet structs. 77 | 78 | - Next we can implement a Stub Resolver using the *UDPSocket* included in rust, instead of having to read a packet file. 79 | 80 | The output of running the stub resolver was : 81 | ``` 82 | cargo run 83 | warning: crate `DNS_Server_Rust` should have a snake case name 84 | | 85 | = help: convert the identifier to snake case: `dns_server_rust` 86 | = note: `#[warn(non_snake_case)]` on by default 87 | 88 | warning: `DNS-Server-Rust` (bin "DNS-Server-Rust") generated 1 warning 89 | Finished dev [unoptimized + debuginfo] target(s) in 0.00s 90 | Running `target/debug/DNS-Server-Rust` 91 | DnsHeader { 92 | id: 6666, 93 | recursion_desired: true, 94 | truncated_message: false, 95 | authoritative_answer: false, 96 | opcode: 0, 97 | response: true, 98 | rescode: NOERROR, 99 | checking_disabled: false, 100 | authed_data: false, 101 | z: false, 102 | recursion_available: true, 103 | questions: 1, 104 | answers: 1, 105 | authoritative_entries: 0, 106 | resource_entries: 0, 107 | } 108 | DnsQuestion { 109 | name: "google.com", 110 | qtype: A, 111 | } 112 | A { 113 | domain: "google.com", 114 | addr: 172.217.167.142, 115 | ttl: 80, 116 | } 117 | ``` 118 | 119 | ## Part 3 : Adding More Record types 120 | 121 | Currently we support only A type records. But ofcourse, there are n number of record types (most of them don't see any use) but some important ones are: 122 | 123 | | ID | Name | Description | Encoding | 124 | | --- | ----- | -------------------------------------------------------- | ------------------------------------------------ | 125 | | 1 | A | Alias - Mapping names to IP addresses | Preamble + Four bytes for IPv4 adress | 126 | | 2 | NS | Name Server - The DNS server address for a domain | Preamble + Label Sequence | 127 | | 5 | CNAME | Canonical Name - Maps names to names | Preamble + Label Sequence | 128 | | 6 | SOA | Start of Authority - Provides authoritative information | Preamble + Label Sequence | 129 | | 15 | MX | Mail eXchange - The host of the mail server for a domain | Preamble + 2-bytes for priority + Label Sequence | 130 | | 16 | TXT | Text - Arbitrary text data associated with a domain | Preamble + Text data | 131 | | 28 | AAAA | IPv6 address | Preamble + Sixteen bytes for IPv6 address | 132 | 133 | - We need to update our `QueryType` enum and change our utility functions. We also need to extend our `DnsRecord` for reading new record types and extend the functions foe reading and writing the new type of records. 134 | 135 | Now if we query for *Yahoo.com* with QueryType set as *MX*, we can see our new type of records: 136 | 137 | ``` 138 | cargo run 139 | Compiling DNS-Server-Rust v0.1.0 (/home/akash/Desktop/rust/DNS-Server-Rust) 140 | 141 | warning: `DNS-Server-Rust` (bin "DNS-Server-Rust") generated 2 warnings 142 | Finished dev [unoptimized + debuginfo] target(s) in 0.14s 143 | Running `target/debug/DNS-Server-Rust` 144 | DnsHeader { 145 | id: 6666, 146 | recursion_desired: true, 147 | truncated_message: false, 148 | authoritative_answer: false, 149 | opcode: 0, 150 | response: true, 151 | rescode: NOERROR, 152 | checking_disabled: false, 153 | authed_data: false, 154 | z: false, 155 | recursion_available: true, 156 | questions: 1, 157 | answers: 3, 158 | authoritative_entries: 0, 159 | resource_entries: 0, 160 | } 161 | DnsQuestion { 162 | name: "yahoo.com", 163 | qtype: MX, 164 | } 165 | MX { 166 | domain: "yahoo.com", 167 | priority: 1, 168 | host: "mta5.am0.yahoodns.net", 169 | ttl: 1673, 170 | } 171 | MX { 172 | domain: "yahoo.com", 173 | priority: 1, 174 | host: "mta7.am0.yahoodns.net", 175 | ttl: 1673, 176 | } 177 | MX { 178 | domain: "yahoo.com", 179 | priority: 1, 180 | host: "mta6.am0.yahoodns.net", 181 | ttl: 1673, 182 | } 183 | ``` 184 | 185 | And for *meetakash.vercel.app* with *SOA* query type: 186 | ``` 187 | cargo run 188 | Compiling DNS-Server-Rust v0.1.0 (/home/akash/Desktop/rust/DNS-Server-Rust) 189 | 190 | warning: `DNS-Server-Rust` (bin "DNS-Server-Rust") generated 2 warnings 191 | Finished dev [unoptimized + debuginfo] target(s) in 0.15s 192 | Running `target/debug/DNS-Server-Rust` 193 | DnsHeader { 194 | id: 6666, 195 | recursion_desired: true, 196 | truncated_message: false, 197 | authoritative_answer: false, 198 | opcode: 0, 199 | response: true, 200 | rescode: NOERROR, 201 | checking_disabled: false, 202 | authed_data: false, 203 | z: false, 204 | recursion_available: true, 205 | questions: 1, 206 | answers: 0, 207 | authoritative_entries: 1, 208 | resource_entries: 0, 209 | } 210 | DnsQuestion { 211 | name: "meetakash.vercel.app", 212 | qtype: SOA, 213 | } 214 | SOA { 215 | domain: "vercel.app", 216 | mname: "ns1.vercel-dns.com", 217 | rname: "hostmaster.nsone.net", 218 | serial: 1659373707, 219 | refresh: 43200, 220 | retry: 7200, 221 | expire: 1209600, 222 | minimum: 14400, 223 | ttl: 1800, 224 | } 225 | ``` 226 | 227 | ## Part 4 : Ab banega Actual DNS Server 228 | 229 | There are essentially two types of DNS servers, A DNS server can do both in theory but usually they are mutually exclusive. 230 | 231 | - *Authoritative Server* : A DNS Server hosting one or more "zones".ex: The authoritative servers for the zone google.com are ns1.google.com, ns2.google.com, ns3.google.com and ns4.google.com. 232 | 233 | - *Caching Server* : A DNS server that serves DNS lookups by first checking its chache to see if it already knows of rhe record being requested, and if not performs a recursive lookup. 234 | 235 | First we can implement a server that simply forwards queries to another caching server, i.e. a "DNS proxy server". We will refactor our [main.rs](src/main.rs) by moving our lookup code into a separate function. Along with that, we will write our server code to handle requests. 236 | 237 | Now we can start our server in one terminal and then use `dig` to perform lookup in a second terminal. 238 | 239 | ``` 240 | dig @127.0.0.1 -p 2053 meetakash.vercel.app 241 | 242 | ; <<>> DiG 9.18.18-0ubuntu0.22.04.2-Ubuntu <<>> @127.0.0.1 -p 2053 meetakash.vercel.app 243 | ; (1 server found) 244 | ;; global options: +cmd 245 | ;; Got answer: 246 | ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 37880 247 | ;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 0 248 | 249 | ;; QUESTION SECTION: 250 | ;meetakash.vercel.app. IN A 251 | 252 | ;; ANSWER SECTION: 253 | meetakash.vercel.app. 1800 IN A 76.76.21.93 254 | meetakash.vercel.app. 1800 IN A 76.76.21.164 255 | 256 | ;; Query time: 148 msec 257 | ;; SERVER: 127.0.0.1#2053(127.0.0.1) (UDP) 258 | ;; WHEN: Sat Jun 08 10:26:37 IST 2024 259 | ;; MSG SIZE rcvd: 110 260 | ``` 261 | 262 | And in our server terminal we can see : 263 | ``` 264 | cargo run 265 | Compiling DNS-Server-Rust v0.1.0 (/home/akash/Desktop/rust/DNS-Server-Rust) 266 | warning: `DNS-Server-Rust` (bin "DNS-Server-Rust") generated 1 warning 267 | Finished dev [unoptimized + debuginfo] target(s) in 0.15s 268 | Running `target/debug/DNS-Server-Rust` 269 | Server started successfully on port 2053 270 | Received query: DnsQuestion { name: "google.com", qtype: A } 271 | Answer: A { domain: "google.com", addr: 142.250.196.78, ttl: 245 } 272 | Received query: DnsQuestion { name: "meetakash.vercel.app", qtype: A } 273 | Answer: A { domain: "meetakash.vercel.app", addr: 76.76.21.93, ttl: 1800 } 274 | Answer: A { domain: "meetakash.vercel.app", addr: 76.76.21.164, ttl: 1800 } 275 | ``` 276 | 277 | Lessgooo, we have a DNS server that is able to respond to queries with several different record types!! 278 | 279 | ## Part 5 : Implementing a recursive resolver 280 | 281 | Our server is very nice as it is now, but we are reliant on another server to actually perform the lookup. 282 | 283 | The question is first issued to one of the Internet's 13 root servers. Any resolver will need to know of these 13 servers before hand. A file containing all of them, in bind format, is available on the internet and called [named.root](https://www.internic.net/domain/named.root). These servers all contain the same information, and to get started we can pick one of them at random. 284 | 285 | The flow is simple: 286 | 287 | - The initial query is sent to the root server, which don't know the full `www.google.com` but they do know about `com`, and the reply will tell us where to go next.(This step is usually cached). 288 | 289 | - Next again, we will get a list of shortlisted domains that will point us to the domain. 290 | 291 | - On this lookup, we will get the IP address of our website. 292 | 293 | In practice, a DNS server will maintain a cache, and most TLD's will be known since before. That means that most queries will only ever require two lookups by the server, and commonly one or zero. 294 | 295 | Now we can extend [dnspacket.rs](src/protocol/dnspacket.rs) for recursive lookups. Then, we can implement our recursive lookup and change our `handle_query` function to use our new recursive lookup! 296 | 297 | Great, now we can see the output as : 298 | ``` 299 | dig @127.0.0.1 -p 2053 www.google.com 300 | 301 | ; <<>> DiG 9.18.18-0ubuntu0.22.04.2-Ubuntu <<>> @127.0.0.1 -p 2053 www.google.com 302 | ; (1 server found) 303 | ;; global options: +cmd 304 | ;; Got answer: 305 | ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 35891 306 | ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0 307 | 308 | ;; QUESTION SECTION: 309 | ;www.google.com. IN A 310 | 311 | ;; ANSWER SECTION: 312 | www.google.com. 300 IN A 172.217.163.196 313 | 314 | ;; Query time: 195 msec 315 | ;; SERVER: 127.0.0.1#2053(127.0.0.1) (UDP) 316 | ;; WHEN: Wed Jun 12 13:42:43 IST 2024 317 | ;; MSG SIZE rcvd: 62 318 | 319 | ``` 320 | 321 | And in our server window : 322 | ``` 323 | Finished dev [unoptimized + debuginfo] target(s) in 0.34s 324 | Running `target/debug/DNS-Server-Rust` 325 | Server started successfully on port 2053 326 | Received query: DnsQuestion { name: "www.google.com", qtype: A } 327 | Attempting lookup of A www.google.com with ns 198.41.0.4 328 | Attempting lookup of A www.google.com with ns 192.41.162.30 329 | Attempting lookup of A www.google.com with ns 216.239.34.10 330 | Answer: A { domain: "www.google.com", addr: 172.217.163.196, ttl: 300 } 331 | ``` 332 | 333 | And that's it! That is our DNS server completed! 334 | 335 | ## Contributing 336 | 337 | Contributions are welcome! Please read the [CONTRIBUTING.md](CONTRIBUTING.md) file for more details on how to get involved. 338 | 339 | ## License 340 | 341 | This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. 342 | 343 | 344 | --------------------------------------------------------------------------------